@ControllerAdvice和我的Controller Aspect区别?

1,719 阅读3分钟

业务背景

项目是前后端分离的服务部署架构,后端采用的web框架是Spring MVC,前端页面调用后端服务时,服务入参类型是java.lang.Integer a,由于前端传了个undefined,后端跳转到/error页面,此时我的同事就很郁闷了,为什么我加了controller层的切面,为什么会没有包装好异常返回呢?

为什么没有捕获controller 异常?

很显然我们入参类型不对就会报错,为什么我们在controller 层做了切面还是没有做好异常包装?那么肯定是没有切面生效!那么我们来看看mvc是怎么处理请求流程的。

图1:mvc分发器主流程:org.springframework.web.servlet.DispatcherServlet#doDispatch

  1. 获取HandlerExecutionChain。这个可以看做是对我们业务处理器和拦截器的包装! 其中主要包含业务处理器和实现HandlerInterceptor接口实例列表。我们想在业务处理器前后加上逻辑,就可以实现HandlerInterceptor然后注册到mvc容器中去就可以了,典型的应用有登陆拦截。
  2. 确定当前请求的处理器适配器。MVC框架SPI,允许核心MVC工作流进行参数化配置,我们只要按照一定的格式就可以被MVC调用,比如我们的@RequestMapping注解控制器就会有一个RequestMappingHandlerAdapter(详见invokeHandlerMethod注释),可以通过适配器handle来触发对请求的处理。
  3. 对步骤1中包装的执行链进行前置调用,调用实现了HandlerInterceptor接口实例的preHandle方法。
  4. 真正调用我们业务处理器的地方!此处就是分析我们的议题入口,后续细说。
  5. 对步骤1中包装的执行链进行后置调用,调用实现了HandlerInterceptor接口实例的postHandle方法。 注意:如果步骤4发生异常,该步骤就跳过了,也是唯一实现HandlerInterceptor接口可能不被调用的逻辑
  6. 处理步骤1~5中发生的异常。
  7. 对步骤1中包装的执行链进行前置调用,调用实现了HandlerInterceptor接口实例的afterCompletion方法。

图2:HandlerAdapter调用业务处理器(具体是HandlerMethod即controller中的映射方法)

RequestMappingHandlerAdapter的supports是支持handle HandlerMethod的。所有我们的handle方法最终入参是转化成HandlerMethod类型(详见invokeHandlerMethod注释)。 具体的调用如上图,这是揭开为什么我们的controller切面没有生效的原因!

  1. 从名字可以看出是获取方法参数值。该方法是委托参数解析器去解析参数值,这一步报错的话,就不会走到步骤2的doInvoke(args)即调用具体controller的@requestMapping注解的方法了。所以我们切面也就没有执行到!
  2. doInvoke(args);使用解析出来的参数值去反射调用具体的处理器方法。

图3:doDispatch执行流程异常处理

图中可以看出mvc框架在执行流程中会捕获异常,在try catch之后会有org.springframework.web.servlet.DispatcherServlet#processDispatchResult专门来处理异常。

核心调用如上图,上图会调用mvc容器初始化时注册的HandlerExceptionResolver实例。org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#doResolveHandlerMethodException。该实例中注册有ControllerAdviceBean(@ControllerAdvice注解的类)和ExceptionHandlerMethodResolver(@ExceptionHandler注解的类)的映射关系。 doResolveException方法就可以调用我们在注解@ControllerAdvice的类中定义的处理特定Exception的方,并按自定义返回modelAndView。如果需要返回json数据,可以使用@RestControllerAdvice。

如有错误,欢迎指正!