Java SpringMVCSpring MVC中,通过组合使用注解@ControllerAdvice和其他一些注解,可以为开发人员实现的控制器类做一些全局性的定制,具体来讲,可作如下定制 :
- 结合
@ExceptionHandler使用 ==> 添加统一的异常处理控制器方法 - 结合
@ModelAttribute使用 ==> 使用共用方法添加渲染视图的数据模型属性 - 结合
@InitBinder使用 ==> 使用共用方法初始化控制器方法调用使用的数据绑定器
数据绑定器涉及到哪些参数/属性需要/不需要绑定,设置数据类型转换时使用的PropertyEditor,Formatter等。
@ControllerAdvice和@InitBinder配合使用打印请求的数据绑定的实体类
import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.WebDataBinder;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.InitBinder;import java.util.Optional;/*** LogControllerAdvice* <p>* encoding:UTF-8** @author Fcant 下午 12:29 2021/2/23/0023*/@Slf4j@ControllerAdvicepublic class ControllerLogAdvice {/*** 应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器* @param binder*/@InitBinderpublic void initBinder(WebDataBinder binder) {if (Optional.ofNullable(binder.getTarget()).isPresent()) {log.info("POST请求映射的对象名为:" + binder.getObjectName());log.info("请求的Body内容为:" + binder.getTarget().toString());}}}
1、@ControllerAdvice注解起作用的时机
首先,容器启动时,会定义类型为RequestMappingHandlerAdapter的bean组件,这是DispatcherServlet用于执行控制器方法的HandlerAdapter,它实现了接口InitializingBean,所以自身在初始化时其方法afterPropertiesSet会被调用执行。
@Overridepublic void afterPropertiesSet() {// Do this first, it may add ResponseBody advice beansinitControllerAdviceCache();// 省略掉无关代码// ...}
从以上代码可以看出,RequestMappingHandlerAdapter bean组件在自身初始化时调用了initControllerAdviceCache,从这个方法的名字上就可以看出,这是一个ControllerAdvice相关的初始化函数,而initControllerAdviceCache具体又做了什么呢?继续来看 :
private void initControllerAdviceCache() {if (getApplicationContext() == null) {return;}// 找到所有使用了注解 @ControllerAdvice 的bean组件List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());// 排序AnnotationAwareOrderComparator.sort(adviceBeans);// this. requestResponseBodyAdvice :// 用于记录所有 @ControllerAdvice + RequestBodyAdvice/ResponseBodyAdvice bean// this.modelAttributeAdviceCache :// 用于记录所有 @ControllerAdvice bean组件中的 @ModuleAttribute 方法// this.initBinderAdviceCache :// 用于记录所有 @ControllerAdvice bean组件中的 @InitBinder 方法// 用于临时记录所有 @ControllerAdvice + RequestResponseBodyAdvice beanList<Object> requestResponseBodyAdviceBeans = new ArrayList<>();// 遍历每个使用了注解 @ControllerAdvice 的 bean 组件for (ControllerAdviceBean adviceBean : adviceBeans) {Class<?> beanType = adviceBean.getBeanType();if (beanType == null) {throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);}// 获取当前 ControllerAdviceBean 中所有使用了 @ModelAttribute 注解的方法Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);if (!attrMethods.isEmpty()) {this.modelAttributeAdviceCache.put(adviceBean, attrMethods);}// 获取当前 ControllerAdviceBean 中所有使用了 @InitMethod 注解的方法Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);if (!binderMethods.isEmpty()) {this.initBinderAdviceCache.put(adviceBean, binderMethods);}// 如果当前 ControllerAdviceBean 继承自 RequestBodyAdvice,将其登记到 requestResponseBodyAdviceBeansif (RequestBodyAdvice.class.isAssignableFrom(beanType)) {requestResponseBodyAdviceBeans.add(adviceBean);}// 如果当前 ControllerAdviceBean 继承自 ResponseBodyAdvice,将其登记到 requestResponseBodyAdviceBeansif (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {requestResponseBodyAdviceBeans.add(adviceBean);}}if (!requestResponseBodyAdviceBeans.isEmpty()) {this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);}if (logger.isDebugEnabled()) {int modelSize = this.modelAttributeAdviceCache.size();int binderSize = this.initBinderAdviceCache.size();int reqCount = getBodyAdviceCount(RequestBodyAdvice.class);int resCount = getBodyAdviceCount(ResponseBodyAdvice.class);if (modelSize == 0 && binderSize == 0 && reqCount == 0 && resCount == 0) {logger.debug("ControllerAdvice beans: none");}else {logger.debug("ControllerAdvice beans: " + modelSize + " @ModelAttribute, " + binderSize +" @InitBinder, " + reqCount + " RequestBodyAdvice, " + resCount + " ResponseBodyAdvice");}}}
从以上initControllerAdviceCache方法的实现逻辑来看,它将容器中所有使用了注解@ControllerAdvice的bean或者其方法都分门别类做了统计,记录到了RequestMappingHandlerAdapter实例的三个属性中 :
requestResponseBodyAdvice- 用于记录所有
@ControllerAdvice+RequestBodyAdvice/ResponseBodyAdvicebean组件 modelAttributeAdviceCache- 用于记录所有
@ControllerAdvicebean组件中的@ModuleAttribute方法 initBinderAdviceCache- 用于记录所有
@ControllerAdvicebean组件中的@InitBinder方法
到此为止,可以知道,使用注解@ControllerAdvice的bean中的信息被提取出来了,但是,这些信息又是怎么使用的呢 ?继续来看。
2、@ControllerAdvice 定义信息的使用
1. requestResponseBodyAdvice的使用
/*** Return the list of argument resolvers to use including built-in resolvers* and custom resolvers provided via {@link #setCustomArgumentResolvers}.*/private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();// ... 省略无关代码resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),this.requestResponseBodyAdvice));// ... 省略无关代码resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(),this.requestResponseBodyAdvice));// ... 省略无关代码resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(),this.requestResponseBodyAdvice));// ... 省略无关代码return resolvers;}
getDefaultArgumentResolvers方法用于准备RequestMappingHandlerAdapter执行控制器方法过程中缺省使用的HandlerMethodArgumentResolver,从上面代码可见,requestResponseBodyAdvice会被传递给RequestResponseBodyMethodProcessor/RequestPartMethodArgumentResolver/HttpEntityMethodProcessor这三个参数解析器,不难猜测,它们在工作时会使用到该requestResponseBodyAdvice,但具体怎么使用,为避免过深细节影响理解,不继续展开。
方法getDefaultArgumentResolvers也在RequestMappingHandlerAdapter初始化方法中被调用执行,如下所示 :
@Overridepublic void afterPropertiesSet() {// Do this first, it may add ResponseBody advice beansinitControllerAdviceCache();if (this.argumentResolvers == null) {List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers(); // <==this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);}// 省略无关代码}
2. modelAttributeAdviceCache的使用
private ModelFactory getModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) {SessionAttributesHandler sessionAttrHandler = getSessionAttributesHandler(handlerMethod);Class<?> handlerType = handlerMethod.getBeanType();Set<Method> methods = this.modelAttributeCache.get(handlerType);if (methods == null) {// 获取当前控制器类中使用了 @ModelAttribute 的方法methods = MethodIntrospector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS);this.modelAttributeCache.put(handlerType, methods);}List<InvocableHandlerMethod> attrMethods = new ArrayList<>();// Global methods first// 遍历@ControllerAdvice bean中所有使用了 @ModelAttribute 的方法,// 将其包装成 InvocableHandlerMethod 放到 attrMethods// ********* 这里就是 modelAttributeAdviceCache 被使用到的地方了 ************this.modelAttributeAdviceCache.forEach((clazz, methodSet) -> {if (clazz.isApplicableToBeanType(handlerType)) {Object bean = clazz.resolveBean();for (Method method : methodSet) {attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));}}});// 遍历当前控制器类中中所有使用了 @ModelAttribute 的方法,// 也将其包装成 InvocableHandlerMethod 放到 attrMethodsfor (Method method : methods) {Object bean = handlerMethod.getBean();attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));}// 此时 attrMethods 包含了两类 InvocableHandlerMethod, 分别来自于 :// 1. @ControllerAdvice bean 中所有使用了 @ModelAttribute 的方法// 2. 当前控制器类中中所有使用了 @ModelAttribute 的方法return new ModelFactory(attrMethods, binderFactory, sessionAttrHandler);}/ 从指定 bean 的方法 method ,其实是一个使用了注解 @ModelAttribute 的方法,/ 构造一个 InvocableHandlerMethod 对象private InvocableHandlerMethod createModelAttributeMethod(WebDataBinderFactory factory,Object bean, Method method) {InvocableHandlerMethod attrMethod = new InvocableHandlerMethod(bean, method);if (this.argumentResolvers != null) {attrMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);}attrMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);attrMethod.setDataBinderFactory(factory);return attrMethod;}
从此方法可以看到,getModelFactory方法使用到了modelAttributeAdviceCache,它会根据其中每个元素构造成一个InvocableHandlerMethod,最终传递给要创建的ModelFactory对象。而getModelFactory又在什么时候被使用呢 ? 它会在RequestMappingHandlerAdapter执行一个控制器方法的准备过程中被调用,如下所示 :
@Nullableprotected ModelAndView invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {ServletWebRequest webRequest = new ServletWebRequest(request, response);try {// 构造调用 handlerMethod 所要使用的数据绑定器工厂WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);// 构造调用 handlerMethod 所要使用的数据模型工厂ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);// 省略无关代码 ...}}
3. initBinderAdviceCache的使用
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {Class<?> handlerType = handlerMethod.getBeanType();Set<Method> methods = this.initBinderCache.get(handlerType);if (methods == null) {// 获取当前控制器类中使用了 @InitBinder 的方法methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);this.initBinderCache.put(handlerType, methods);}List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();// Global methods first// 遍历@ControllerAdvice bean中所有使用了 @InitBinder 的方法,// 将其包装成 InvocableHandlerMethod 放到 initBinderMethods// ********* 这里就是 initBinderAdviceCache 被使用到的地方了 ************this.initBinderAdviceCache.forEach((clazz, methodSet) -> {if (clazz.isApplicableToBeanType(handlerType)) {Object bean = clazz.resolveBean();for (Method method : methodSet) {initBinderMethods.add(createInitBinderMethod(bean, method));}}});// 遍历当前控制器类中所有使用了 @InitBinder 的方法,// 将其包装成 InvocableHandlerMethod 放到 initBinderMethodsfor (Method method : methods) {Object bean = handlerMethod.getBean();initBinderMethods.add(createInitBinderMethod(bean, method));}// 此时 initBinderMethods 包含了两类 InvocableHandlerMethod, 分别来自于 :// 1. @ControllerAdvice bean 中所有使用了 @InitBinder 的方法// 2. 当前控制器类中中所有使用了 @InitBinder 的方法return createDataBinderFactory(initBinderMethods);}// 从指定 bean 的方法 method ,其实是一个使用了注解 @InitBinder 的方法,// 构造一个 InvocableHandlerMethod 对象private InvocableHandlerMethod createInitBinderMethod(Object bean, Method method) {InvocableHandlerMethod binderMethod = new InvocableHandlerMethod(bean, method);if (this.initBinderArgumentResolvers != null) {binderMethod.setHandlerMethodArgumentResolvers(this.initBinderArgumentResolvers);}binderMethod.setDataBinderFactory(new DefaultDataBinderFactory(this.webBindingInitializer));binderMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);return binderMethod;}/*** Template method to create a new InitBinderDataBinderFactory instance.* <p>The default implementation creates a ServletRequestDataBinderFactory.* This can be overridden for custom ServletRequestDataBinder subclasses.* @param binderMethods {@code @InitBinder} methods* @return the InitBinderDataBinderFactory instance to use* @throws Exception in case of invalid state or arguments*/protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)throws Exception {return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());}
从此方法可以看到,getDataBinderFactory方法使用到了initBinderAdviceCache,它会根据其中每个元素构造成一个InvocableHandlerMethod,最终传递给要创建的InitBinderDataBinderFactory对象。而getDataBinderFactory又在什么时候被使用呢 ? 它会在RequestMappingHandlerAdapter执行一个控制器方法的准备过程中被调用,如下所示 :
@Nullableprotected ModelAndView invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {ServletWebRequest webRequest = new ServletWebRequest(request, response);try {// 构造调用 handlerMethod 所要使用的数据绑定器工厂WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);// 构造调用 handlerMethod 所要使用的数据模型工厂ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);// 省略无关代码 ...}}
到此为止,基本上可以看到,通过@ControllerAdvice注解的bean组件所定义的@ModelAttribute/@InitBinder方法,或者RequestBodyAdvice/ResponseBodyAdvice,是如何被RequestMappingHandlerAdapter提取和使用的了。虽然并未深入到更细微的组件研究它们最终的使用,不过结合这些组件命名以及这些更深一层的使用者组件的名称,即便是猜测,也不难理解猜到它们如何被使用了。
关于@ControllerAdvice和@ExceptionHandler这一组合,在上面提到的RequestMappingHandlerAdapter逻辑中,并未涉及到。那如果使用了这种组合,又会是怎样一种工作机制呢 ?事实上,@ControllerAdvice和@ExceptionHandler这一组合所做的定义,会被ExceptionHandlerExceptionResolver消费使用。
