掌握@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 MVCHandlerInterceptor拦截器来做,没毛病,相信大部分公司的同学也都是这么来干的。那么本文就介绍一种更为优雅、更为简便的实现方案:使用@ControllerAdvice + RequestBodyAdvice/ResponseBodyAdvice不仅仅只有拦截器一种。

@ControllerAdvice / @RestControllerAdvice

对于这个注解你可能即熟悉,却又觉得陌生。熟悉是因为你看到很多项目都使用了@ControllerAdvice + @ExceptionHandler来实现全局异常捕获;陌生在于你除了copy代码时看到过外,自己似乎从来没有真正使用过它。 在前面关于@ModelAttribute@InitBinder 的相关文章中其实和这个注解是打过照面的:在此注解标注的类上使用@InitBinder等注解可以使得它对"全局"生效实现统一的控制。本文将把@ControllerAdvice此注解作为重点进一步的去了解它的使用以及工作机制。

此类的命名是很有信息量的:ControllerAdvice通知。关于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说它可以和如上我指出的三个注解的一起使用。关于它的使用我总结有如下注意事项:

  1. @ControllerAdvice只需要标注上即可,Spring MVC会在容器里自动探测到它(请确保能被扫描到,否则无效哦~)
  2. 若有多个@ControllerAdvice可以使用@Order或者Ordered接口来控制顺序
  3. 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 + ".");  		}
  1. 它的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有关,ResponseBodyAdviceSpring4.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)

使用时需要注意如下几点:

  1. 若不标注@JsonView注解,默认是接收所有(这是我们绝大部分的使用场景)
  2. @JsonView的value有且只能写一个类型(必须写)
  3. @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可见最底层的原理就是readerWithViewreadValue的区别。

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格式,因此此种方式会有很大的用武之地,我举例几个经典使用场景供以参考:

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

总结

本文旨在介绍@ControllerAdviceRequestBodyAdvice/ResponseBodyAdvice的作用,为你解决在解决一些拦截问题时提供一个新的思路,希望能够对你的眼界、代码结构上的把控能有所帮助。 同时也着重介绍了@JsonView的使用:它可以放入参时接收指定的字段;也可以让返回值中敏感字段(如密码、盐值等)不予返回,可做到非常灵活的配置和管理,实现一套代码多处使用的目的,提高集成程度。

咀咒,需要注意的是:xxxBodyAdvice虽然使用方便,但是它的普适性还是没有HandlerInterceptor那么强的,下面我列出使用它的几点局限/限制:

  1. xxxAdvice必须被@ControllerAdvice注解标注了才会生效,起到拦截的效果
  2. 它只能作用于基于消息转换器的请求/响应(参考注解@RequestBody/@ResponseBody
  3. 当然,只能作用于@RequestMapping模式下的处理器模型上