使用示例
准备一个控制器如下(其它的同上):
@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模式下的处理器模型上