有道无术,术尚可求,有术无道,止于术。
本系列Jackson 版本 2.17.0
源码地址:https://gitee.com/pearl-organization/study-jaskson-demo
1. 需求场景
在某个疯狂星期四的下午,小坤👱正在啃着腿,偷偷刷着电脑版某音💃💃💃,突然经理的👴从旁边凑了过来,并道:这(人人)不错.....
小坤👱:经经经....理
经理👴:没事,都是爷们儿,我懂...这里有个需求你做下
经理👴:认证项目中引入了一个第三包,登录时调用包中的认证方法返回令牌对象TokenInfo,我们将令牌序列化后返回给前端
经理👴:令牌对象中的username、password属性添加了@JsonIgnore注解,在序列化时会被忽略,birthday属性格式为yyyy-MM-dd HH:mm:ss::
public class TokenInfo {
@JsonIgnore
String username;
@JsonIgnore
String password;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
Date birthday;
// 省略......
}
经理👴:现在要求序列化时username不能被忽略,birthday的格式只需要具体到年月日
小坤👱:没问题,经理,交给我吧!
小坤👱看了看代码,稍作思考,这.....还不简单吗....既然第三方包的对象无法修改,那我先定义一个VO,使用注解指定序列化规则:
public class TokenInfoVO {
String username;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "GMT+8")
Date birthday;
// 省略......
}
然后将令牌对象转换为VO再执行序列化:
ObjectMapper objectMapper = new ObjectMapper();
// 模拟第三包返回令牌对象
TokenInfo tokenInfo=new TokenInfo();
tokenInfo.setUsername("王法");
tokenInfo.setBirthday(new Date());
tokenInfo.setPassword("123456");
// 转换为对应格式的VO
TokenInfoVO vo=new TokenInfoVO();
vo.setUsername(tokenInfo.getUsername());
vo.setBirthday(tokenInfo.getBirthday());
// 序列化
String value = objectMapper.writeValueAsString(vo);
System.out.println(value);//
几分钟不到,小坤👱就已完成了需求,并满脸欢喜的在群里@了经理:李总,刚才的需求已经做完了。
小坤👱内心OS:怎么样,傻眼了吧,没想到吧,我这么快就做完了,厉害吧!!有我这里牛逼的开发,你就偷着乐吧你!!!
小坤👱紧紧盯着屏幕,期待这经理会在群里表扬几句.........
可是没想到经理👴回话:小坤,你这是写的什么阿!!!有考虑过代码的可维护性、可扩展性、简洁性吗?明明一行代码就能搞定的问题,你这是写的屎山吗???
小坤👱刚浮起的笑意顿时僵住了:犊子,这是在玩我吧,不能修改第三方类,又要添加基于注解的序列化规则,还特么能有其他的处理方式?
2. 混合注解
Mixin翻译为:混合类型;米心;混进;糅合;
Jackson提供了一种混合注解机制(mix-in annotations),允许开发者在不修改原始类的情况下,为其添加或覆盖特定的注解。
2.1 创建混合类
创建一个混合类,将需要重新处理的属性复制过来,并添加需要的注解,例如username设置为不忽略,birthday指定新的格式化模板:
public abstract class MixinTokenInfo {
@JsonIgnore(value = false)
String username;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "GMT+8")
Date birthday;
}
2.2 关联
将混合类与原始类关联起来,就可以动态地改变原始类的序列化或反序列化行为。
ObjectMapper 提供了关联方法:
/**
* 向指定的类或接口添加混合注解,以扩充其功能
*
* @param target 被覆盖的类(或接口)
* @param mixinSource 混合类
* @since 2.5
*/
public ObjectMapper addMixIn(Class<?> target, Class<?> mixinSource)
{
_mixIns.addLocalDefinition(target, mixinSource);
return this;
}
示例代码:
// 关联
objectMapper.addMixIn(TokenInfo.class,MixinTokenInfo.class);
2.3 测试
最终代码如下所示:
ObjectMapper objectMapper = new ObjectMapper();
// 关联
objectMapper.addMixIn(TokenInfo.class,MixinTokenInfo.class);
// 模拟第三包返回令牌对象
TokenInfo tokenInfo=new TokenInfo();
tokenInfo.setUsername("王法");
tokenInfo.setBirthday(new Date());
tokenInfo.setPassword("123456");
// 序列化
String value = objectMapper.writeValueAsString(tokenInfo);
System.out.println(value);
查看输出结果:
{"username":"王法","birthday":"2024-04-10"}
3. @JsonMixin
Spring Boot默认使用Jackson作为JSON框架,例如自动配置中已经帮我们注册了一个ObjectMapper :
@Bean
@Primary // 主要的
@ConditionalOnMissingBean // 在应用程序没有注册ObjectMapper时生效
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}
在进行JSON操作时,直接注入ObjectMapper即可:
@Autowired
ObjectMapper objectMapper;
也提供了相应的扩展,比如Spring Boot 2.7版本提供了@JsonMixin,启动时会扫描加载被@JsonMixin标识的类,不需要再显式的调用addMixIn方法声明关联关系,使用起来更加的方便。
@JsonMixin注解源码如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JsonMixin {
@AliasFor("type")
Class<?>[] value() default {};
@AliasFor("value")
Class<?>[] type() default {};
}
在Spring Boot工程中,混合类添加@JsonMixin注解,并指定原始类:
@JsonMixin(TokenInfo.class) // 需要修改的目标类
public abstract class MixinTokenInfo {
@JsonIgnore(value = false)
String username;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "GMT+8")
Date birthday;
}
单元测试:
@Autowired
ObjectMapper objectMapper;
@Test
void testMixIn() throws JsonProcessingException {
// 模拟第三包返回令牌对象
TokenInfo tokenInfo=new TokenInfo();
tokenInfo.setUsername("王法");
tokenInfo.setBirthday(new Date());
tokenInfo.setPassword("123456");
// 序列化
String value = objectMapper.writeValueAsString(tokenInfo);
System.out.println(value);
}
查看输出结果:
{"username":"王法","birthday":"2024-04-11"}