@ControllerAdvice配合RequestBodyAdvice/ResponseBodyAdvice使用
我们在实际的项目开发中,肯定会有这样的需求:请求时记录请求日志,返回时记录返回日志;对所有的入参解密,对所有的返回值加密…。这些都是与业务没关系的花边但又不可缺少的功能,若你全都写在Controller的方法内部,那将造成大量的代码重复且严重干扰了业务代码的可读性。
怎么破?可能你第一反应想到的是使用Spring MVC的HandlerInterceptor拦截器来做。那么本文就介绍一种更为优雅、更为简便的实现方案:使用@ControllerAdvice + RequestBodyAdvice/ResponseBodyAdvice不仅仅只有拦截器一种。
@ControllerAdvice / @RestControllerAdvice
@ControllerAdvice定义:
// @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 {};
}
关于它的使用我总结有如下注意事项:
- @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形式的匹配。对于其他几个属性的匹配可参照下面这段匹配代码(我配上了文字说明):
@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;
}
这里做个说明:
(1)若注解的多个属性都给值,它们是取并集的关系(只要符合一个就成)
(2)ControllerAdviceBean.findAnnotatedBeans()去找@ControllerAdvice类会被调用两次:
RequestMappingHandlerAdapter#afterPropertiesSet() -> initControllerAdviceCache()
ExceptionHandlerExceptionResolver#afterPropertiesSet() -> initExceptionHandlerAdviceCache()
前面说了:@ControllerAdvice它即可用于正常的(和ResponseBodyAdvice等联用),也可使用在异常上(和@RestControllerAdvice联用)
(3)若注解的属性一个都没有指定值,那它将作用于所有的@Controller们(为何是所有的Controller呢?各位可参考ControllerAdviceBean#isApplicableToBeanType()方法的调用处:只在处理RequestMapping、@RequestBody相关的类里使用到,当然处理异常时会看这个异常我到底要不要处理等等…)
虽然可以不用指定包名,但我个人标记比较喜欢使用basePackageClasses属性把它显示的指明出来~
针对于@RestControllerAdvice,它就类似于@RestController和@Controller之间的区别,在@ControllerAdvice的基础上带有@ResponseBody的效果。
@ControllerAdvice在容器初始化的时候被解析,伪代码如下(注意:不同Spring版本入口可能不同,有些方法被进一步封装):
所有的被标注有此注解的Bean最终都变成一个org.springframework.web.method.ControllerAdviceBean,它内部持有Bean本身,以及判断逻辑器(HandlerTypePredicate)的引用
//版本 spring 5.2.8
// -- RequestMappingHandlerAdapter:
@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBody advice beans
this.methodResolver = new ControllerMethodResolver(this.argumentResolverConfigurer,
this.reactiveAdapterRegistry, this.applicationContext, this.messageReaders);
}
// -- ControllerMethodResolver
private void initControllerAdviceCaches(ApplicationContext applicationContext) {
List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(applicationContext);
for (ControllerAdviceBean bean : beans) {
Class<?> beanType = bean.getBeanType();
if (beanType != null) {
Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
this.modelAttributeAdviceCache.put(bean, attrMethods);
}
Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
if (!binderMethods.isEmpty()) {
this.initBinderAdviceCache.put(bean, binderMethods);
}
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(bean, resolver);
}
}
}
}
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,默认就会被添加进去,所以注解@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来指定视图类型:
@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执行原理简述
它主要是在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。可见最底层的原理就是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,只是它是标注在方法返回值上的。
使用场景
- 打印请求、响应日志
- 对参数解密、对响应加密
- 对请求传入的非法字符做过滤/检测
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
2021-11-23 介绍Spring的FactoryBean接口