如何优雅的动态序列化接口返回数据

如何优雅的动态序列化接口返回数据

一、问题引出:

最近重构代码时发现一个有趣的现象:用户信息包括用户登录信息,用户资料信息,用户位置信息,用户积分和余额信息等内容,系统中定义了一个大而全的VO对象UserFullDataVO包含了上面的所有信息,在UserService服务中提供了一个方法getUserFullData返回了用户信息。在Controller中针对前端的需要分别定义了以下的接口:

less 复制代码
//获取用户登录信息
@GetMapping("/user/user-account")
UserAccountVO getUserAcccount(@RequestHeader("userId") Long userId);

//获取用户资料信息
@GetMapping("/user/user-info")
UserInfoVO getUserInfo(@RequestHeader("userId") Long userId);

//获取用户位置信息
@GetMapping("/user/user-location")
UserLocationVO getUserLocation(@RequestHeader("userId") Long userId);

//获取用户积分和余额信息
@GetMapping("/user/user-balance")
UserBalanceVO getUserBalance(@RequestHeader("userId") Long userId);

其实现代码如下:

less 复制代码
UserAccountVO getUserAcccount(@RequestHeader("userId") Long userId) {
  ...............
  UserFullDataVO data = userService.getUserFullData(userId);
  
  UserAccountVO resultVO = new UserAccountVO();
  BeanUtils.copyProperties(data, resultVO);
  return resultVO;
}

//其他几个接口的实现都类似

这样做一来会造成大量的重复代码,二来需要针对每个接口定制一个VO实体对象,非常繁琐。其实这些接口都是在UserFullDataVO对象中只给前端返回部分需要的字段,那么有没有一种优雅的办法来动态指定哪些字段需要输出到前端了?

我们接口返回的是VO实体对象,但是Spring框架层会将其序列化成json数据返回给前端(我们项目中序列化用的jackson),因此我们可以在接口上通过注解指定要输出或者过滤掉的字段,然后再用动态代理技术在序列化时只序列化指定的字段,这样就优雅的完成了动态序列化接口返回数据。

二、jackson简介:

Jackson是一个简单基于Java应用库,Jackson可以轻松的将Java对象转换成json对象和xml文档,同样也可以将json、xml转换成Java对象。Jackson所依赖的jar包较少,简单易用并且性能也要相对高些,并且Jackson社区相对比较活跃,更新速度也比较快。

Jackson提供了一系列注解,方便对JSON序列化和反序列化进行控制,下面介绍一些常用的注解。

1、@JsonIgnore 此注解用于属性上,作用是进行JSON操作时忽略该属性;

2、@JsonFormat 此注解用于属性上,作用是把Date类型直接转化为想要的格式,如@JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss")。

3、@JsonProperty 此注解用于属性上,作用是把该属性的名称序列化为另外一个名称,如把trueName属性序列化为name,@JsonProperty("name")。

针对上面的问题,可以在DTO对象上添加jackson注解来实现动态输出,但是这种方案会有代码侵入性,尤其是DTO对象还是从其他模块引入时,无法修改别人的DTO定义代码时,这种方案则无能为力了,因此这里我们采用了一种通过Spring AOP技术实现的无代码侵入性的动态json输出;

三、实现自定义注解:

首先要实现自定义注解,在这个注解里面包含要忽略字段的DTO对象和要忽略的属性列表,代码如下:

scss 复制代码
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonFieldFilter {
    Class<?> mixin() default Object.class;
    Class<?> target() default Object.class;
}

@Retention(RetentionPolicy.RUNTIME)
public @interface JsonFieldFilters {
    JsonFieldFilter[] value();
}

四、定义切面并实现动态JSON输出:

1、定义切面类,指定要切入的方法:

java 复制代码
@Around("execution(public * com.*.*.*.controller.EmployeeController.*(..))")

2、扫描切面方法上是否有上面定义的自定义注解,只有在定义了注解的情况下才需要修改json输出,否则按原来的输出方式进行输出:

ini 复制代码
MethodSignature msig = (MethodSignature) pjp.getSignature();
JsonFieldFilter jsonFieldFilter = msig.getMethod().getAnnotation(JsonFieldFilter.class);
if (jsonFieldFilter == null) {
    return pjp.proceed();
}

3、读取自定义注解上的目标DTO对象和要忽略的属性列表,将其设置到ObjectMapper对象中并输出json到Response对象中:

ini 复制代码
ObjectMapper mapper = new ObjectMapper();
Class<?> mixin = jsonFieldFilter.mixin();
Class<?> target = jsonFieldFilter.target();
if (target != null) {
    mapper.addMixInAnnotations(target, mixin);
} else {
    mapper.addMixInAnnotations(msig.getMethod().getReturnType(), mixin);
}
mapper.writeValue(response.getOutputStream(), pjp.proceed());

4、针对每一种不同输出的接口定义一个注解,注解中指定要忽略的字段属性列表:

kotlin 复制代码
@JsonIgnoreProperties(value={"accountStatus", "level", "gender",
                            "startDate", "endDate", "pLoginName", "pUserName"})
public interface EmployeeDetailInfoFilter {
}

5、在controller接口方法上添加注解:

python 复制代码
@JsonFieldFilter(mixin=EmployeeDetailInfoFilter.class, target=EmployeeVODTO.class)

6、切面中实现动态JSON输出:

ini 复制代码
@Aspect
@Component
@EnableAspectJAutoProxy
public class JsonFilterAspect {

    private final static Logger LOGGER = LoggerFactory.getLogger(JsonFilterAspect.class);

    @Around("execution(public * com.*.*.*.controller.EmployeeController.*(..))")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature msig = (MethodSignature) pjp.getSignature();
        JsonFieldFilter jsonFieldFilter = msig.getMethod().getAnnotation(JsonFieldFilter.class);
        if (jsonFieldFilter == null) {
            return pjp.proceed();
        }

        HttpServletResponse response = null;
        Object[] args = pjp.getArgs();
        if ((args.length > 0) && (args[0] instanceof HttpServletResponse)) {
            response = (HttpServletResponse)args[0];
        }
        if (response == null) {
            return pjp.proceed();
        }

        try {
            ObjectMapper mapper = new ObjectMapper();
            Class<?> mixin = jsonFieldFilter.mixin();
            Class<?> target = jsonFieldFilter.target();
            if (target != null) {
                mapper.addMixInAnnotations(target, mixin);
            } else {
                mapper.addMixInAnnotations(msig.getMethod().getReturnType(), mixin);
            }

            response.setHeader("Content-Type", "application/json;charset=UTF-8");
            mapper.writeValue(response.getOutputStream(), pjp.proceed());

            return null;
        } catch (Exception ex) {
            LOGGER.error("返回输出json失败,错误信息:" + ex.getMessage(), ex);
        }

        return pjp.proceed();
    }

}

1、由于要将DTO对象序列化为json字符串并输出到前端,因此需要获取Response对象,所以上面代码中约定好接口的第一个参数为HttpResponse对象;

2、由于jackson版本的原因,可能在低版本的jackson中输出中文时会有乱码,因此需要在输出前添加下面的设置代码,以保证中文输出不会乱码:

vbscript 复制代码
response.setHeader("Content-Type", "application/json;charset=UTF-8");
相关推荐
用户908324602731 天前
Spring AI 1.1.2 + Neo4j:用知识图谱增强 RAG 检索(上篇:图谱构建)
java·spring boot
用户8307196840822 天前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解2 天前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解2 天前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记2 天前
Spring Boot Web MVC配置详解
spring boot·后端
初次攀爬者3 天前
Kafka 基础介绍
spring boot·kafka·消息队列
用户8307196840823 天前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
Java水解3 天前
SpringBoot3全栈开发实战:从入门到精通的完整指南
spring boot·后端
初次攀爬者4 天前
RocketMQ在Spring Boot上的基础使用
java·spring boot·rocketmq
花花无缺4 天前
搞懂@Autowired 与@Resuorce
java·spring boot·后端