Jackson 2.x 系列【20】混合注解 Mixin Annotations

242 阅读4分钟

有道无术,术尚可求,有术无道,止于术。

本系列Jackson 版本 2.17.0

源码地址:https://gitee.com/pearl-organization/study-jaskson-demo

1. 需求场景

在某个疯狂星期四的下午,小坤👱正在啃着腿,偷偷刷着电脑版某音💃💃💃,突然经理的👴从旁边凑了过来,并道:这(人人)不错.....

小坤👱:经经经....理

经理👴:没事,都是爷们儿,我懂...这里有个需求你做下

经理👴:认证项目中引入了一个第三包,登录时调用包中的认证方法返回令牌对象TokenInfo,我们将令牌序列化后返回给前端

经理👴:令牌对象中的usernamepassword属性添加了@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"}