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

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

一、问题引出:

最近重构代码时发现一个有趣的现象:用户信息包括用户登录信息,用户资料信息,用户位置信息,用户积分和余额信息等内容,系统中定义了一个大而全的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");
相关推荐
lxyzcm15 分钟前
C++23新特性解析:[[assume]]属性
java·c++·spring boot·c++23
迷糊的『迷』2 小时前
vue-axios+springboot实现文件流下载
vue.js·spring boot
小池先生2 小时前
springboot启动不了 因一个spring-boot-starter-web底下的tomcat-embed-core依赖丢失
java·spring boot·后端
苹果醋34 小时前
2020重新出发,MySql基础,MySql表数据操作
java·运维·spring boot·mysql·nginx
小蜗牛慢慢爬行4 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
azhou的代码园4 小时前
基于JAVA+SpringBoot+Vue的制造装备物联及生产管理ERP系统
java·spring boot·制造
wm10434 小时前
java web springboot
java·spring boot·后端
路在脚下@12 小时前
spring boot的配置文件属性注入到类的静态属性
java·spring boot·sql
啦啦右一12 小时前
Spring Boot | (一)Spring开发环境构建
spring boot·后端·spring
森屿Serien12 小时前
Spring Boot常用注解
java·spring boot·后端