一、背景:
在SpringMVC项目的controller层中,通常通过ResponseBody注解实现将一个DTO对象序列化成json字符串输出到前端,但在实际很多情况下不同接口都是输出同一个大的DTO对象中的部分字段信息,比如包含人员信息的DTO对象employeeDTO,第一个接口只需要输出人员基本信息,第二个接口只需要输出人员的联系信息,第三个接口只需要输出人员的工作经历,第四个接口只需要输出人员的教育信息,等等其他接口分别输出不同的信息,通常的做法是针对每种输出定义一个employeeDTO对象的子DTO对象(只包含其部分信息),然后在每个controller接口里通过BeanMapper进行对象转换,同时还涉及到对象的封包和解包,这样做带来三个弊端:
1、代码重复冗余;
2、不用接口的转换要先复制代码再修改部分细节,容易出错;
3、不易扩展,以后要新增一个接口或者增加删除部分字段信息时,设计到大量代码的修改;
动态json输出技术,只需要在controller接口上添加一个注解,在注解中配置好不输出的字段信息,实现对代码的无侵入性,减少重复代码,增加删除字段以及增加接口只需要修改下不输出字段信息列表即可,扩展非常方便;
二、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对象和要忽略的属性列表,代码如下:
1 @Retention(RetentionPolicy.RUNTIME) 2 public @interface JsonFieldFilter { 3 Class<?> mixin() default Object.class; 4 Class<?> target() default Object.class; 5 } 6 7 @Retention(RetentionPolicy.RUNTIME) 8 public @interface JsonFieldFilters { 9 JsonFieldFilter[] value(); 10 }
四、切面中实现动态json输出:
1、定义切面类,指定要切入的方法;
关键代码:
1 @Around("execution(public * com.*.*.*.controller.EmployeeController.*(..))")
2、扫描切面方法上是否有上面定义的自定义注解,只有在定义了注解的情况下才需要修改json输出,否则按原来的输出方式进行输出;
关键代码:
1 MethodSignature msig = (MethodSignature) pjp.getSignature(); 2 JsonFieldFilter jsonFieldFilter = msig.getMethod().getAnnotation(JsonFieldFilter.class); 3 if (jsonFieldFilter == null) { 4 return pjp.proceed(); 5 }
3、读取自定义注解上的目标DTO对象和要忽略的属性列表,将其设置到ObjectMapper对象中并输出json到Response对象中;
关键代码:
1 ObjectMapper mapper = new ObjectMapper(); 2 Class<?> mixin = jsonFieldFilter.mixin(); 3 Class<?> target = jsonFieldFilter.target(); 4 if (target != null) { 5 mapper.addMixInAnnotations(target, mixin); 6 } else { 7 mapper.addMixInAnnotations(msig.getMethod().getReturnType(), mixin); 8 } 9 mapper.writeValue(response.getOutputStream(), pjp.proceed());
4、针对每一种不同输出的接口定义一个注解,注解中指定要忽略的字段属性列表;
关键代码:
1 @JsonIgnoreProperties(value={"accountStatus", "level", "gender", 2 "startDate", "endDate", "pLoginName", "pUserName"}) 3 public interface EmployeeDetailInfoFilter { 4 }
5、在controller接口方法上添加注解;
关键代码:
1 @JsonFieldFilter(mixin=EmployeeDetailInfoFilter.class, target=EmployeeVODTO.class)
切面的完整代码如下:
1 @Aspect 2 @Component 3 @EnableAspectJAutoProxy 4 public class JsonFilterAspect { 5 6 private final static Logger LOGGER = LoggerFactory.getLogger(JsonFilterAspect.class); 7 8 @Around("execution(public * com.*.*.*.controller.EmployeeController.*(..))") 9 public Object doAround(ProceedingJoinPoint pjp) throws Throwable { 10 MethodSignature msig = (MethodSignature) pjp.getSignature(); 11 JsonFieldFilter jsonFieldFilter = msig.getMethod().getAnnotation(JsonFieldFilter.class); 12 if (jsonFieldFilter == null) { 13 return pjp.proceed(); 14 } 15 16 HttpServletResponse response = null; 17 Object[] args = pjp.getArgs(); 18 if ((args.length > 0) && (args[0] instanceof HttpServletResponse)) { 19 response = (HttpServletResponse)args[0]; 20 } 21 if (response == null) { 22 return pjp.proceed(); 23 } 24 25 try { 26 ObjectMapper mapper = new ObjectMapper(); 27 Class<?> mixin = jsonFieldFilter.mixin(); 28 Class<?> target = jsonFieldFilter.target(); 29 if (target != null) { 30 mapper.addMixInAnnotations(target, mixin); 31 } else { 32 mapper.addMixInAnnotations(msig.getMethod().getReturnType(), mixin); 33 } 34 35 response.setHeader("Content-Type", "application/json;charset=UTF-8"); 36 mapper.writeValue(response.getOutputStream(), pjp.proceed()); 37 38 return null; 39 } catch (Exception ex) { 40 LOGGER.error("返回输出json失败,错误信息:" + ex.getMessage(), ex); 41 } 42 43 return pjp.proceed(); 44 } 45 46 }
需要注意两点:
1、由于要将DTO对象序列化为json字符串并输出到前端,因此需要获取Response对象,所以上面代码中约定好接口的第一个参数为HttpResponse对象;
2、由于jackson版本的原因,可能在低版本的jackson中输出中文时会有乱码,因此需要在输出前添加下面的设置代码,以保证中文输出不会乱码:
1 response.setHeader("Content-Type", "application/json;charset=UTF-8");