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

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

一、问题引出:

最近重构代码时发现一个有趣的现象:用户信息包括用户登录信息,用户资料信息,用户位置信息,用户积分和余额信息等内容,系统中定义了一个大而全的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");
相关推荐
深蓝轨迹4 分钟前
SpringBoot YAML配置文件全解析:语法+读取+高级用法
java·spring boot·后端·学习
深蓝轨迹16 分钟前
乐观锁 vs 悲观锁 含面试模板
java·spring boot·笔记·后端·学习·mysql·面试
tant1an2 小时前
Spring Boot 基础入门:从核心配置到 SSMP 整合实战
java·数据库·spring boot·sql·spring
彭于晏Yan4 小时前
Springboot实现微服务监控
spring boot·后端·微服务
小江的记录本4 小时前
【Spring Boot—— .yml(YAML)】Spring Boot中.yml文件的基础语法、高级特性、实践技巧
xml·java·spring boot·后端·spring·spring cloud·架构
稻草猫.4 小时前
SpringBoot日志全解析:从调试到持久化
java·开发语言·spring boot·java-ee·idea
老友@5 小时前
接口调用的演进史——从“发 HTTP 请求”到“可治理的系统能力
spring boot·后端·架构
深蓝轨迹5 小时前
IDEA 中 Spring Boot 配置文件的自动提示消失(无法扫描配置文件)的完整解决方案
java·spring boot·intellij-idea
毕业设计-小慧6 小时前
计算机毕业设计springboot电影选座与订票系统 基于SpringBoot的影院在线票务管理平台 基于SpringBoot的智能影厅座位预约系统
spring boot·后端·课程设计
常利兵7 小时前
Spring Boot + MyBatis,给数据穿上“隐形盔甲”
java·spring boot·mybatis