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

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

一、问题引出:

最近重构代码时发现一个有趣的现象:用户信息包括用户登录信息,用户资料信息,用户位置信息,用户积分和余额信息等内容,系统中定义了一个大而全的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");
相关推荐
IT研究室8 小时前
最新计算机毕业设计选题推荐-基于spring boot的葡萄酒文化宣传网站-网站-文档指导-Java-springboot
java·spring boot·课程设计
凤山老林10 小时前
Spring Boot 集成 Sa-Token 实现高可用权限认证:登录鉴权、动态路由权限与接口防重放实战
spring boot·后端·sa-token·bootstrap
Wx-bishekaifayuan12 小时前
springboot桂味美食分享系统40540-计算机课程设计、毕业设计
spring boot·后端·python·django·课程设计·express·旅游
墨神谕14 小时前
RabbitMQ 从入门到实战:核心原理、消息模型、可靠性机制与 Spring Boot 实战
spring boot·rabbitmq·java-rabbitmq
威哥爱编程14 小时前
【AI全栈12-12】Spring Boot 3.x → 4.x 迁移实操:Jakarta 11、Jackson 3 与 AI 2.0
java·spring boot·后端
威哥爱编程14 小时前
【AI全栈12-11】Spring Boot 把 AI 接口真正上线扛量:限流 / 降级 / 可观测
java·spring boot·后端
小蒜学长14 小时前
基于SpringBoot+uni-app的闪洁家政系统的设计与实现(代码+数据库+LW)
java·spring boot·后端·uni-app·家政服务
做个文艺程序员14 小时前
MQ第07篇:Java SaaS多租户消息隔离实战:RabbitMQ VHost+RocketMQ Tag+命名空间架构
spring boot·rabbitmq·rocketmq·saas·多租户
需要82614 小时前
Spring Boot 启动流程:run() 里到底发生了什么
java·spring boot·后端
凤山老林16 小时前
Spring Boot 集成 Spring Modulith:搭一个能拆微服务的模块化单体
spring boot·spring·微服务