@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,只是它是标注在方法返回值上的。

使用场景

  1. 打印请求、响应日志
  2. 对参数解密、对响应加密
  3. 对请求传入的非法字符做过滤/检测

 

 

posted @   残城碎梦  阅读(915)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
历史上的今天:
2021-11-23 介绍Spring的FactoryBean接口
点击右上角即可分享
微信分享提示