ControllerAdvice分析说明
@ControllerAdvice见的最多的就是同于对异常的处理。比如下面的这个样子
@ControllerAdvice
public class TestControllerAdvised {
@ExceptionHandler(value = Exception.class)
public String modelAndViewException(){
// 做一些异常的处理逻辑
}
}
在阅读SpringMvc源码的时候,发现除了异常处理之外,还有别的功能。就有了这篇文章
一切的开始还是从源码的注释开始
ControllerAdvice源码
他是一个特殊的@Component。可以在它标注的Bean里面探测到@ExceptionHandle,@InitBinder,@ModelAttribute标注的方法,可以在多个@Controller标注得类上面做操作。除此之外,还可以通过它来配置RequestBodyAdvice
和ResponseBodyAdvice
。
先说说这三个注解的作用
-
@InitBinder,它可以自定义DataBinder,DataBinder是SpringMvc用来给目标的Bean设置属性值的,包括支持验证,和参数绑定结果扽西,并且还可以自定义可以字段,那些字段是可以绑定的,那些字段是必须的。等等。此外,它标注的方法的返回值必须为null。
- 获取@InitBinder注解标注方法
RequestMappingHandlerAdapter#getDataBinderFactory
,@InitBinder可以在@Controller和@ControllerAdvice里面都可以用。 - 调用@InitBinder注解方法来自定义DataBinder的操作。
InitBinderDataBinderFactory#initBinder(WebDataBinder,NativeWebRequest)
。 - 校验返回值必须为void的操作。
InitBinderDataBinderFactory#initBinder(WebDataBinder,NativeWebRequest)
。
- 获取@InitBinder注解标注方法
-
@ModelAttribute,它可以在调用对应的Controller处理之前,先前一步操作Model,MVC,是Model,View,COntroller,Model是SpringMvc里面很重要的概念,对应在真实的代码逻辑上就是一个大Map(
ModelAndViewContainer#ModelMap属性
)。模型渲染是需要Model的。所以可以在先前一步处理Model。会将它所标注的方法返回值放在Model中,Key就是注解中value()
的值。它还可以添加到@ModelAttribute标注的方法中。用来表示该方法依赖那个model的值。如果在model中没有找到,每次循环默认取第一个。对应的代码如下:
ModelFactory#invokeModelAttributeMethods
// 这个方法是在循环中调用的, 注意下面的remove操作,如果找到依赖了,就会移除找到的这个,否则就会获取第一个,然后将它移除。 private ModelMethod getNextModelMethod(ModelAndViewContainer container) { for (ModelMethod modelMethod : this.modelMethods) { if (modelMethod.checkDependencies(container)) { this.modelMethods.remove(modelMethod); return modelMethod; } } ModelMethod modelMethod = this.modelMethods.get(0); this.modelMethods.remove(modelMethod); return modelMethod; } public boolean checkDependencies(ModelAndViewContainer mavContainer) { for (String name : this.dependencies) { if (!mavContainer.containsAttribute(name)) { return false; } } return true; }
还可以添加到RequestMapping标注的方法的参数里面。@ModelAttribute value字段表示模型的名字,如果有,会直接从model中找出来赋值,如果没有就会尝试从Request中找,赋值,将对应的值放在model中,key 如果指定了value就是value属性的值,如果没有,就默认是参数类型小写。对应的代码如下:
ModelAttributeMethodProcessor#resolveArgument
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer"); Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory"); // 获取参数名字,取值为@ModelAttribute中value的值,如果没有就是类型名字小写 String name = ModelFactory.getNameForParameter(parameter); // 获取注解的属性值 ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class); if (ann != null) { mavContainer.setBinding(name, ann.binding()); } Object attribute = null; BindingResult bindingResult = null; // 看是否有对应的key。下面的都是获取对应的值了 if (mavContainer.containsAttribute(name)) { attribute = mavContainer.getModel().get(name); } else { // Create attribute instance try { attribute = createAttribute(name, parameter, binderFactory, webRequest); } catch (BindException ex) { if (isBindExceptionRequired(parameter)) { // No BindingResult parameter -> fail with BindException throw ex; } // Otherwise, expose null/empty value and associated BindingResult if (parameter.getParameterType() == Optional.class) { attribute = Optional.empty(); } else { attribute = ex.getTarget(); } bindingResult = ex.getBindingResult(); } } if (bindingResult == null) { // Bean property binding and validation; // skipped in case of binding failure on construction. WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); if (binder.getTarget() != null) { if (!mavContainer.isBindingDisabled(name)) { bindRequestParameters(binder, webRequest); } validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new BindException(binder.getBindingResult()); } } // Value type adaptation, also covering java.util.Optional if (!parameter.getParameterType().isInstance(attribute)) { attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter); } bindingResult = binder.getBindingResult(); } // Add resolved attribute and BindingResult at the end of the model // 将处理过的添加到Model中。 Map<String, Object> bindingResultModel = bindingResult.getModel(); mavContainer.removeAttributes(bindingResultModel); mavContainer.addAllAttributes(bindingResultModel); return attribute; }
还可以在返回值的类型上面添加注解,会将该值添加到Model中去。对应的代码在
ModelAttributeMethodProcessor#handleReturnValue
中,同样的,如果value属性指定,key的名字就是它,否则就是类名小写。public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { if (returnValue != null) { String name = ModelFactory.getNameForReturnValue(returnValue, returnType); mavContainer.addAttribute(name, returnValue); } }
- 获取@ModelAttribute注解的方法
RequestMappingHandlerAdapter#getModelFactory(HandlerMethod WebDataBinderFactory)
,和上面是一样的, 先获取当前Controller里面的,在获取全局的(@ControllerAdvice里面的)
- 获取@ModelAttribute注解的方法
-
@ExceptionHandle
用来处理异常的,在指定的class中的方法,或者当前Controller中的方法中。他有两种使用方式
- 写在@ControllerAdvice类中。
- 写在当前处理请求的Controller中。
具体的一些信息建议直接看它的注释。清晰方便。
- 参数
- 返回值
在出现异常之后,会通过HandlerExceptionResolver
来处理异常,发现@ExceptionHandler对应的代码在 ExceptionHandlerExceptionResolver#getExceptionHandlerMethod
,还是先从当前的Controller里面找,再从@ControllerAdvice标注的类里面找。
看到这里有个问题,@ControllerAdvice
标注的类是在哪里加载进来的,是怎么发现的?
这种发现模式是通用的,获取Bean工厂里面所有的Bean。遍历这些Bean,找对应标注了这个注解的类。模板如下:
public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) { List<ControllerAdviceBean> adviceBeans = new ArrayList<>(); // 找到所有的Bean, for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class)) { if (!ScopedProxyUtils.isScopedTarget(name)) { // 通过这种方法来发现 ControllerAdvice controllerAdvice = context.findAnnotationOnBean(name, ControllerAdvice.class); if (controllerAdvice != null) { adviceBeans.add(new ControllerAdviceBean(name, context, controllerAdvice)); } } } OrderComparator.sort(adviceBeans); return adviceBeans; }
一般来说,在Spring中,这种自动发现Bean的这种功能都是写在 InitializingBean#afterPropertiesSet
的。
RequestBodyAdvice,ResponseBodyAdvice作用
看这个名字,就是到是一个通知类,在请求处理前,和请求返回前的操作。
-
ResponseBodyAdvice
用在标注了
@ResponseBody
和返回值为ResponseEntity
的方法上的。在写响应值之前的操作。public interface ResponseBodyAdvice<T> { boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType); rn the body that was passed in or a modified (possibly new) instance @Nullable T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response); }
-
RequestBodyAdvice
用在标注了
@RequestBody
和参数值为HttpEntity
的方法上的。public interface RequestBodyAdvice { //是否支持 boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType); HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException; Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType); @Nullable // 请求没有请求体调用它。 Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType); }
他俩是怎么注册到Springmvc中的呢?
两种方式。
- 直接添加在RequestMappingHandlerAdapter中
- 标注@ControllerAdvice注解会自动发现。
自动发现是怎么做的呢?
因为它是直接添加到RequestMappingHandlerAdapter中,那么在构建RequestMappingHandlerAdapter的时候,就可以从Spring中获取所有标注了@ControllerAdvice注解的类,遍历获取,用
isAssignableFrom
判断就好,对应的代码在RequestMappingHandlerAdapter#initControllerAdviceCache
。
举例
-
@InitBinder注解
像DataBinder中增加
Validator
-
ResponseBodyAdvice
在原来的返回值中增加外壳,用Result来包装一下
-
Controller
@GetMapping("/test") @ResponseBody public Message listResponseBody(@ModelAttribute(name = "age") int age) { Message message = new Message(); message.setId(33L); return message; }
-
ResponseBodyAdvices实现类
@ControllerAdvice public class TestRequestBodyAdvice implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { ServletServerHttpResponse response1 = (ServletServerHttpResponse) response; int status = response1.getServletResponse().getStatus(); return status==HttpStatus.OK.value()? // 如果是200,就用ok,否则就是500 new Result(HttpStatus.OK.value(),body,HttpStatus.OK.getReasonPhrase()): new Result(HttpStatus.INTERNAL_SERVER_ERROR.value(),body,HttpStatus.OK.getReasonPhrase()); } }
-
结果
{ "code": 200, "data": { "id": 33, "text": null, "summary": null, "created": "2022-02-19T08:46:33.913+00:00" }, "msg": "OK" }
可以看到,没有直接在处理的方法中嵌套,而是直接返回对象,通过Advice来统一处理。
-
写在最后(有点重要)
@ControllerAdvice也是有属性的。通过这些属性可以指定包,或者注解,或者类型表示这些@ControllerAdvice可以应用于哪些Controller。默认都是全局的,这些条件只要满足一个就好
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
// 指定包
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
// 基于哪些类所在的包
Class<?>[] basePackageClasses() default {};
// 指定类型
Class<?>[] assignableTypes() default {};
// 指定注解
Class<? extends Annotation>[] annotations() default {};
}
对应的判断逻辑在HandlerTypePredicate#test
中,前面已经说了,会从全局中获取,在扫描的时候会将所有的@ControllerAdvice拿到,在应用的时候在判断。类似的操作如下:
这是在判断@ModelAttitude中的操作。
关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。