第八篇:跨控制器间共享 - @ControllerAdvice
第八篇:跨控制器间共享 - @ControllerAdvice
官方文档
参考资料
-
@InitBinder
:《SpringMVC- 第三篇:在控制器方法中获取请求参数和构建响应》中 -
@ModelAttribute
:《SpringMVC- 第四篇:域对象共享数据》中 -
@ExceptionHandler
:《SpringMVC- 第七篇:控制器方法异常处理》中
@ControllerAdvice
@ControllerAdvice 修饰的类专门用于声明在多个@Controller 中被共享的@ExceptionHandler, @InitBinder 和 @ModelAttribute 修饰的方法。
@ControllerAdvice 修饰的类也是一个 bean 组件,多个@ControllerAdvice 修饰的 bean 可以通过@Order 控制顺序。
ControllerAdviceBean 的作用是封装@ControllerAdvice 修饰的 SpringIOC 管理的 bean 的信息,而不需要实例化这个 bean
ControllerAdviceBean 的 findAnnotatedBeans(ApplicationContext) 方法可用于发现@ControllerAdvice 注解修饰 bean 并将其转化为 ControllerAdviceBean。但是 ControllerAdviceBean 可以从基于任意对象创建,包括没有@ControllerAdvice 注解修饰的类。
通过查看 ControllerAdviceBean 在哪里被引用,我们可以很快发现:
@ControllerAdvice
中的@InitBinder 和@ModelAttribute 修饰的方法会在RequestMappingHandlerAdapter
的initControllerAdviceCache
方法中进行缓存,然后留作后用。
@ControllerAdvice
中的@ExceptionHandler 修饰的方法,在ExceptionHandlerExceptionResolver
的initExceptionHandlerAdviceCache
方法中进行缓存这两个方法真的非常相像。
我们会在后面的小节进行详细的代码分析。
ControllerAdviceBean
还在RequestResponseBodyAdviceChain
中使用到,用于将RequestBodyAdvice
和ResponseBodyAdvice
,转化为ControllerAdviceBean
类型,先放着,以后再了解。
在 SpringMVC 启动时,RequestMappingHandlerMapping
和 ExceptionHandlerExceptionResolver
检测 controller advice 类型的 bean 并在运行时应用它们。来自 @ControllerAdvice
的全局 @ExceptionHandler
方法优先级在抛出错误的控制器方法所在的@Controller 类中的 @ExceptionHandler
方法之后。相反,来自 @ControllerAdvice
全局的 @ModelAttribute
和 @InitBinder
方法的优先级在控制器方法所在的@Controller 类中的本地方法之前。
@ControllerAdvice 自身的配置
@ControllerAdvice 生效的范围控制
默认情况下,@ControllerAdvice
中的方法全局地应用于所有控制器(@Controller)。使用像 annotations
,assignableTypes
,basePackageClasses
, 和 basePackages
(别名为 value
) 这样的属性(又叫 selectors 选择器)来定义更窄的目标控制器(@Controller)集合。如果声明了多个选择器,则应用或逻辑进行组合,这意味着 Controller 应该至少匹配一个选择器。注意,选择器检查是在运行时执行的,因此添加许多选择器可能会对性能产生负面影响并增加复杂性。
-
annotations
控制器至少带有一个此配置指定的注解 -
assignableTypes
控制器至少与此配置中配置的一个类同类型或者为其子类或者为其实现类 -
basePackageClasses
此配置中配置的所有的类或者接口所在的包(及其子包)中的控制器都是目标控制器 -
basePackages
或者value
此配置中指定的包(及其子包)中的控制器都是目标控制器,basePackageClasses
是其类型安全的替代配置。
// 所有被@RestController修饰的 Controller 都是目标 Controller
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// 对应 value 配置,省略了
// "org.example.controllers"包及其子包中下的所有Controller 都是目标 Controller
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// 实现了 ControllerInterface 的控制器或者继承了 AbstractController 的 Controller 都是目标 Controller
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
// 标记类PackageMarker所在的包及其子包中的所有 Controller 都是目标 Controller
@ControllerAdvice(basePackageClasses = {PackageMarker.class})
public class ExampleAdvice3 {}
这个比较简单,就不写代码测试了。
简单分析一下,这些配置都在 ControllerAdviceBean
的 createBeanTypePredicate
中使用,
private static HandlerTypePredicate createBeanTypePredicate(@Nullable ControllerAdvice controllerAdvice) {
if (controllerAdvice != null) {
return HandlerTypePredicate.builder()
.basePackage(controllerAdvice.basePackages())
.basePackageClass(controllerAdvice.basePackageClasses())
.assignableType(controllerAdvice.assignableTypes())
.annotation(controllerAdvice.annotations())
.build();
}
return HandlerTypePredicate.forAnyHandlerType();
}
此方法专门生成 HandlerTypePredicate
,专门用来根据选择器(前面提到的 4 个属性)判断控制器是否为目标控制器。这个 HandlerTypePredicate
也很简单,实现函数式接口 Predicate
,实现 Predicate
接口唯一需要实现的 test 方法,传入控制器的类型,Class 对象,输出 boolean,true 表示是目标控制器,false 表是不是。
public boolean test(Class<?> controllerType) {
if (!hasSelectors()) {
return true;
}
else if (controllerType != null) {
for (String basePackage : this.basePackages) {
if (controllerType.getName().startsWith(basePackage)) {
return true;
}
}
for (Class<?> clazz : this.assignableTypes) {
if (ClassUtils.isAssignable(clazz, controllerType)) {
return true;
}
}
for (Class<? extends Annotation> annotationClass : this.annotations) {
if (AnnotationUtils.findAnnotation(controllerType, annotationClass) != null) {
return true;
}
}
}
return false;
}
多个@ControllerAdvice 之间的顺序
这里我们只学习用 @Order
注解来设置 ControllerAdvice 之间的顺序,其他的关于顺序之间的注意点,看@ControllerAdvice 的源码注释。
@Order
注解的值越小表示越靠前,优先级越大,默认值是 Ordered.LOWEST_PRECEDENCE
,只为 Integer.MAX_VALUE
,int 的最大值,表示最低优先级
具体使用结合@InitBinder、@ModelAttribute、@ExceptionHandler 修饰的方法来看
@ControllerAdvice 内部的方法
@ModelAttribute 方法和@InitBinder 方法
这两个方法的初始化相关的源码分析,因为他们是一起初始化的,所以放在一起说
通过查看 ControllerAdviceBean
在哪里被引用,我们可以很快发现了 RequestMappingHandlerAdapter
。
首先 RequestMappingHandlerAdapter
也是一个 bean,实现了 InitializingBean
接口,实现 afterPropertiesSet
方法,在其中做一些初始化操作
public void afterPropertiesSet() {
// 检查是否有全局的 @ModelAttribute方法和@InitBinder方法,并进行缓存,
// 检查有没有实现了 RequestBodyAdvice 和 ResponseBodyAdvice 的 ControllerAdviceBean 类,并进行缓存
initControllerAdviceCache();
if (this.argumentResolvers == null) {
// 初始化参数解析器
// 如果没有初始化 argumentResolvers,则 调用 getDefaultArgumentResolvers添加默认的参数解析器,一般有26个
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.initBinderArgumentResolvers == null) {
// 初始化专用于 @InitBinder方法 的参数解析器
// 如果没有初始化 initBinderArgumentResolvers 调用 getDefaultInitBinderArgumentResolvers 添加默认的参数解析器,一般有13个
List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
// 初始化返回值处理器
// 如果没有初始化 returnValueHandlers 调用 getDefaultReturnValueHandlers 添加默认的返回值处理器,一般有15个
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
这里重点看 initControllerAdviceCache
方法,@ControllerAdvice
中的 @InitBinder
和 @ModelAttribute
修饰的方法会在 RequestMappingHandlerAdapter
的 initControllerAdviceCache
方法中进行缓存,存放到 modelAttributeAdviceCache
和 initBinderAdviceCache
中,同时将请求体响应体的通知也进行了缓存,存放到 requestResponseBodyAdvice
。
@ControllerAdvice
注释的 bean 是否实现RequestBodyAdvice
接口或者ResponseBodyAdvice
接口,实现了就会被添加到requestResponseBodyAdvice
中,将来在RequestResponseBodyAdviceChain
中使用。先放着,以后再了解。TODO
RequestResponseBodyAdviceChain
的一个场景就是处理就是在处理@ResponseBody
结果的时候,@ResponseBody
的处理类是RequestResponseBodyMethodProcessor
,RequestResponseBodyMethodProcessor
的handleReturnValue
方法调用其父类AbstractMessageConverterMethodProcessor
的writeWithMessageConverters
方法,在writeWithMessageConverters
最终写入body
之前,调用了getAdvice()
方法,
private void initControllerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
// 在给定的ApplicationContext中查找带有@ControllerAdvice注释的bean,并将它们包装为ControllerAdviceBean实例。
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
// 根据筛选器选择给定目标类型上的方法。
// MODEL_ATTRIBUTE_METHODS 用于筛选不被@RequestMapping修饰,且被@ModelAttribute修饰的方法
// 排除@RequestMapping注解是因为@ModelAttribute注解可以跟@RequestMapping注解一起使用,具体请看《SpringMVC-第四篇:域对象共享数据》
Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
// 添加到缓存中
this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
}
// INIT_BINDER_METHODS 用于筛选被@InitBinder修饰的方法
Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
if (!binderMethods.isEmpty()) {
// 添加到缓存中
this.initBinderAdviceCache.put(adviceBean, binderMethods);
}
// @ControllerAdvice注释的bean是否实现 RequestBodyAdvice接口或者 ResponseBodyAdvice接口
if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
requestResponseBodyAdviceBeans.add(adviceBean);
}
}
if (!requestResponseBodyAdviceBeans.isEmpty()) {
// 添加到缓存中
this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
// 在RequestResponseBodyAdviceChain中,处理 RequestBodyAdvice 和 ResponseBodyAdvice,会判断 RequestBodyAdvice 或者 ResponseBodyAdvice,会判断是不是 ControllerAdviceBean 类型
}
......
// 日志
}
此外,看完了 initControllerAdviceCache
方法,我们再看看 getDefaultInitBinderArgumentResolvers
方法,这个方法的作用是初始化专用于 @InitBinder
方法的参数解析器,只有添加了相应的参数解析器,才能使用特定类型的参数或者使用特定的注解修饰参数。
private List<HandlerMethodArgumentResolver> getDefaultInitBinderArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(20);
// 注解修饰的参数的解析器
// @RequestParam注解
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver());
// @PathVariable注解
resolvers.add(new PathVariableMethodArgumentResolver());
resolvers.add(new PathVariableMapMethodArgumentResolver());
// @MatrixVariable注解
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
// @Value注解
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
// @SessionAttribute注解
resolvers.add(new SessionAttributeMethodArgumentResolver());
// @RequestAttribute注解
resolvers.add(new RequestAttributeMethodArgumentResolver());
// 特定类型的参数的解析器
// WebRequest ServletRequest MultipartRequest HttpSession InputStream Reader HttpMethod Locale 等等
resolvers.add(new ServletRequestMethodArgumentResolver());
// ServletResponse OutputStream Writer
resolvers.add(new ServletResponseMethodArgumentResolver());
// 添加自定义的参数的解析器,即 RequestMappingHandlerAdapter 的 customArgumentResolvers,可在实例化后,注册到ICO容器前 调用setCustomArgumentResolvers方法添加
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}
// Catch-all
resolvers.add(new PrincipalMethodArgumentResolver());
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
return resolvers;
}
RequestMappingHandlerAdapter
又是什么时候生效呢?
在 DispatcherServlet
的 doDispatch
方法中,通过 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
获取 HandlerAdapter
(控制器方法适配器),
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
......
// 确定使用哪个 HandlerAdapter,一般都是使用 RequestMappingHandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
......
// Actually invoke the handler.
// 调用 HandlerAdapter ,最终调用 控制器方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
......
}
catch (Exception ex) {
......
}
catch (Throwable err) {
......
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
......
}
catch (Throwable err) {
......
}
finally {
......
}
}
具体看 getHandlerAdapter
方法:
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
// 注意,适配器是已经事先生成好了的,默认有三种适配器
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
// 如果支持,则直接返回
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
实际调试过程中,有三种类型的适配器,其中就有我们前面提到的 RequestMappingHandlerAdapter
,实际最终返回的,也是这个类型
附上 HandlerAdapter 的继承图:
RequestMappingHandlerAdapter
中缓存 modelAttributeAdviceCache
和 initBinderAdviceCache
何时是如何生效的呢?那我们就继续往后看在 DispatcherServlet
的 doDispatch
方法
在 DispatcherServlet
的 doDispatch
方法中 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
中,实际上是通过调用 HandlerMethodAdapter
的 handle
方法来调用控制器方法
AbstractHandlerMethodAdapter
类实现了 handle
方法,将其委托给子类实现。
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 委托为 handleInternal 方法,让子类重写
return handleInternal(request, response, (HandlerMethod) handler);
}
RequestMappingHandlerAdapter
又将其委托给 invokeHandlerMethod
方法
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
// 根据外部@ControllerAdvice中的@InitBinder方法(排在前)和控制器方法所在的控制器内部的@InitBinder方法(排在后)初始化 WebDataBinderFactory
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
// 根据外部@ControllerAdvice中的@ModelAttribute方法(排在前)和控制器方法所在的控制器内部的@ModelAttribute方法(排在后)初始化 ModelFactory
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
// 初始化 Model,依次调用前面在初始化 ModelFactory 时传入的 @ModelAttribute方法,
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
// 异步请求的相关处理
......
// 执行控制器方法,在执行控制器方法前,如果需要的话,会依次调用WebDataBinderFactory中的 @InitBinder方法
invocableMethod.invokeAndHandle(webRequest, mavContainer);
// 异步请求的相关处理
......
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}
先看 getDataBinderFactory
方法,可以看到是先添加 initBinderAdviceCache
缓存中的也就是外部外部 @ControllerAdvice
中的 @InitBinder
方法,再添加控制器方法所在控制器中(即本地)的 @InitBinder
方法。
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
Class<?> handlerType = handlerMethod.getBeanType();
// 控制器方法所在的控制器中的 @InitBinder方法的缓存(即本地@InitBinder方法)
Set<Method> methods = this.initBinderCache.get(handlerType);
if (methods == null) {
// 如果没有,那就查一下,进行缓存,查出来也没有没有也要存
methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
this.initBinderCache.put(handlerType, methods);
}
List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
// 先添加缓存中外部的 @InitBinder 方法
this.initBinderAdviceCache.forEach((controllerAdviceBean, methodSet) -> {
if (controllerAdviceBean.isApplicableToBeanType(handlerType)) {
Object bean = controllerAdviceBean.resolveBean();
for (Method method : methodSet) {
initBinderMethods.add(createInitBinderMethod(bean, method));
}
}
});
// 再添加本地的 @InitBinder 方法
for (Method method : methods) {
Object bean = handlerMethod.getBean();
initBinderMethods.add(createInitBinderMethod(bean, method));
}
return createDataBinderFactory(initBinderMethods);
}
其中,在填充 initBinderMethods
列表的时候,会调用 createInitBinderMethod
方法,而在 createInitBinderMethod
方法中,会使用我们前面初始化的 initBinderArgumentResolvers
作为 @InitBinder
修饰的方法的参数解析器:
private InvocableHandlerMethod createInitBinderMethod(Object bean, Method method) {
InvocableHandlerMethod binderMethod = new InvocableHandlerMethod(bean, method);
// 如果 RequestMappingHandlerAdapter 实例化之后没有调用 setInitBinderArgumentResolvers 初始化 initBinderArgumentResolvers,
// 则使用 getDefaultInitBinderArgumentResolvers 中初始化的 13个参数解析器
if (this.initBinderArgumentResolvers != null) {
// 设置方法的参数解析器
binderMethod.setHandlerMethodArgumentResolvers(this.initBinderArgumentResolvers);
}
// 设置方法的数据绑定器
binderMethod.setDataBinderFactory(new DefaultDataBinderFactory(this.webBindingInitializer));
// 设置方法的参数名发现器
binderMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
return binderMethod;
}
再看 getModelFactory
方法,基本上结构跟 getDataBinderFactory
方法是差不多的,从中可以看到是先添加 modelAttributeAdviceCache
缓存中的也就是外部外部 @ControllerAdvice
中的 @ModelAttribute
方法,再添加控制器方法所在控制器中(即本地)的 @ModelAttribute
方法。
private ModelFactory getModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) {
SessionAttributesHandler sessionAttrHandler = getSessionAttributesHandler(handlerMethod);
Class<?> handlerType = handlerMethod.getBeanType();
// 控制器方法所在的控制器中的 @ModelAttribute方法的缓存(即本地@ModelAttribute方法),注意,不包括同时被@RequestMapping注解修饰的方法
Set<Method> methods = this.modelAttributeCache.get(handlerType);
if (methods == null) {
// 如果没有,那就查出来,进行缓存,没有就算了
methods = MethodIntrospector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS);
this.modelAttributeCache.put(handlerType, methods);
}
List<InvocableHandlerMethod> attrMethods = new ArrayList<>();
// 先添加外部的 @ModelAttribute方法
this.modelAttributeAdviceCache.forEach((controllerAdviceBean, methodSet) -> {
if (controllerAdviceBean.isApplicableToBeanType(handlerType)) {
Object bean = controllerAdviceBean.resolveBean();
for (Method method : methodSet) {
attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
}
}
});
// 再添加本地的 @ModelAttribute方法
for (Method method : methods) {
Object bean = handlerMethod.getBean();
attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
}
// 构建 Model工厂
return new ModelFactory(attrMethods, binderFactory, sessionAttrHandler);
}
再看看何时调用的 @ModelAttribute
方法,modelFactory.initModel
中的 invokeModelAttributeMethods
方法依次调用初始化时传入的 @ModelAttribute
修饰的方法。invokeModelAttributeMethods
方法内部就不深入了解了。
public void initModel(NativeWebRequest request, ModelAndViewContainer container, HandlerMethod handlerMethod) throws Exception {
// 处理@sessionAttributes注解
Map<String, ?> sessionAttributes = this.sessionAttributesHandler.retrieveAttributes(request);
container.mergeAttributes(sessionAttributes);
// 依次调用所有@ModelAttribute方法
invokeModelAttributeMethods(request, container);
// 处理@sessionAttributes注解
for (String name : findSessionAttributeArguments(handlerMethod)) {
if (!container.containsAttribute(name)) {
Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
if (value == null) {
throw new HttpSessionRequiredException("Expected session attribute '" + name + "'", name);
}
container.addAttribute(name, value);
}
}
}
看看何时调用的 @InitBinder
方法,在 invocableMethod.invokeAndHandle(webRequest, mavContainer);
方法中调用控制器方法前调用,具体的定位有点复杂,这里先不仔细研究了,将来学习了 SpringMVC中的类型转换
和 SpringMVC中的数据绑定
再继续深入。
这里只是简单涉及了一点 HandlerAdapter 的源码
关于控制器方法适配器的相关源码分析,请看《SpringMVC 控制器方法(handler)适配器 - HandlerAdapter》和《SpringMVC-RequestMappingHandlerAdapter 源码解析.md》
简单实践。
被通知的控制器 AdvisedController
@RestController
@RequestMapping("/advise")
public class AdvisedController {
@ModelAttribute
public void setModelAttribute(Model model) {
System.out.println("Controller local @ModelAttribute method");
String path = model.getAttribute("AttributeSetPath") != null ? (String) model.getAttribute("AttributeSetPath") : "";
model.addAttribute("AttributeSetPath", path + "end");
}
@InitBinder
public void initBinder(WebDataBinder binder) {
System.out.println("Controller local @initBinder method");
System.out.println("binder AllowedFields:"+Arrays.toString(binder.getAllowedFields()));
System.out.println("binder DisallowedFields:"+Arrays.toString(binder.getDisallowedFields()));
}
@RequestMapping("/target")
public String target(Model model) {
System.out.println(model);
return "success";
}
@RequestMapping("/testInitBinder")
public String testInitBinder(Date date, Model model) {
System.out.println(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(date));
return "success";
}
}
第一个控制器通知@ControllerAdvice,限制目标控制器所在的包为 HelloController
所在的包,也就是 AdvisedController
所在的包
同时,在其中添加了 @ModelAttribute
方法和 @InitBinder
方法,在执行这些方法时,首先输出此方法所在 ControllerAdvice 的 Order 信息,然后输出自身的方法信息,方便控制台分析。
后续的两个 ControllerAdvice 方法也大同小异
@ControllerAdvice(basePackageClasses = HelloController.class)
@Order(0)
public class ControllerAdvice0 {
private int showOrder(){
// 获取特定类型上的特定注解对象,这样我们就可以直接获取其配置,这应该是直接从Class对象中获取的
Order order = AnnotatedElementUtils.findMergedAnnotation(this.getClass(), Order.class);
int value = order.value();
System.out.println("执行排序为: "+value+" 的ControllerAdvice");
return value;
}
@ModelAttribute
public void setModelAttribute(Model model) {
int value = showOrder();
System.out.println("执行 ModelAttribute 方法");
String path = model.getAttribute("AttributeSetPath")!=null ? (String) model.getAttribute("AttributeSetPath") : "";
model.addAttribute("AttributeSetPath", path+value+"-");
}
@InitBinder
public void initBinder(WebDataBinder binder){
showOrder();
System.out.println("执行 initBinder 方法");
binder.setAllowedFields("aaa");
}
}
第二个 ControllerAdvice 方法
@ControllerAdvice(basePackageClasses = HelloController.class)
@Order(1)
public class ControllerAdvice1 {
private int showOrder(){
// 获取特定类型上的特定注解对象,这样我们就可以直接获取其配置,这应该是直接从Class对象中获取的
Order order = AnnotatedElementUtils.findMergedAnnotation(this.getClass(), Order.class);
int value = order.value();
System.out.println("执行排序为: "+value+" 的ControllerAdvice");
return value;
}
@ModelAttribute
public void setModelAttribute(Model model) {
int value = showOrder();
System.out.println("执行 ModelAttribute 方法");
String path = model.getAttribute("AttributeSetPath") != null ? (String) model.getAttribute("AttributeSetPath") : "";
model.addAttribute("AttributeSetPath", path + value + "-");
}
@InitBinder
public void initBinder(WebDataBinder binder){
int value = showOrder();
System.out.println("执行 initBinder 方法");
binder.setDisallowedFields("bbb");
}
}
第三个 ControllerAdvice 方法
@ControllerAdvice(basePackageClasses = HelloController.class)
@Order(2)
public class ControllerAdvice2 {
private int showOrder(){
// 获取特定类型上的特定注解对象,这样我们就可以直接获取其配置,这应该是直接从Class对象中获取的
Order order = AnnotatedElementUtils.findMergedAnnotation(this.getClass(), Order.class);
int value = order.value();
System.out.println("执行排序为: "+value+" 的ControllerAdvice");
return value;
}
@ModelAttribute
public void setModelAttribute(Model model) {
int value = showOrder();
System.out.println("执行 ModelAttribute 方法");
String path = model.getAttribute("AttributeSetPath") != null ? (String) model.getAttribute("AttributeSetPath") : "";
model.addAttribute("AttributeSetPath", path + value + "-");
}
@InitBinder
public void initBinder(WebDataBinder binder){
int value = showOrder();
System.out.println("执行 initBinder 方法");
binder.addCustomFormatter(new DateFormatter("yyyy年MM月dd日"));
}
}
此外我们还尝试自定义个一个 RequestResponseBodyAdvice
,实现 RequestBodyAdvice
和 ResponseBodyAdvice<User>
,不过这里我们不做具体操作
@ControllerAdvice(basePackageClasses= HelloController.class)
@Order(3)
public class MyRequestResponseBodyAdvice implements RequestBodyAdvice, ResponseBodyAdvice<User> {
private int showOrder(){
// 获取特定类型上的特定注解对象,这样我们就可以直接获取其配置,这应该是直接从Class对象中获取的
Order order = AnnotatedElementUtils.findMergedAnnotation(this.getClass(), Order.class);
int value = order.value();
System.out.println("执行排序为: "+value+" 的ControllerAdvice");
return value;
}
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return false;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
return null;
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return null;
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return null;
}
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return false;
}
@Override
public User beforeBodyWrite(User body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return null;
}
}
测试方法
@SpringJUnitWebConfig(locations = "classpath:SpringMVC.xml")
class AdvisedControllerTest {
MockMvc mockMvc;
@BeforeEach
void setup(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
void target() throws Exception {
mockMvc.perform(get("/advise/target")).andDo(print());
}
@Test
void testInitBinder() throws Exception {
mockMvc.perform(get("/advise/testInitBinder").queryParam("date","2022年9月15日")).andDo(print());
}
}
运行 testInitBinder
方法,查看日志
手动输出的日志:
执行排序为: 0 的ControllerAdvice
执行 ModelAttribute 方法
执行排序为: 1 的ControllerAdvice
执行 ModelAttribute 方法
执行排序为: 2 的ControllerAdvice
执行 ModelAttribute 方法
Controller local @ModelAttribute method
执行排序为: 0 的ControllerAdvice
执行 initBinder 方法
执行排序为: 1 的ControllerAdvice
执行 initBinder 方法
执行排序为: 2 的ControllerAdvice
执行 initBinder 方法
Controller local @initBinder method
binder AllowedFields:[aaa]
binder DisallowedFields:[bbb]
2022-09-15T00:00:00
Mock 请求和响应信息
MockHttpServletRequest:
HTTP Method = GET
Request URI = /advise/testInitBinder
Parameters = {date=[2022年9月15日]}
Headers = []
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = xyz.xiashuo.springmvccontrolleradvice.controller.AdvisedController
Method = xyz.xiashuo.springmvccontrolleradvice.controller.AdvisedController#testInitBinder(Date, Model)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json", Content-Length:"7"]
Content type = application/json
Body = success
Forwarded URL = null
Redirected URL = null
Cookies = []
从手动输出的日志我们可以看到,
因为 @ModelAttribute
方法会在任意控制器方法调用前执行,所以先按照 Order 执行 @ControllerAdvice
中的 @ModelAttribute
方法,然后再执行目标控制器中的 @ModelAttribute
方法,同时,这些 @ModelAttribute
方法使用的都是同一个 Model 域,即使用的都是同一个 Model 实例,
然后,因为我们的控制器方法需要解析请求参数,所以会调用 @InitBinder
方法,如果没有参数,是不会调用 @InitBinder
方法的,比如 AdvisedController
中的 target
方法。首先开始按照 Order 执行 @ControllerAdvice
中的 @InitBinder
方法,然后再执行目标控制器中的 @InitBinder
方法,同时,这些 @InitBinder
方法使用的都是同一个 WebDataBinder
实例。
最后执行目标控制器方法,可以看到正确地转化了时间字符串。
其实,这个处理流程也可以看作是不使用 @ControllerAdvice
类的一个普通请求的处理流程,控制器内部的 @InitBinder
方法和 @ModelAttribute
方法就是在 RequestMappingHandlerAdapter
中处理的。
PS:
MyRequestResponseBodyAdvice
单独实现了 RequestBodyAdvice
和 ResponseBodyAdvice<User>
,在 RequestMappingHandlerAdapter
的 initControllerAdviceCache
中正常解析

@ExceptionHandler 方法
其实对@ExceptionHandler 方法的缓存的调用,在 《SpringMVC-第七篇:控制器方法异常处理》
的 ExceptionHandlerExceptionResolver
小节中都已经详细描述了。这里就不再重复了。
最终的结论是:
先在当前控制器中的 @ExceptionHandler
方法这个范围内查找,如果没找到,就按照按照 @ControllerAdvice
的排序依次在 @ControllerAdvice
中包含的 @ExceptionHandler
方法中查找。
拿如何在一个类中查找处理当前异常呢?
先根据当前抛出的异常的类型进行匹配,按照继承路径最短原则,遍历所有的异常处理方法的 @ExceptionHandler
注解中配置的异常类型,查找异常类型就是当前爆出的异常类型或者为当前爆出的异常类型的父类,同时继承层级最短的一个,如果找不到,就查找当前抛出的异常的根异常,然后继续以这个根异常来进行类型匹配,直到找到合适的异常,如果还是没找到,那就没有了,换下一个类。
所以,只要在当前类中能找到一个能处理当前爆出的异常的 @ExceptionHandler
方法,都不会继续从下一个类中查找,也就是说,只有当前类中完全不包含能处理当前爆出的异常的 @ExceptionHandler
方法,才会去下一个类中查找。
我们最开始从爆出异常的控制器方法所在的控制器中查找,然后按照 Order,从匹配的
@ControllerAdvice
中查找。
简单实践
自定义异常 MyException
public class MyException extends Exception {
private static final long serialVersionUID = -2957544073971720547L;
public MyException(String message,Exception cause) {
super(message, cause);
}
}
在 AdvisedController
中添加抛出异常的请求和异常处理类
// 匹配当前抛出的异常类型的cause异常
@ExceptionHandler(ArithmeticException.class)
public String exceptionHandler(ArithmeticException exception) {
String message = exception.toString();
System.out.println("ArithmeticException exceptionHandler:" + message);
return message;
}
// 匹配当前抛出的异常类型的父类
@ExceptionHandler(Exception.class)
public String exceptionHandler(Exception exception) {
String message = exception.toString();
System.out.println("Exception exceptionHandler:" + message);
return message;
}
// 直接匹配当前抛出的异常
@ExceptionHandler(MyException.class)
public String exceptionHandler(MyException exception) {
String message = exception.toString();
System.out.println("MyException exceptionHandler:" + message);
return message;
}
@RequestMapping("/testException")
public String testException(Model model) throws Exception {
Exception nullPointerException = new ArithmeticException("customize NullPointer exception");
Exception myException = new MyException("customize my exception", nullPointerException);
throw myException;
//return "success";
}
测试方法
@Test
void testException() throws Exception {
mockMvc.perform(get("/advise/testException")).andDo(print());
}
直接运行测试,输出
MyException exceptionHandler:xyz.xiashuo.springmvccontrolleradvice.exceptions.MyException: customize my exception
说明此时触发的是 @ExceptionHandler(MyException.class)
修饰的方法
注释掉 @ExceptionHandler(MyException.class)
修饰的方法,再次运行测试,输出
Exception exceptionHandler:xyz.xiashuo.springmvccontrolleradvice.exceptions.MyException: customize my exception
说明此时触发的是 @ExceptionHandler(Exception.class)
修饰的方法
再次注释掉 @ExceptionHandler(Exception.class)
修饰的方法,再次运行测试,输出
ArithmeticException exceptionHandler:java.lang.ArithmeticException: customize NullPointer exception
说明此时触发的是 @ExceptionHandler(ArithmeticException.class)
修饰的方法。
这也验证了我们之前总结的 @ExceptionHandler
方法查找的顺序
-
与当前爆出的异常类型完全匹配的完全匹配
@ExceptionHandler
方法 -
与当前爆出的异常类型的父类匹配的
@ExceptionHandler
方法
如果找不到,开始把当前爆出的异常类型的 cause 异常当作当前爆出的异常,重复上面的步骤,还是没有呢?就从下一个类来查找,比如从 @ControllerAdvice
中查找,我们来试试
我们在 ControllerAdvice0
中添加异常处理方法,
// 直接匹配当前抛出的异常
@ExceptionHandler(MyException.class)
@ResponseBody
public String exceptionHandler(MyException exception) {
String message = exception.toString();
System.out.println("ControllerAdvice0 MyException exceptionHandler:" + message);
return message;
}
此时,@ExceptionHandler(MyException.class)
修饰的方法和 @ExceptionHandler(Exception.class)
修饰的方法都被注释,@ExceptionHandler(ArithmeticException.class)
修饰的方法没有被注释,运行测试方法,输出的依然是
ArithmeticException exceptionHandler:java.lang.ArithmeticException: customize NullPointer exception
只有当我们连 AdvisedController
中的 @ExceptionHandler(ArithmeticException.class)
修饰的方法也注释掉,AdvisedController
没有任何 @ExceptionHandler
方法能够处理当前爆出的异常的时候才会生效
ControllerAdvice0 MyException exceptionHandler:xyz.xiashuo.springmvccontrolleradvice.exceptions.MyException: customize my exception
所以,只要在当前类中能找到一个能处理当前爆出的异常的 @ExceptionHandler
方法,都不会继续从下一个类中查找,也就是说,只有当前类中完全不包含能处理当前爆出的异常的 @ExceptionHandler
方法,才会去下一个类中查找。
我们最开始从爆出异常的控制器方法所在的控制器中查找,然后按照 Order,从匹配的
@ControllerAdvice
中查找。