前言
我们在实际的项目开发中,肯定会有这样的需求:请求时记录请求日志,返回时记录返回日志;对所有的入参解密,对所有的返回值加密…。这些都是与业务没关系的花边但又不可缺少的功能,若你全都写在Controller的方法内部,那将造成大量的代码重复且严重干扰了业务代码的可读性。
怎么破?可能你第一反应想到的是使用Spring MVC的HandlerInterceptor拦截器来做,没毛病,相信大部分公司的同学也都是这么来干的。那么本文就介绍一种更为优雅、更为简便的实现方案:使用@ControllerAdvice + RequestBodyAdvice/ResponseBodyAdvice不仅仅只有拦截器一种。
@ControllerAdvice / @RestControllerAdvice
对于这个注解你可能即熟悉,却又觉得陌生。熟悉是因为你看到很多项目都使用了@ControllerAdvice + @ExceptionHandler来实现全局异常捕获;陌生在于你除了copy代码时看到过外,自己似乎从来没有真正使用过它。
在前面关于@ModelAttribute和@InitBinder 的相关文章中其实和这个注解是打过照面的:在此注解标注的类上使用@InitBinder等注解可以使得它对"全局"生效实现统一的控制。本文将把@ControllerAdvice此注解作为重点进一步的去了解它的使用以及工作机制。
此类的命名是很有信息量的:Controller的Advice通知。关于Advice的含义,熟悉AOP相关概念的同学就不会陌生了,因此可以看到它整体上还是个AOP的设计思想,只是实现方式不太一样而已。
@ControllerAdvice使用AOP思想可以这么理解:此注解对目标Controller的通知是个环绕通知,织入的方式是注解方式,增强器是注解标注的方法。如此就很好理解@ControllerAdvice搭配@InitBinder/@ModelAttribute/@ExceptionHandler起到的效果喽~
使用示例
最简单的示例前文有过,这里摘抄出一小段:
@RestControllerAdvice public class MyControllerAdvice { @InitBinder public void initBinder(WebDataBinder binder) { //binder.setDisallowedFields("name"); binder.registerCustomEditor(String.class, new StringTrimmerEditor()); } }
这样我们的@InitBinder
标注的方法对所有的Controller
都是生效的。(@InitBinder
写在Controller
内部只对当前处理器生效)
原理分析
接下来就看看这个注解到底是怎么work的,做到知其然,知其所以然。
// @since 3.2 @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 {}; }
官方doc说它可以和如上我指出的三个注解的一起使用。关于它的使用我总结有如下注意事项:
- @ControllerAdvice只需要标注上即可,Spring MVC会在容器里自动探测到它(请确保能被扫描到,否则无效哦~)
- 若有多个@ControllerAdvice可以使用@Order或者Ordered接口来控制顺序
- basePackageClasses属性最终也是转换为了basePackages拿去匹配的,相关代码如下:
HandlerTypePredicate: // 这是packages属性本文:有一个判空的过滤器 public Builder basePackage(String... packages) { Arrays.stream(packages).filter(StringUtils::hasText).forEach(this::addBasePackage); return this; } // packageClasses最终都是转换为了addBasePackage // 只是它的pachage值是:ClassUtils.getPackageName(clazz) // 说明:ClassUtils.getPackageName(String.class) --> java.lang public Builder basePackageClass(Class<?>... packageClasses) { Arrays.stream(packageClasses).forEach(clazz -> addBasePackage(ClassUtils.getPackageName(clazz))); return this; } private void addBasePackage(String basePackage) { this.basePackages.add(basePackage.endsWith(".") ? basePackage : basePackage + "."); }
- 它的
basePackages
扫包不支持占位符Ant
形式的匹配。对于其他几个属性的匹配可参照下面这段匹配代码(我配上了文字说明):
HandlerTypePredicate: @Override public boolean test(Class<?> controllerType) { // 1、若所有属性一个都没有指定,那就是default情况-->作用于所有的Controller if (!hasSelectors()) { return true; } else if (controllerType != null) { // 2、注意此处的basePackage只是简单的startsWith前缀匹配而已~~~ // 说明:basePackageClasses属性最终都是转为它来匹配的, // 如果写了一个Controller类匹配上了,那它所在的包下所有的都是匹配的(因为同包嘛) for (String basePackage : this.basePackages) { if (controllerType.getName().startsWith(basePackage)) { return true; } } // 3、指定具体的Class类型,只会匹配数组里面的这些类型,精确匹配。 for (Class<?> clazz : this.assignableTypes) { if (ClassUtils.isAssignable(clazz, controllerType)) { return true; } } // 4、根据类上的注解类型来匹配(若你想个性化灵活配置,可以使用这种方式) for (Class<? extends Annotation> annotationClass : this.annotations) { if (AnnotationUtils.findAnnotation(controllerType, annotationClass) != null) { return true; } } } return false; }
这里做个说明:
- 若注解的多个属性都给值,它们是取并集的关系(只要符合一个就成)
- ControllerAdviceBean.findAnnotatedBeans()去找@ControllerAdvice类会被调用两次:
- RequestMappingHandlerAdapter#afterPropertiesSet() -> initControllerAdviceCache()
- ExceptionHandlerExceptionResolver#afterPropertiesSet() -> initExceptionHandlerAdviceCache()
- 因为前面说了:@ControllerAdvice它即可用于正常的(和ResponseBodyAdvice等联用),也可使用在异常上(和@RestControllerAdvice联用)
- 若注解的属性一个都没有指定值,那它将作用于所有的@Controller们(为何是所有的Controller呢?各位可参考ControllerAdviceBean#isApplicableToBeanType()方法的调用处:只在处理RequestMapping、@RequestBody相关的类里使用到,当然处理异常时会看这个异常我到底要不要处理等等…)
- 虽然可以不用指定包名,但我个人标记比较喜欢使用basePackageClasses属性把它显示的指明出来~
针对于@RestControllerAdvice,它就类似于@RestController和@Controller之间的区别,在@ControllerAdvice的基础上带有@ResponseBody的效果。
@ControllerAdvice在容器初始化的时候被解析,伪代码如下:
所有的被标注有此注解的Bean最终都变成一个org.springframework.web.method.ControllerAdviceBean,它内部持有Bean本身,以及判断逻辑器(HandlerTypePredicate)的引用
RequestMappingHandlerAdapter: @Override public void afterPropertiesSet() { // Do this first, it may add ResponseBody advice beans initControllerAdviceCache(); ... } private void initControllerAdviceCache() { // 因为它需要通过它去容器内找到所有标注有@ControllerAdvice注解的Bean们 if (getApplicationContext() == null) { return; } // 关键就是在findAnnotatedBeans方法里:传入了容器上下文 List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); // 注意此处是有个排序的~~~~ AnnotationAwareOrderComparator.sort(adviceBeans); ... // 注意:找到这些标注有@ControllerAdvice后并不需要保存下来。 // 而是一个一个的找它们里面的@InitBinder/@ModelAttribute 以及 RequestBodyAdvice和ResponseBodyAdvice // 说明:异常注解不在这里解析,而是在`ExceptionHandlerMethodResolver`里~~~ for (ControllerAdviceBean adviceBean : adviceBeans) { ... } } ControllerAdviceBean: // 找到容器内(包括父容器)所有的标注有@ControllerAdvice的Bean们~~~ public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) { return Arrays.stream(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class)) .filter(name -> context.findAnnotationOnBean(name, ControllerAdvice.class) != null) .map(name -> new ControllerAdviceBean(name, context)) .collect(Collectors.toList()); }
这就是@ControllerAdvice被解析、初始化的原理。它提供一个书写Advice增强器的平台,在初始化的时候根据此类完成解析各种注解作用于各个功能上,从而在运行期直接运行即可。