一, 前言
最近有个需求, 其实这个需求以前就有, 比如定义了一个 vo, 包含了 10 个字段,
在接口 A 里, 要返回全部字段;
但是在接口 B 里呢, 需要复用这个 vo, 但是只需要返回其中 8 个字段.
可能呢, 有些同学会选择重新定义一个新的 vo, 但这样, 会导致 vo 类数量特别多; 你说, 要是全部字段都返回吧, 则会给前端同学造成困扰.
针对需要排除部分字段, 希望能达到下面这样的效果:
1, 在 controller 上指定一个 profile
2, 在 profile 要应用到的 class 类型中, 在 field 上添加注解
3, 请求接口, 返回的结果, 如下:
4, 如果注释掉注解那两行, 则效果如下:
针对仅需要包含部分字段, 希望能达到下面的效果:
1, 在 controller 上指定 profile
- /**
- * 测试 include 类型的 profile, 这里指定了:
- * 激活 profile 为 includeProfile
- * User 中, 对应的 field 将会被序列化, 其他字段都不会被序列化
- */
- @GetMapping("/test.do")
- @ActiveFastJsonProfileInController(profile = "includeProfile",clazz = User.class)
- public CommonMessage<User> test() {
- User user = new User();
- user.setId(111L);
- user.setAge(8);
- user.setUserName("kkk");
- user.setHeight(165);
- CommonMessage<User> message = new CommonMessage<>();
- message.setCode("0000");
- message.setDesc("成功");
- message.setData(user);
- return message;
- }
2, 在 ActiveFastJsonProfileInController 注解的 clazz 指定的类中, 对需要序列化的字段进行注解:
- @Data
- public class User {
- @FastJsonFieldProfile(profiles = {"includeProfile"},profileType = FastJsonFieldProfileType.INCLUDE)
- private Long id;
- private String userName;
- private Integer age;
- @FastJsonFieldProfile(profiles = {"includeProfile"},profileType = FastJsonFieldProfileType.INCLUDE)
- private Integer height;
- }
3, 请求结果如下:
- {
- code: "0000",
- data: {
- id: 111,
- height: 165
- },
- desc: "成功"
- }
二, 实现思路
思路如下:
自定义注解, 加在 controller 方法上, 指定要激活的 profile, 以及对应的 class
启动过程中, 解析上述注解信息, 构造出以下 map:
添加 controllerAdvice, 实现
org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
接口, 对返回的 responseBody 进行处理
在 controllerAdvice 中, 获取请求路径, 然后根据请求路径, 去第二步的 map 中, 查询激活的 profile 和 class 信息
根据第四步获取到的: 激活的 profile 和 class 信息, 计算出对应的 field 集合, 比如, 根据 profile 拿到一个字段集合:{name,age}, 而这两个字段都是 exclude 类型的, 所以不能对着两个字段进行序列化
根据第五步的 field 集合, 对 responseBody 对象进行处理, 不对排除集合中的字段序列化
这么讲起来, 还是比较抽象, 具体可以看第一章的效果截图.
三, 实现细节
使用 fastjson 进行序列化
spring boot 版本为 2.1.10, 网上有很多文章, 都是说的 1.x 版本时候的方法, 在 2.1 版本并不适用. 因为默认使用是 jackson, 所以我们这里将 fastjson 的 HttpMessageConverter 的顺序提前了:
- @Configuration
- public class WebMvcConfig extends WebMvcConfigurationSupport {
- @Override
- protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
- super.extendMessageConverters(converters);
- FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
- Charset defaultCharset = Charset.forName("utf-8");
- FastJsonConfig fastJsonConfig = new FastJsonConfig();
- fastJsonConfig.setCharset(defaultCharset);
- converter.setFastJsonConfig(fastJsonConfig);
- converter.setDefaultCharset(defaultCharset);
- // 将 fastjson 的消息转换器提到第一位
- converters.add(0, converter);
- }
- }
读取 controller 上方法的注解信息, 并构造 map
这里, 要点是, 获取到 RequestMapping 注解上的 url, 以及对应方法上的自定义注解:
- RequestMappingHandlerMapping handlerMapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
- // 获取 handlerMapping 的 map
- Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
- for (HandlerMethod handlerMethod : handlerMethods.values()) {
- Class<?> beanType = handlerMethod.getBeanType();// 获取所在类
- // 获取方法上注解
- RequestMapping requestMapping = handlerMethod.getMethodAnnotation(RequestMapping.class);
- }
动态注册 bean
上面构造了 map 后, 本身可以直接保存到一个 public static 字段, 但感觉不是很优雅, 于是, 构造了一个 bean(包含上述的 map), 注册到 spring 中:
- //bean 的类定义
- @Data
- public class VoProfileRegistry {
- private ConcurrentHashMap<String,ActiveFastJsonProfileInController> hashmap = new ConcurrentHashMap<String,ActiveFastJsonProfileInController>();
- }
- // 动态注册到 spring
- applicationContext.registerBean(VoProfileRegistry.class);
- VoProfileRegistry registry = myapplicationContext.getBean(VoProfileRegistry.class);
- registry.setHashmap(hashmap);
在 controllerAdvice 中, 对返回的 responseBody 进行处理时, 根据请求 url, 从上述的 map 中, 获取 profile 等信息:
- org.springframework.Web.servlet.mvc.method.annotation.ResponseBodyAdvice#beforeBodyWrite
- @Override
- public CommonMessage<Object> beforeBodyWrite(CommonMessage<Object> body, MethodParameter returnType, MediaType selectedContentType,
- Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
- ServerHttpResponse response) {
- String requestPath = request.getURI().getPath();
- log.info("path:{}",requestPath);
- VoProfileRegistry voProfileRegistry = applicationContext.getBean(VoProfileRegistry.class);
- ConcurrentHashMap<String, ActiveFastJsonProfileInController> hashmap = voProfileRegistry.getHashmap();
- // 从 map 中获取该 url, 激活的 profile 等信息
- ActiveFastJsonProfileInController activeFastJsonProfileInControllerAnnotation = hashmap.get(requestPath);
- if (activeFastJsonProfileInControllerAnnotation == null) {
- log.info("no matched json profile,skip");
- return body;
- }
- ......// 进行具体的对 responseBody 进行过滤
- }
四, 总结与源码
如果使用 fastjson 的话, 是支持 propertyFilter 的, 具体可以了解下, 也是对字段进行 include 和 exclude, 但感觉不是特别方便, 尤其是粒度要支持到接口级别.
另外, 本来, 我也有另一个方案: 在 controllerAdvice 里, 获取到要排除的字段集合后, 设置到 ThreadLocal 变量中, 然后修改 fastjson 的源码,(fastjson 对类进行序列化时, 要获取 class 的 field 集合, 可以在那个地方, 对 field 集合进行处理), 但是吧, 那样麻烦不少, 想了想就算了.
大家有什么意见和建议都可以提, 也欢迎加群讨论.
源码在码云上 (GitHub 太慢了):
https://gitee.com/ckl111/json-profile
来源: https://www.cnblogs.com/grey-wolf/p/11828582.html