掌握@ControllerAdvice配合RequestBodyAdvice/ResponseBodyAdvice使用,让你的选择不仅仅只有拦截器【享学Spring MVC】
- 2019 年 10 月 10 日
- 笔记
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/f641385712/article/details/101396307
前言
要么出众,要么出局(stand out or get out)。
前言
我们在实际的项目开发中,肯定会有这样的需求:请求时记录请求日志,返回时记录返回日志;对所有的入参解密,对所有的返回值加密…。这些都是与业务没关系的花边但又不可缺少的功能,若你全都写在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; }
说明一点:若注解的多个属性都给值,它们是取并集的关系。
针对于@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增强器的平台,在初始化的时候根据此类完成解析各种注解作用于各个功能上,从而在运行期直接运行即可。
RequestBodyAdvice/ResponseBodyAdvice
顾名思义,它们和@RequestBody
和@ResponseBody
有关,ResponseBodyAdvice
是Spring4.1
推出的,另外一个是4.2
后才有。它哥俩和@ControllerAdvice
一起使用会有很好的化学反应
说明:这哥俩是接口不是注解,实现类需要自己提供实现
RequestBodyAdvice
官方解释为:允许body体转换为对象之前进行自定义定制;也允许该对象作为实参传入方法之前对其处理。
public interface RequestBodyAdvice { // 第一个调用的。判断当前的拦截器(advice是否支持) // 注意它的入参有:方法参数、目标类型、所使用的消息转换器等等 boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType); // 如果body体木有内容就执行这个方法(后面的就不会再执行喽) Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType); // 重点:它在body被read读/转换**之前**进行调用的 HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException; // 它在body体已经转换为Object后执行。so此时都不抛出IOException了嘛~ Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType); }
它的内置实现有这些:
RequestResponseBodyAdviceChain
比较特殊,放在后面重点说明。RequestBodyAdviceAdapter
没啥说的,因此主要看看JsonViewRequestBodyAdvice
这个实现。
JsonViewRequestBodyAdvice
Spring MVC
的内置实现,它支持的是Jackson的com.fasterxml.jackson.annotation.@JsonView
这个注解,@JsonView
一般用于标注在HttpEntity/@RequestBody
上,来决定处理入参的哪些key。 该注解指定的反序列视图将传递给MappingJackson2HttpMessageConverter
,然后用它来反序列化请求体(从而做对应的过滤)。
// @since 4.2 public class JsonViewRequestBodyAdvice extends RequestBodyAdviceAdapter { // 处理使用的消息转换器是AbstractJackson2HttpMessageConverter类型 // 并且入参上标注有@JsonView注解的 @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return (AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType) && methodParameter.getParameterAnnotation(JsonView.class) != null); } // 显然这里实现的beforeBodyRead这个方法: // 它把body最终交给了MappingJacksonInputMessage来反序列处理消息体 // 注意:@JsonView能处理这个注解。也就是说能指定把消息体转换成指定的类型,还是比较实用的 // 可以看到当标注有@jsonView注解后 targetType就没啥卵用了 @Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> selectedConverterType) throws IOException { JsonView ann = methodParameter.getParameterAnnotation(JsonView.class); Assert.state(ann != null, "No JsonView annotation"); Class<?>[] classes = ann.value(); // 必须指定class类型,并且有且只能指定一个类型 if (classes.length != 1) { throw new IllegalArgumentException("@JsonView only supported for request body advice with exactly 1 class argument: " + methodParameter); } // 它是一个InputMessage的实现 return new MappingJacksonInputMessage(inputMessage.getBody(), inputMessage.getHeaders(), classes[0]); } }
说明:这个类只要你导入了jackson的jar,默认就会被添加进去,so注解@JsonView
属于天生就支持的。伪代码如下:
WebMvcConfigurationSupport: @Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { ... if (jackson2Present) { adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice())); adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice())); } ... }
使用示例
@Getter @Setter @ToString public static class User { @JsonView({Simple.class, Complex.class}) private Long id; @JsonView({Simple.class, Complex.class}) private String name; @JsonView({Complex.class}) private Integer age; } // 准备两个view类型(使用接口、类均可) interface Simple {} interface Complex {}
至于我为何这么准备示例,有兴趣的同学可以了解下@JsonView
注解的用法和使用场景,你便会有所收获。
继续准备一个控制器,使用@JsonView
来指定视图类型:
@ResponseBody @PostMapping("/test/requestbody") public String testRequestBodyAdvice(@JsonView(Simple.class) @RequestBody User user) { System.out.println(user); return "hello world"; }
这时候请求(发送的body里有age这个key哦):
控制台输出:
HelloController.User(id=1, name=fsx, age=null)
可以看到即使body体里有age这个key,服务端也是不会给与接收的(age仍然为null),就因为我要的是Simple类型的JsonView。这个时候若换成@JsonView(Complex.class)
那最终的结果就为:
HelloController.User(id=1, name=fsx, age=18)
使用时需要注意如下几点:
- 若不标注
@JsonView
注解,默认是接收所有(这是我们绝大部分的使用场景) @JsonView
的value有且只能写一个类型(必须写)- 若
@JsonView
指定的类型,在POJO的所有属性(或者set方法)里都没有@JsonView
对应的指定,那最终一个值都不会接收(因为一个都匹配不上)。
@JsonView
执行原理简述
简单说说@JsonView
在生效的原理。它主要是在AbstractJackson2HttpMessageConverter
的这个方法里(这就是为何JsonViewRequestBodyAdvice
只会处理这种消息转转器的原因):
AbstractJackson2HttpMessageConverter(实际为MappingJackson2HttpMessageConverter): @Override public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { JavaType javaType = getJavaType(type, contextClass); // 把body内的东西转换为java对象 return readJavaType(javaType, inputMessage); } private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException { if (inputMessage instanceof MappingJacksonInputMessage) { Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView(); if (deserializationView != null) { return this.objectMapper.readerWithView(deserializationView).forType(javaType).readValue(inputMessage.getBody()); } } return this.objectMapper.readValue(inputMessage.getBody(), javaType); }
因为标注了@JsonView
注解就使用的是它MappingJacksonInputMessage
。so可见最底层的原理就是readerWithView
和readValue
的区别。
ResponseBodyAdvice
它允许在@ResponseBody/ResponseEntity
标注的处理方法上在用HttpMessageConverter
在写数据之前做些什么。
// @since 4.1 泛型T:body类型 public interface ResponseBodyAdvice<T> { boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType); @Nullable T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response); }
它的内置实现类们:
AbstractMappingJacksonResponseBodyAdvice
它做出了限定:body使用的消息转换器必须是AbstractJackson2HttpMessageConverter
才会生效。
public abstract class AbstractMappingJacksonResponseBodyAdvice implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType); } // 最终使用MappingJacksonValue来序列化body体 @Override @Nullable public final Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType contentType, Class<? extends HttpMessageConverter<?>> converterType, ServerHttpRequest request, ServerHttpResponse response) { if (body == null) { return null; } MappingJacksonValue container = getOrCreateContainer(body); beforeBodyWriteInternal(container, contentType, returnType, request, response); return container; } }
JsonViewResponseBodyAdvice
继承自父类,用法几乎同上面的@JsonView
,只是它是标注在方法返回值上的。
它的源码此处忽略,没什么特别的需要说明的
使用示例
准备一个控制器如下(其它的同上):
@ResponseBody @GetMapping("/test/responsebody") @JsonView(Simple.class) public User testResponseBodyAdvice() { User user = new User(); user.setId(1L); user.setName("fsx"); user.setAge(18); return user; }
请求结果如下:
它的使用注意事项同上,基本原理同上(writerWithView/writer的区别
)。
RequestResponseBodyAdviceChain
它是代理模式的实现,用于执行指定的RequestBodyAdvice/ResponseBodyAdvice
们,实现方式基本同前面讲过多次的xxxComposite
模式。
需要注意的是,两个advice的
support()
方法都只只只在这里被调用。所以很容易相想到Spring调用advice增强时最终调用的都是它,它就是一个门面。
// @since 4.2 请注意:它的访问权限是default哦 class RequestResponseBodyAdviceChain implements RequestBodyAdvice, ResponseBodyAdvice<Object> { //它持有所有的,记住是所有的advice们 private final List<Object> requestBodyAdvice = new ArrayList<>(4); private final List<Object> responseBodyAdvice = new ArrayList<>(4); // 可以看到这是个通用的方法。内来进行区分存储的 getAdviceByType这个区分方法可以看一下 // 兼容到了ControllerAdviceBean以及beanType本身 public RequestResponseBodyAdviceChain(@Nullable List<Object> requestResponseBodyAdvice) { this.requestBodyAdvice.addAll(getAdviceByType(requestResponseBodyAdvice, RequestBodyAdvice.class)); this.responseBodyAdvice.addAll(getAdviceByType(requestResponseBodyAdvice, ResponseBodyAdvice.class)); } @Override public boolean supports(MethodParameter param, Type type, Class<? extends HttpMessageConverter<?>> converterType) { throw new UnsupportedOperationException("Not implemented"); } @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { throw new UnsupportedOperationException("Not implemented"); } // 可以看到最终都是委托给具体的Advice去执行的(supports方法) // 特点:符合条件的所有的`Advice`都会顺序的、依次的执行 @Override public HttpInputMessage beforeBodyRead(HttpInputMessage request, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) { if (advice.supports(parameter, targetType, converterType)) { request = advice.beforeBodyRead(request, parameter, targetType, converterType); } } return request; } ... // 其余方法略。处理逻辑同上顺序执行。 // 最重要的是如下这个getMatchingAdvice()匹配方法 private <A> List<A> getMatchingAdvice(MethodParameter parameter, Class<? extends A> adviceType) { // 简单的说你想要的是Request的还是Response的List呢? List<Object> availableAdvice = getAdvice(adviceType); if (CollectionUtils.isEmpty(availableAdvice)) { return Collections.emptyList(); } List<A> result = new ArrayList<>(availableAdvice.size()); for (Object advice : availableAdvice) { if (advice instanceof ControllerAdviceBean) { ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice; // 这里面会调用beanTypePredicate.test(beanType)方法 // 也就是根据basePackages等等判断此advice是否是否要作用在本类上 if (!adviceBean.isApplicableToBeanType(parameter.getContainingClass())) { continue; } advice = adviceBean.resolveBean(); } // 当前的advice若是满足类型要求的,那就添加进去 最终执行切面操作 if (adviceType.isAssignableFrom(advice.getClass())) { result.add((A) advice); } } return result; } }
这是批量代理模式的典型实现,Spring
框架中不乏这种实现方式,对使用者非常友好,也很容易控制为链式执行或者短路执行。
初始化解析流程分析
我们知道所有的xxxBodyAdvice
最终都是通过暴露的RequestResponseBodyAdviceChain
来使用的,它内部持有容器内所有的Advice的引用。由于RequestResponseBodyAdviceChain
的访问权限是default
,所以这套机制完全由Spring
内部控制。 他唯一设值处是:AbstractMessageConverterMethodArgumentResolver
。
AbstractMessageConverterMethodArgumentResolver(一般实际为RequestResponseBodyMethodProcessor): // 唯一构造函数,指定所有的advices public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters, @Nullable List<Object> requestResponseBodyAdvice) { Assert.notEmpty(converters, "'messageConverters' must not be empty"); this.messageConverters = converters; this.allSupportedMediaTypes = getAllSupportedMediaTypes(converters); this.advice = new RequestResponseBodyAdviceChain(requestResponseBodyAdvice); }
此构造函数在new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)
时候调用,传进来的requestResponseBodyAdvice
就刚好是在初始化RequestMappingHandlerAdapter
的时候全局扫描进来的所有的增强器们。
使用场景
本文介绍了@ControllerAdvice
的使用以及它的解析原理,最重要的是结合RequestBodyAdvice/ResponseBodyAdvice
来实现类似拦截器的效果。在现在前后端分离的开发模式下,大部分的情况下的请求是json格式,因此此种方式会有很大的用武之地,我举例几个经典使用场景供以参考:
- 打印请求、响应日志
- 对参数解密、对响应加密
- 对请求传入的非法字符做过滤/检测
总结
本文旨在介绍@ControllerAdvice
和RequestBodyAdvice/ResponseBodyAdvice
的作用,为你解决在解决一些拦截问题时提供一个新的思路,希望能够对你的眼界、代码结构上的把控能有所帮助。 同时也着重介绍了@JsonView
的使用:它可以放入参时接收指定的字段;也可以让返回值中敏感字段(如密码、盐值等)不予返回,可做到非常灵活的配置和管理,实现一套代码多处使用的目的,提高集成程度。
咀咒,需要注意的是:xxxBodyAdvice
虽然使用方便,但是它的普适性还是没有HandlerInterceptor
那么强的,下面我列出使用它的几点局限/限制:
xxxAdvice
必须被@ControllerAdvice
注解标注了才会生效,起到拦截的效果- 它只能作用于基于消息转换器的请求/响应(参考注解
@RequestBody/@ResponseBody
) - 当然,只能作用于
@RequestMapping
模式下的处理器模型上