版权声明:本文为博主原创文章,遵循 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
起到的效果喽~
使用示例
最简单的示例前文有过,这里摘抄出一小段:
代码语言:javascript复制@RestControllerAdvice
public class MyControllerAdvice {
@InitBinder
public void initBinder(WebDataBinder binder) {
//binder.setDisallowedFields("name");
binder.registerCustomEditor(String.class, new StringTrimmerEditor());
}
}
这样我们的@InitBinder
标注的方法对所有的Controller
都是生效的。(@InitBinder
写在Controller
内部只对当前处理器生效)
原理分析
接下来就看看这个注解到底是怎么work的,做到知其然,知其所以然。
代码语言:javascript复制// @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
在容器初始化的时候被解析,伪代码如下:
代码语言:javascript复制所有的被标注有此注解的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体转换为对象之前进行自定义定制;也允许该对象作为实参传入方法之前对其处理。
代码语言:javascript复制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()));
}
...
}
使用示例
代码语言:javascript复制@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哦):
控制台输出:
代码语言:javascript复制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
,只是它是标注在方法返回值上的。
它的源码此处忽略,没什么特别的需要说明的
使用示例
准备一个控制器如下(其它的同上):
代码语言:javascript复制@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
模式。
代码语言:javascript复制需要注意的是,两个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
模式下的处理器模型上