本文已收录在Github关注我,紧跟本系列专栏文章,咱们下篇再续!

  • 🚀 魔都架构师 | 全网30W技术追随者
  • 🔧 大厂分布式系统/数据中台实战专家
  • 🏆 主导交易系统百万级流量调优 & 车联网平台架构
  • 🧠 AIGC应用开发先行者 | 区块链落地实践者
  • 🌍 以技术驱动创新,我们的征途是改变世界!
  • 👉 实战干货:编程严选网

1 错误场景

public class UserController {

    public UserController() {
        log.info("construct");
    }

    @GetMapping("/reg/{name}")
    @ResponseBody
    public String saveUser(String name) {
        log.info("register JavaEdge success!");
        return "OK";
    }
}

验证请求的Token合法性的Filter。Token校验失败时,直接抛自定义异常,移交给Spring处理:

@WebFilter
@Component
@Slf4j
public class PermissionFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        String token = httpServletRequest.getHeader("token");
        if (!"JavaEdge".equals(token)) {
            log.info("throw IllegalRequestException");
            resolver.resolveException(httpServletRequest, httpServletResponse, null,
                    new IllegalRequestException());
        }
        chain.doFilter(request, response);
    }
}
public class IllegalRequestException extends RuntimeException {

    public IllegalRequestException() {
        super();
    }
}
@RestControllerAdvice
public class IllegalRequestExceptionHandler {

    @ExceptionHandler(IllegalRequestException.class)
    @ResponseBody
    public String handle() {
        System.out.println("403");
        return "{\"code\": 403}";
    }
}

测试HTTP请求:

阿里四面:Spring Exception的原理你精通了吗?_1024程序员节

日志输出如下:说明IllegalRequestExceptionHandler未生效。

阿里四面:Spring Exception的原理你精通了吗?_1024程序员节_02

why?需精通Spring异常处理流程。

2 解析

阿里四面:Spring Exception的原理你精通了吗?_原力计划_03

当所有Filter执行完毕,Spring才处理Servlet相关,在DispatcherServlet,Spring处理了请求和处理器的对应关系及统一异常处理

Filter内异常无法被统一处理,因为异常处理发生在DispatcherServlet#doDispatch(),但此时,过滤器已全部执行完

3 Spring异常统一处理

3.1 Spring加载并暴露ControllerAdvice

WebMvcConfigurationSupport#handlerExceptionResolver()

实例化并注册一个ExceptionHandlerExceptionResolver:

@Bean
public HandlerExceptionResolver handlerExceptionResolver(
    @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
  List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
  configureHandlerExceptionResolvers(exceptionResolvers);
  if (exceptionResolvers.isEmpty()) {
    // 添加默认异常解析器
    addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager);
  }
  extendHandlerExceptionResolvers(exceptionResolvers);
  HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
  composite.setOrder(0);
  composite.setExceptionResolvers(exceptionResolvers);
  return composite;
}

最终,Spring实例化ExceptionHandlerExceptionResolver类:

public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
  implements ApplicationContextAware, InitializingBean {

  @Override
  public void afterPropertiesSet() {
    initExceptionHandlerAdviceCache();
    ...
 }

initExceptionHandlerAdviceCache

完成所有ControllerAdvice中的ExceptionHandler初始化:查找 @ControllerAdvice 注解的Bean集,放入exceptionHandlerAdviceCache。这里看到自定义illegalRequestExceptionHandler:

阿里四面:Spring Exception的原理你精通了吗?_异常处理_04

private void initExceptionHandlerAdviceCache() {
  ...
  List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
  for (ControllerAdviceBean adviceBean : adviceBeans) {
    Class<?> beanType = adviceBean.getBeanType();
    if (beanType == null) {
      throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
    }
    ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
    if (resolver.hasExceptionMappings()) {
      this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
    }
    ...
}

所有被 @ControllerAdvice 注解的异常处理器,都在 ExceptionHandlerExceptionResolver 实例化时自动扫描并装载在其exceptionHandlerAdviceCache。

initHandlerExceptionResolvers

当第一次客户端请求发生时,DispatcherServlet#initHandlerExceptionResolvers() 获取所有注册到 Spring 的 HandlerExceptionResolver 实例(如ExceptionHandlerExceptionResolver),存到handlerExceptionResolvers

/** List of HandlerExceptionResolvers used by this servlet. */
@Nullable
private List<HandlerExceptionResolver> handlerExceptionResolvers;
private void initHandlerExceptionResolvers(ApplicationContext context) {
  this.handlerExceptionResolvers = null;

  if (this.detectAllHandlerExceptionResolvers) {
    // Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
    Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
        .beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
    if (!matchingBeans.isEmpty()) {
      this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
      // We keep HandlerExceptionResolvers in sorted order.
      AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
    }
  }
  ...
}

3.2 ControllerAdvice如何被Spring消费并处理异常?

3.2.1 DispatcherServlet

① doDispatch()
protected void doDispatch(HttpServletRequest request, HttpServletResponse response)
        throws Exception {
    // ...
    try {
        ModelAndView mv = null;
        Exception dispatchException = null;
        try {
            // ...
            // 查找当前请求对应的 handler, 并执行
            // ...
        }
        catch (Exception ex) {
           // 先赋值 
            dispatchException = ex;
        }
        catch (Throwable err) {
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }
       // 再移交
        processDispatchResult(processedRequest, response, mappedHandler, mv,
                dispatchException);
    }
    // ...
}
② processDispatchResult
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
    @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
    @Nullable Exception exception) throws Exception {

    boolean errorView = false;

    if (exception != null) {
        if (exception instanceof ModelAndViewDefiningException) {
            logger.debug("ModelAndViewDefiningException encountered", exception);
            mv = ((ModelAndViewDefiningException) exception).getModelAndView();
        }
        else {
            Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
           // 当Exception非空时,继续移交
            mv = processHandlerException(request, response, handler, exception);
            errorView = (mv != null);
        }
    }
}
③ processHandlerException
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception {

    // Success and error responses may use different content types
    request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

    // Check registered HandlerExceptionResolvers...
    ModelAndView exMv = null;
    if (this.handlerExceptionResolvers != null) {
        for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
           // 从 handlerExceptionResolvers 获取有效的异常解析器以解析异常
            exMv = resolver.resolveException(request, response, handler, ex);
            if (exMv != null) {
                break;
            }
        }
    }
}

这里的 handlerExceptionResolvers 一定包含声明的IllegalRequestExceptionHandler#IllegalRequestException 的异常处理器的 ExceptionHandlerExceptionResolver 包装类。

4 修正

为利用到 Spring MVC 异常处理机制,改造Filter:

  • 手动捕获异常
  • 将异常通过 HandlerExceptionResolver 解析处理

修改 PermissionFilter,注入 HandlerExceptionResolver:

@WebFilter
@Component
@Slf4j
public class PermissionFilter implements Filter {

    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;

然后,在 doFilter 捕获异常并移交 HandlerExceptionResolver:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    HttpServletResponse httpServletResponse = (HttpServletResponse) response;

    String token = httpServletRequest.getHeader("token");
    if (!"JavaEdge".equals(token)) {
        log.info("throw IllegalRequestException");
       // see! 
        resolver.resolveException(httpServletRequest, httpServletResponse, null,
                new IllegalRequestException());
        return;
        // see! 
    }
    chain.doFilter(request, response);
}

再用错误 Token 请求,日志:

[21:40:21.095] [http-nio-12345-exec-1] [INFO ] [c.j.spring.exception.PermissionFilter:35 ] - throw IllegalRequestException 403

响应体:

{
 "code": 403
}