文章会从三个方面进行分析:
- 提出统一异常处理机制的好处,以及该机制使用姿势
- 提供案例:不使用该机制会产生什么样的情况
- 机制背后对应的原理分析(重点)
机制好处及使用姿势
Spring MVC为我们的WEB应用提供了统一异常处理机制,其好处是:
- 业务逻辑和异常处理解耦(业务代码不应该过多地关注异常的处理[职责单一原则])
- 消除充斥各处的
try catch块代码,使代码更整洁 - 便于统一向前端、客户端返回友好的错误提示
使用姿势如下
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // Http Status Code 500
public ResponseDTO handleException(Exception e) {
// 兜底逻辑,通常用于处理未预期的异常,比如不知道哪儿冒出来的空指针异常
log.error("", e);
return ResponseDTO.failedResponse().withErrorMessage("服务器开小差了");
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST) // Http Status Code 400
public ResponseDTO handleBizException(BizException e) {
// 可预期的业务异常,根据实际情况,决定是否要打印异常堆栈
log.warn("业务异常:{}", e);
return ResponseDTO.failedResponse().withErrorMessage(e.getMessage());
}
}
注:该demo隐含的前提条件如下
- 使用Lombok(当然,也可以手动获取Logger)
-
GlobalExceptionHandler需要被@ControllerAdvice(Spring 3.2+)或@RestControllerAdvice(Spring 4.3+)注解,并且能够被Spring扫描到
为配合解释该解决方案,再提供一些基础信息
- 业务异常类
- 响应信息包装类
// 1
public class BizException extends RuntimeException {
public BizException(String message) {
super(message);
}
}
// 2
@Data
public class ResponseDTO<T> implements Serializable {
private static final long serialVersionUID = -3436143993984825439L;
private boolean ok = false;
private T data;
private String errorMessage = "";
public static ResponseDTO successResponse() {
ResponseDTO message = new ResponseDTO();
message.setOk(true);
return message;
}
public static ResponseDTO failedResponse() {
ResponseDTO message = new ResponseDTO();
message.setOk(false);
return message;
}
public ResponseDTO withData(T data) {
this.data = data;
return this;
}
public ResponseDTO withErrorMessage(String errorMsg) {
this.errorMessage = errorMsg;
return this;
}
}
案例分析
案例分析一:
@GetMapping("/testBizException")
public ResponseDTO testBizException() {
if (checkFailed) {
throw new BizException("test BizException");
}
}
当我们请求/testBizException时,该接口在校验失败后抛出了一个BizException,用以代表我们的业务异常,比如参数校验失败(解决方案还有JSR-303的Bean Validation,在此不讨论),优惠券已过期等等业务异常信息。如果没有统一异常处理,我们可能会使用如下方式
try {
// check
} catch (BizException e) {
return ResponseDTO.failedResponse().withErrorMessage("test BizException");
}
这种方式,一是不优雅,二是业务逻辑跟异常处理耦合在了一起。
使用统一异常处理之后,直接抛出业务异常,并提供异常上下文(message + errorCode),代码会流转到GlobalExceptionHandler#handleBizException,统一打印业务日志以及返回错误码和业务异常信息,且Http Status Code 返回400。
案例分析二:
@GetMapping("/testUnExpectedException")
public ResponseDTO testUnExpectedException() {
int i = 1 / 0;
}
当我们请求/testUnExpectedException时,该接口会抛出java.lang.ArithmeticException: / by zero,用以代表未预期的异常,比如该案例中的0除异常,仅管此处能一眼辩识出来,但更多的时候,0由变量表示,很容易被忽视,又或者是其它未预期的空指针异常等。当不知道哪里有可能会出异常,又为了前端友好提示,其中一个做法就是try catch大包大揽,将整个方法都try catch住,于是代码产生了腐朽的味道。
try {
// do business
} catch (Exception e) {
log.error("xxx", e);
return ResponseDTO.failedResponse().withErrorMessage("服务器开小差");
}
使用统一异常处理之后,业务代码里不再充斥(滥用)try catch块,只需要关心业务逻辑,当出现不可预期的异常时,代码会流转到GlobalExceptionHandler#handleException,统一打印异常堆栈,以及返回错误码和统一异常信息,且Http Status Code 返回500。
以上便是Spring MVC为我们提供的统一异常处理机制,我们可以好好加以利用。实际上,该机制在很多公司都在使用,可以从一些开源代码管中窥豹,其中著名的代表就有Apollo,参考com.ctrip.framework.apollo.common.controller.GlobalDefaultExceptionHandler
原理分析
了解存在的问题,以及对应的解决方案之后,接下来分析统一异常处理的工作原理
前提假设:
- 原理分析基于Spring Boot
1.5.19.RELEASE,对应的springframework版本为4.3.22.RELEASE - 理解Spring Boot自动装配原理
先来分析Spring Boot是如何使GlobalExceptionHandler生效的,步骤如下:
- 启动类
(XXXApplication)被@SpringBootApplication注解,而@SpringBootApplication又被@EnableAutoConfiguration所注解,@EnableAutoConfiguration导入EnableAutoConfigurationImportSelector -
EnableAutoConfigurationImportSelector实现了ImportSelector接口,其核心方法是selectImports,在该方法中有一行代码是List<String> configurations = getCandidateConfigurations(annotationMetadata,attributes);其含义是通过Spring 的SPI机制,从classpath 所有jar包的META-INF/spring.factories文件中,找到EnableAutoConfiguration对应的"一堆"类[自动装配原理]。这些类作为selectImports的返回值,后期会被Spring加载并实例化,并置入IOC容器中,其中有一项为WebMvcAutoConfiguration -
WebMvcAutoConfiguration类存在内部类WebMvcAutoConfigurationAdapter,内部类将导入EnableWebMvcConfiguration(WebMvcConfigurationSupport的子类) -
WebMvcConfigurationSupport有个factory methodhandlerExceptionResolver(),该方法向Spring容器中注册了一个HandlerExceptionResolverComposite(实现HandlerExceptionResolver接口),并且默认情况下,给该Composite类添加了三个HandlerExceptionResolver,其中有一个类为ExceptionHandlerExceptionResolver -
ExceptionHandlerExceptionResolver在InitializingBean的回调方法afterPropertiesSet中,调用initExceptionHandlerAdviceCache()方法进行异常处理器通知缓存的初始化:查找IOC容器中,所有被@ControllerAdvice注解的Bean,如果Bean中存在异常映射,则该Bean会作为key,对应的ExceptionHandlerMethodResolver作为value被缓存起来 -
ExceptionHandlerMethodResolver是真正干活的类,用于解析被@ExceptionHandler注解的方法,保存异常类及对应的异处常理方法<exceptionType, method>。对应到上述案例一,保存的是BizException到handleBizException()方法的映射关系,表明:当业务代码抛出BizException时,会由handleBizException()进行处理
private void initExceptionHandlerAdviceCache() {
...
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
AnnotationAwareOrderComparator.sort(adviceBeans);
for (ControllerAdviceBean adviceBean : adviceBeans) {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
...
}
...
}
}
应用启动完毕之后,GlobalExceptionHandler已经生效,即exceptionHandlerAdviceCache已经缓存了异常处理器及其对应的ExceptionHandlerMethodResolver,一旦发生了异常,会从exceptionHandlerAdviceCache里依次判断哪个异常处理器可以用,并找到对应的异常处理方法进行异常的处理。
接着分析异常处理的具体流程,当一个Controller方法中抛出异常后,步骤如下:
- org.springframework.web.servlet.DispatcherServlet#doDispatch会catch住异常,并调用
processDispatchResult();方法进行异常的处理
// DispatcherServlet#processHandlerException
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// DispatcherServlet#processHandlerException
for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
这里的this.handlerExceptionResolvers是在Spring Boot启动的过程中初始化的,其中就包含上述启动步骤4中的HandlerExceptionResolverComposite。因此,这里会调用HandlerExceptionResolverComposite的resolveException方法进行异常的处理
-
XXXComposite在Spring中是个组合类,一般内部会维护一个由Composite父接口实例构成的列表,如HandlerExceptionResolverComposite实现了HandlerExceptionResolver接口,其内部维护了一个HandlerExceptionResolver集合。HandlerExceptionResolverComposite的resolveException方法同样是迭代其内部维护的集合,并依次调用其resolveException方法进行解析,
其内部集合中有一个ExceptionHandlerExceptionResolver实例,且首先会进入该实例进行处理
// HandlerExceptionResolverComposite#resolveException
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,Exception ex) {
if (this.resolvers != null) {
for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {
ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (mav != null) {
return mav;
}
}
}
return null;
}
- 根据抛出的异常类型,拿到异常处理器及对应的异常处理方法,并转化成ServletInvocableHandlerMethod,并执行
invokeAndHandle方法,也即是说,最终会转换成执行异常处理器的异常处理方法。(org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle是Spring MVC处理Http请求中的重要方法,篇幅原因不在此介绍其原理)
// ExceptionHandlerExceptionResolver#doResolveHandlerMethodException
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
...
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
...
// ExceptionHandlerExceptionResolver#getExceptionHandlerMethod
// 这段逻辑是@ExceptionHandler写在Controller类里的处理方式,这种方式不通用也不常用,不做介绍
...
for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
ControllerAdviceBean advice = entry.getKey();
// @ControllerAdvice注解可以指定仅拦截某些类,这里判断handlerType是否在其作用域内
if (advice.isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = entry.getValue();
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
}
}
}
根据异常,找到异常处理方法
// ExceptionHandlerMethodResolver#resolveMethodByExceptionType
public Method resolveMethod(Exception exception) {
Method method = resolveMethodByExceptionType(exception.getClass());
if (method == null) {
Throwable cause = exception.getCause();
if (cause != null) {
method = resolveMethodByExceptionType(cause.getClass());
}
}
return method;
}
// ExceptionHandlerMethodResolver#resolveMethodByExceptionType
public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
Method method = this.exceptionLookupCache.get(exceptionType);
if (method == null) {
// 核心方法
method = getMappedMethod(exceptionType);
this.exceptionLookupCache.put(exceptionType, (method != null ? method : NO_METHOD_FOUND));
}
return (method != NO_METHOD_FOUND ? method : null);
}
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>();
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
if (mappedException.isAssignableFrom(exceptionType)) {
matches.add(mappedException);
}
}
// 如果找到多个匹配的异常,就排序之后取第一个(最优的)
if (!matches.isEmpty()) {
Collections.sort(matches, new ExceptionDepthComparator(exceptionType));
return this.mappedMethods.get(matches.get(0));
}
else {
return null;
}
}
案例中,我们的mappedException有两个:BizException与Exception,都满足mappedException.isAssignableFrom(exceptionType)条件,均会被加入matches中,经过排序之后,"最匹配"的BizException会排在matchs集合的第一个位置,所以会选择它所对应的异常处理方法返回。因此,"最匹配"的关键点就在于比较器ExceptionDepthComparator,根据类名,可以推测其出比较的依据是目标异常类与待排序异常类的"深度"。
举个例子,假设目标异常类为BizException,而待排序的集合中分别有BizException、RuntimeException、Exception,那么他们之间的深度分别为0,1,2,因此,排序之后,集合中的BizException与目标异常类BizException最为匹配,排在了集合中首位,RuntimeException次匹配,排在了集合的第二位,Exception最不匹配,排在集合的第三位。
public int compare(Class<? extends Throwable> o1, Class<? extends Throwable> o2) {
int depth1 = getDepth(o1, this.targetException, 0);
int depth2 = getDepth(o2, this.targetException, 0);
return (depth1 - depth2);
}
private int getDepth(Class<?> declaredException, Class<?> exceptionToMatch, int depth) {
if (exceptionToMatch.equals(declaredException)) {
// Found it!
return depth;
}
// If we've gone as far as we can go and haven't found it...
if (exceptionToMatch == Throwable.class) {
return Integer.MAX_VALUE;
}
return getDepth(declaredException, exceptionToMatch.getSuperclass(), depth + 1);
}
总结:
Spring Boot应用启动时,会扫描被
@ControllerAdvice注解的Bean,找到其内部被@ExceptionHandler注解的方法,解析其所能处理的异常类,并缓存到exceptionHandlerAdviceCache当HTTP请求在Controller中发生异常,会被DispatcherServlet捕获,并调用
ExceptionHandlerExceptionResolver#resolveException进行异常的解析,解析的过程依赖exceptionHandlerAdviceCache进行真正的异常处理方法的查找,找到之后封装成ServletInvocableHandlerMethod,然后被Spring进行调用,也即是会回调到我们的异常处理器的异常处理方法之中,即处理了异常。
注:
本文限于篇幅原因,不会面面俱到,只重点分析统一异常处理器的生效过程,以及作用过程,摘出其中重点的代码进行分析而忽略了其中的一些分支情况,读者们可自行跟踪代码看看其中的细节处理。