Spring MVC统一异常处理及原理分析

文章会从三个方面进行分析:

  1. 提出统一异常处理机制的好处,以及该机制使用姿势
  2. 提供案例:不使用该机制会产生什么样的情况
  3. 机制背后对应的原理分析(重点)

机制好处及使用姿势

Spring MVC为我们的WEB应用提供了统一异常处理机制,其好处是:

  1. 业务逻辑和异常处理解耦(业务代码不应该过多地关注异常的处理[职责单一原则])
  2. 消除充斥各处的try catch块代码,使代码更整洁
  3. 便于统一向前端、客户端返回友好的错误提示
使用姿势如下
@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隐含的前提条件如下

  1. 使用Lombok(当然,也可以手动获取Logger)
  2. GlobalExceptionHandler需要被@ControllerAdvice(Spring 3.2+)或@RestControllerAdvice(Spring 4.3+)注解,并且能够被Spring扫描到

为配合解释该解决方案,再提供一些基础信息

  1. 业务异常类
  2. 响应信息包装类
// 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

原理分析

了解存在的问题,以及对应的解决方案之后,接下来分析统一异常处理的工作原理

前提假设:

  1. 原理分析基于Spring Boot 1.5.19.RELEASE,对应的springframework版本为4.3.22.RELEASE
  2. 理解Spring Boot自动装配原理

先来分析Spring Boot是如何使GlobalExceptionHandler生效的,步骤如下:

  1. 启动类(XXXApplication)@SpringBootApplication注解,而@SpringBootApplication又被@EnableAutoConfiguration所注解,@EnableAutoConfiguration导入EnableAutoConfigurationImportSelector
  2. EnableAutoConfigurationImportSelector实现了ImportSelector接口,其核心方法是selectImports,在该方法中有一行代码是List configurations = getCandidateConfigurations(annotationMetadata,attributes);其含义是通过Spring 的SPI机制,从classpath 所有jar包的META-INF/spring.factories文件中,找到EnableAutoConfiguration对应的"一堆"类[自动装配原理]。这些类作为selectImports的返回值,后期会被Spring加载并实例化,并置入IOC容器中,其中有一项为WebMvcAutoConfiguration
  3. WebMvcAutoConfiguration类存在内部类WebMvcAutoConfigurationAdapter,内部类将导入EnableWebMvcConfiguration(WebMvcConfigurationSupport的子类)
  4. WebMvcConfigurationSupport有个factory methodhandlerExceptionResolver(),该方法向Spring容器中注册了一个HandlerExceptionResolverComposite(实现HandlerExceptionResolver接口),并且默认情况下,给该Composite类添加了三个HandlerExceptionResolver,其中有一个类为ExceptionHandlerExceptionResolver
  5. ExceptionHandlerExceptionResolverInitializingBean的回调方法afterPropertiesSet中,调用initExceptionHandlerAdviceCache()方法进行异常处理器通知缓存的初始化:查找IOC容器中,所有被@ControllerAdvice注解的Bean,如果Bean中存在异常映射,则该Bean会作为key,对应的ExceptionHandlerMethodResolver作为value被缓存起来
  6. ExceptionHandlerMethodResolver是真正干活的类,用于解析被@ExceptionHandler注解的方法,保存异常类及对应的异处常理方法。对应到上述案例一,保存的是BizExceptionhandleBizException()方法的映射关系,表明:当业务代码抛出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方法中抛出异常后,步骤如下:

  1. 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。因此,这里会调用HandlerExceptionResolverCompositeresolveException方法进行异常的处理

  1. XXXComposite在Spring中是个组合类,一般内部会维护一个由Composite父接口实例构成的列表,如HandlerExceptionResolverComposite实现了HandlerExceptionResolver接口,其内部维护了一个HandlerExceptionResolver集合。HandlerExceptionResolverCompositeresolveException方法同样是迭代其内部维护的集合,并依次调用其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;
}
  1. 根据抛出的异常类型,拿到异常处理器及对应的异常处理方法,并转化成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 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 exceptionType) {
    List> matches = new ArrayList>();
    for (Class 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有两个:BizExceptionException,都满足mappedException.isAssignableFrom(exceptionType)条件,均会被加入matches中,经过排序之后,"最匹配"的BizException会排在matchs集合的第一个位置,所以会选择它所对应的异常处理方法返回。因此,"最匹配"的关键点就在于比较器ExceptionDepthComparator,根据类名,可以推测其出比较的依据是目标异常类与待排序异常类的"深度"。

举个例子,假设目标异常类为BizException,而待排序的集合中分别有BizExceptionRuntimeExceptionException,那么他们之间的深度分别为0,1,2,因此,排序之后,集合中的BizException与目标异常类BizException最为匹配,排在了集合中首位,RuntimeException次匹配,排在了集合的第二位,Exception最不匹配,排在集合的第三位。

public int compare(Class o1, Class 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);
}

总结:

  1. Spring Boot应用启动时,会扫描被@ControllerAdvice注解的Bean,找到其内部被@ExceptionHandler注解的方法,解析其所能处理的异常类,并缓存到exceptionHandlerAdviceCache

  2. 当HTTP请求在Controller中发生异常,会被DispatcherServlet捕获,并调用ExceptionHandlerExceptionResolver#resolveException进行异常的解析,解析的过程依赖exceptionHandlerAdviceCache进行真正的异常处理方法的查找,找到之后封装成ServletInvocableHandlerMethod,然后被Spring进行调用,也即是会回调到我们的异常处理器的异常处理方法之中,即处理了异常。

注:

本文限于篇幅原因,不会面面俱到,只重点分析统一异常处理器的生效过程,以及作用过程,摘出其中重点的代码进行分析而忽略了其中的一些分支情况,读者们可自行跟踪代码看看其中的细节处理。

你可能感兴趣的:(Spring MVC统一异常处理及原理分析)