如何优雅的动态序列化接口返回数据
一、问题引出:
最近重构代码时发现一个有趣的现象:用户信息包括用户登录信息,用户资料信息,用户位置信息,用户积分和余额信息等内容,系统中定义了一个大而全的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");