文章会从三个方面进行分析:
- 提出统一异常处理机制的好处,以及该机制使用姿势
- 提供案例:不使用该机制会产生什么样的情况
- 机制背后对应的原理分析(重点)
机制好处及使用姿势
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 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
其含义是通过Spring 的SPI机制,从classpath 所有jar包的configurations = getCandidateConfigurations(annotationMetadata,attributes); 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
注解的方法,保存异常类及对应的异处常理方法
。对应到上述案例一,保存的是BizException
到handleBizException()
方法的映射关系,表明:当业务代码抛出BizException
时,会由handleBizException()
进行处理
private void initExceptionHandlerAdviceCache() {
...
List 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 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> matches = new ArrayList>();
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进行调用,也即是会回调到我们的异常处理器的异常处理方法之中,即处理了异常。
注:
本文限于篇幅原因,不会面面俱到,只重点分析统一异常处理器的生效过程,以及作用过程,摘出其中重点的代码进行分析而忽略了其中的一些分支情况,读者们可自行跟踪代码看看其中的细节处理。