掌握@ControllerAdvice配合RequestBodyAdvice/ResponseBodyAdvice使用,让你的选择不仅仅只有拦截器【享学Spring MVC】

您所在的位置:网站首页 jsonview使用 掌握@ControllerAdvice配合RequestBodyAdvice/ResponseBodyAdvice使用,让你的选择不仅仅只有拦截器【享学Spring MVC】

掌握@ControllerAdvice配合RequestBodyAdvice/ResponseBodyAdvice使用,让你的选择不仅仅只有拦截器【享学Spring MVC】

#掌握@ControllerAdvice配合RequestBodyAdvice/ResponseBodyAdvice使用,让你的选择不仅仅只有拦截器【享学Spring MVC】| 来源: 网络整理| 查看: 265

版权声明:本文为博主原创文章,遵循 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起到的效果喽~

使用示例

最简单的示例前文有过,这里摘抄出一小段:

@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[] 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)

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

若不标注@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 { boolean supports(MethodParameter returnType, Class> converterType); @Nullable T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response); }

它的内置实现类们:

在这里插入图片描述AbstractMappingJacksonResponseBodyAdvice

它做出了限定:body使用的消息转换器必须是AbstractJackson2HttpMessageConverter才会生效。

public abstract class AbstractMappingJacksonResponseBodyAdvice implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter returnType, Class> converterType) { return AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType); } // 最终使用MappingJacksonValue来序列化body体 @Override @Nullable public final Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType contentType, Class> 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 { //它持有所有的,记住是所有的advice们 private final List requestBodyAdvice = new ArrayList(4); private final List responseBodyAdvice = new ArrayList(4); // 可以看到这是个通用的方法。内来进行区分存储的 getAdviceByType这个区分方法可以看一下 // 兼容到了ControllerAdviceBean以及beanType本身 public RequestResponseBodyAdviceChain(@Nullable List requestResponseBodyAdvice) { this.requestBodyAdvice.addAll(getAdviceByType(requestResponseBodyAdvice, RequestBodyAdvice.class)); this.responseBodyAdvice.addAll(getAdviceByType(requestResponseBodyAdvice, ResponseBodyAdvice.class)); } @Override public boolean supports(MethodParameter param, Type type, Class> converterType) { throw new UnsupportedOperationException("Not implemented"); } @Override public boolean supports(MethodParameter returnType, Class> converterType) { throw new UnsupportedOperationException("Not implemented"); } // 可以看到最终都是委托给具体的Advice去执行的(supports方法) // 特点:符合条件的所有的`Advice`都会顺序的、依次的执行 @Override public HttpInputMessage beforeBodyRead(HttpInputMessage request, MethodParameter parameter, Type targetType, Class> 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 List getMatchingAdvice(MethodParameter parameter, Class> converters, @Nullable List 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模式下的处理器模型上


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3