这些Jackson用法你都会吗(下篇)

    上篇我们介绍了Jackson的配置、官方注解与自定义序列化器和反序列化器等功能,这些功能都比较常规并且有很强的实用性。本篇将关注Jackson的一些相对高级的功能,赶紧让我们来看看吧。

泛型反序列化

    针对一般的JSON反序列化,通过ObjectMapper.readValue(String, Class<T>)即可正常处理反序列化操作,但是如果反序列化的目标是一个泛型类,如下所示:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class Box<Timplements Serializable {
    private static final long serialVersionUID = 1L;
    private T content;
}

    这时候使用objectMapper.readValue(json, Box.class)进行反序列化实际上并不是十分合适:其一是方法返回值Box会有使用了原生泛型警告;其二是Box在后续的使用中会使人感到迷惑,确定不了Box中的content到底是什么数据类型,使用中容易导致ClassCastException

    针对泛型类的反序列化,Jackson提供了readValue的重载方法(objectMapper.readValue(String, TypeReference<T>)和objectMapper.readValue(String, JavaType))让我们可以更加方便地对泛型类进行反序列化。

TypeReference

    其中,com.fasterxml.jackson.core.type.TypeReference是一个抽象类,通常通过实现TypeReference以完成泛型限定(类实现的时候需要声明泛型参数,这时候父类可以获取得到这个泛型信息)。比如需要转成Box<String>,那么直接通过new TypeReference<Box<String>>(){}即可生成对应的TypeReference。(缺点是TypeReference最终都会生成一个匿名内部类)。

public class JacksonTest {

    @Test
    public void genericDeserTest(){
        String json = "{"content":"小饼干"}";
        Box<String> value = om.readValue(json, new TypeReference<Box<String>>() {
        });
        assert "小饼干".equals(value.getContent());
    }

}
这些Jackson用法你都会吗(下篇)
TypeReference最终都会生成一个匿名内部类

JavaType

    另一种是使用com.fasterxml.jackson.databind.JavaType,这是Jackson提供的专门用于定义参数化泛型的类,该类一般需要通过com.fasterxml.jackson.databind.type.TypeFactory类型工厂生成,TypeFactory可以由ObjectMapper获取得到,其提供了很多构造参数化泛型的方法,参见下图:

这些Jackson用法你都会吗(下篇)
TypeFactory参数化泛型构造方法

    这里我们介绍几种常用的方法:

constructCollectionType和constructCollectionLikeType

    对于constructCollectionLikeType方法,第一个入参表示类似容器的类或者JavaType,第二个入参表示容器成员类或者JavaType,即前者是容器类型,后者是容器成员类型;相应地,constructCollectionType方法的第一个参数则被限定了必须是Collection的实现(即必须是Java集合框架中的Collection)。示例如下;

public class JacksonDeserializeTest {
    @Test
    @DisplayName("类集合泛型")
    void collection() throws Exception {
        TypeFactory typeFactory = om.getTypeFactory();

        CollectionLikeType strBoxType = typeFactory.constructCollectionLikeType(Box.class, String.class);
        System.out.println(strBoxType.getGenericSignature());

        CollectionLikeType strListBoxType = typeFactory.constructCollectionLikeType(Box.class, typeFactory.constructCollectionType(List.class, String.class));
        System.out.println(strListBoxType.getGenericSignature());

        CollectionType strListType = typeFactory.constructCollectionType(List.class, String.class);
        System.out.println(strListType.getGenericSignature());

        CollectionType strSetType = typeFactory.constructCollectionType(Set.class, typeFactory.constructType(String.class));
        System.out.println(strSetType.getGenericSignature());
    }
}
这些Jackson用法你都会吗(下篇)
构造类集合泛型

constructMapType和constructMapLikeType

    对于constructMapLikeType方法,第一个入参表示类似映射表的类型,第二个参数表示映射表键的类型,第三个参数表示映射表值的类型;相应地,constructMapType方法的第一个参数也被限定了必须是Map的实现。示例如下:

public class JacksonDeserializeTest {
    @Test
    @DisplayName("类映射表泛型")
    void map() throws Exception {
        TypeFactory typeFactory = om.getTypeFactory();
        MapLikeType strIntMap = typeFactory.constructMapLikeType(Map.class, String.class, Integer.class);
        System.out.println(strIntMap.getGenericSignature());

        MapLikeType strIntListMap = typeFactory.constructMapLikeType(Map.class, typeFactory.constructType(String.class),
                typeFactory.constructCollectionLikeType(List.class, Integer.class));
        System.out.println(strIntListMap.getGenericSignature());

        MapType oriStrIntMap = typeFactory.constructMapType(LinkedHashMap.class, String.class, Integer.class);
        System.out.println(oriStrIntMap.getGenericSignature());

        MapType oriStrIntListMap = typeFactory.constructMapType(TreeMap.class, typeFactory.constructType(String.class),
                typeFactory.constructCollectionType(List.class, Integer.class));
        System.out.println(oriStrIntListMap.getGenericSignature());
    }
}
这些Jackson用法你都会吗(下篇)
构造类映射表泛型

constructParametricType

    这个方法用于创建参数化泛型,对泛型的约束是最小的,有非常不错的通用性,第一个参数表示泛型类型,第二个参数表示泛型参数类型。示例如下:

public class JacksonDeserializeTest {
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Accessors(chain = true)
    static class Triple<FSTimplements Serializable {
        private F first;
        private S second;
        private T third;
    }

    @Test
    @DisplayName("参数化泛型")
    void parametricType() throws Exception {
        TypeFactory typeFactory = om.getTypeFactory();
        JavaType strBoxType = typeFactory.constructParametricType(Box.class, String.class);
        System.out.println(strBoxType.getGenericSignature());

        JavaType intListBoxType = typeFactory.constructParametricType(Box.class, typeFactory.constructCollectionType(List.class, Integer.class));
        System.out.println(intListBoxType.getGenericSignature());

        JavaType tripleType = typeFactory.constructParametricType(Triple.class, String.class, Integer.class, String.class);
        System.out.println(tripleType.getGenericSignature());
    }
}
这些Jackson用法你都会吗(下篇)
构造参数化泛型

其他方法

    除了以上几个常用的方法外,其他方法的使用都十分相似,使用上非常容易上手,可以直接通过com.fasterxml.jackson.databind.JavaType.getGenericSignature()方法打印出定义的泛型是否符合自己想要的。比如constructArrayType可以构造泛型数组;constructType可以直接从类或者TypeReference构造成JavaType,该方法是连接TypeReferenceJavaType的关键。

延伸扩展

    除了TypeReferenceJavaType可以构造参数化泛型外,实际上JDK中的sun包也提供了一个sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl.make静态方法可以创建参数化泛型,当然生成的ParameterizedTypeImpl无法直接被ObjectMapper使用(需要经过TypeFactory.constructType(java.lang.reflect.Type)转换成JavaType)。而在GsonParameterizedTypeImpl却可以直接使用。

这些Jackson用法你都会吗(下篇)
Sun的ParameterizedTypeImpl同样可以构造参数化泛型

自定义注解

    有时候我们会有一些特殊的需求会用到自定义注解来处理JSON的格式化,Jackson同样提供了对自定义注解的支持。

通过@JacksonAnnotationsInside注解实现自定义注解

    Jackson提供了@com.fasterxml.jackson.annotation.JacksonAnnotationsInside注解用于实现自定义注解参与JSON格式化,只需要在自定义注解上添加该注解,并且通过@com.fasterxml.jackson.databind.annotation.JsonSerialize指定序列化器,@com.fasterxml.jackson.databind.annotation.JsonDeserialize指定反序列化器(可以只指定其中一个,这样只在序列化或者反序列化时注解才生效)。示例如下:

// 自定义Base64编解码注解
@Inherited
@Documented
@JacksonAnnotationsInside
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JsonSerialize(using = Base64EncodedSerializer.class)
@JsonDeserialize(using = Base64DecodedDeserializer.class)
public @interface Base64Codec {

}

// Base64序列化器
public class Base64EncodedSerializer extends JsonSerializer<String{

    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (Objects.isNull(value)) {
            gen.writeNull();
        } else {
            gen.writeString(Base64.getEncoder().encodeToString(value.getBytes(StandardCharsets.UTF_8)));
        }
    }

}

// Base64反序列化器
public class Base64DecodedDeserializer extends JsonDeserializer<String{

    @Override
    public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        String value = p.getValueAsString();
        if (Objects.isNull(value)) {
            return null;
        }
        return new String(Base64.getDecoder().decode(value), StandardCharsets.UTF_8);
    }

}

// 测试用例
public class CustomAnnotationTest {

    private final ObjectMapper om = new ObjectMapper();
    private final ObjectMapper prettyOm = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Accessors(chain = true)
    static class UserInfo implements Serializable {
        private static final long serialVersionUID = 1L;
        private String username;
        @Base64Codec
        private String avatar;
    }

    @Test
    @DisplayName("自定义注解")
    void customAnnotation() throws Exception {
        String avatar = "avatar/reka.png";
        UserInfo user = new UserInfo("Reka", avatar);
        String json = prettyOm.writeValueAsString(user);
        System.out.println(json);
        UserInfo info = om.readValue(json, UserInfo.class);
        assert user.getAvatar().equals(info.getAvatar());
    }
}

    当我们运行测试用例后,将打印出如下结果同时断言成立,说明自定义注解确实参与到了JSON的序列化和反序列化。

这些Jackson用法你都会吗(下篇)
通过@JacksonAnnotationsInside实现自定义注解

通过内省器AnnotationIntrospector实现自定义注解

    Jackson同时提供了一个非常强大的抽象类AnnotationIntrospector注解内省器用于编织我们的自定义注解功能,我们这里只简单介绍以下使用该类实现自定义注解,只需要实现该抽象类并覆盖findSerializer(查询并返回注解元素对应的序列化器)和findDeserializer(查询并返回注解元素对应的反序列化器)方法,然后将其设置到ObjectMapper中,那么该ObjectMapper将激活对应的自定义注解格式化功能。示例如下:

// 自定义注解内省器
public class Base64CodecIntrospector extends AnnotationIntrospector {

    private static final Base64EncodedSerializer SERIALIZER = new Base64EncodedSerializer();
    private static final Base64DecodedDeserializer DESERIALIZER = new Base64DecodedDeserializer();

    @Override
    public Version version() {
        return PackageVersion.VERSION;
    }

    @Override
    public Object findSerializer(Annotated am) {
        Base64Codec2 anno = am.getAnnotation(Base64Codec2.class);
        if (Objects.nonNull(anno)) {
            return SERIALIZER;
        }
        return super.findSerializer(am);
    }

    @Override
    public Object findDeserializer(Annotated am) {
        Base64Codec2 anno = am.getAnnotation(Base64Codec2.class);
        if (Objects.nonNull(anno)) {
            return DESERIALIZER;
        }
        return super.findDeserializer(am);
    }

}

// 测试用例
public class CustomAnnotationTest {

    private final ObjectMapper om = new ObjectMapper();
    private final ObjectMapper prettyOm = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Accessors(chain = true)
    static class RoleInfo implements Serializable {
        private static final long serialVersionUID = 1L;
        private String roleName;
        @Base64Codec2
        private String avatar;
    }

    @Test
    @DisplayName("内省器实现自定义注解")
    void customAnnotationWithIntrospector() throws Exception {
        ObjectMapper om = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT).setAnnotationIntrospector(new Base64CodecIntrospector());
        String avatar = "avatar/role_admin.png";
        RoleInfo role = new RoleInfo("admin", avatar);
        String json = om.writeValueAsString(role);
        System.out.println(json);
        RoleInfo info = om.readValue(json, RoleInfo.class);
        assert role.getAvatar().equals(info.getAvatar());
    }

}

    可以看到,我们自定义了Base64CodecIntrospector并将通过ObjectMapper.setAnnotationIntrospector方法其注册到了ObjectMapper中,运行用例的结果同样说明了自定义注解确实生效了。

这些Jackson用法你都会吗(下篇)
通过内省器实现自定义注解

两种实现的比较

    两种实现方法各有优点,通过@JacksonAnnotationsInside注解实现更加简单;而通过AnnotationIntrospector内省器实现更加灵活(内省器里还有更多强大的方法可以设置某些条件下注解才生效),但一个ObjectMapper只能设置一个内省器(或者拆分设置序列化内省器和反序列化内省器),因此限制了我们必须妥善处理同时使用多个内省器的继承链逻辑(所有内省器都要逐个继承,并且通过super完成上级内省机制)。「具体采用哪种实现方式就见仁见智了,适合自已业务场景的才是最优解」

Spring集成

    通过SpringMVC实现RESTful服务,通常在请求的时候涉及到了JSON的反序列化,响应的时候涉及到了JSON的序列化(对于SpringMVC而言就是消息格式转换)。如果用到了Feign等涉及到消息格式转换同样需要处理格式转换逻辑。SpringMVC中默认集成的JSON消息转换就是Jackson,只不过默认的Jackson通常不能满足我们的业务需求,需要进一步的改造,改造逻辑比较简单,只需要添加一个新的配置类并实现org.springframework.web.servlet.config.annotation.WebMvcConfigurer类,同时覆盖configureMessageConverters方法即可配置属于我们自己的JSON消息格式转换器。示例如下:

@Configuration
public class CustomWebMvcConfiguration implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.removeIf(converter -> (converter instanceof StringHttpMessageConverter || converter instanceof MappingJackson2HttpMessageConverter));
        converters.add(0new StringHttpMessageConverter(StandardCharsets.UTF_8));
        ObjectMapper om = new ObjectMapper()
                // 设置地区为中国
                .setLocale(Locale.CHINA)
                // 设置时区为东8区
                .setTimeZone(TimeZone.getTimeZone(ZoneOffset.ofHours(8)))
                // 设置 Date 时间格式化格式为 yyyy-MM-dd HH:mm:ss
                .setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA))
                // 关闭时间转时间戳
                .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                // 不序列化 null 值
                .setSerializationInclusion(JsonInclude.Include.NON_NULL)
                // 禁用反序列化时遇到未知属性报错
                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                // 设置自定义的注解内省器
                .setAnnotationIntrospector(new Base64CodecIntrospector());
        converters.add(0new MappingJackson2HttpMessageConverter(om));
    }

}

    如上,新增该配置类并且让其能够被Spring扫描到即可,这样进行JSON消息转换时就会使用我们自定义的转换器。由于比较简单,这里就不进一步验证了。

Redis集成

    考虑到现在的Java项目基本都是基于Spring全家桶,这里JacksonRedis的集成我们也就基于Spring进行。主要就是配置RedisTemplate的键值序列化器,同时「激活安全类型规则」(这一步是重点,不激活该配置将导致所有序列化到Redis的数据都丢失类信息,导致后续反序列化只能反序列化为LinkedHashMap)。这里附上其基本配置:

@Configuration
public class CustomRedisTemplateConfiguration {

    @Bean
    @Primary
    public StringRedisSerializer keySerializer() {
        return new StringRedisSerializer(StandardCharsets.UTF_8);
    }

    @Bean
    @Primary
    public Jackson2JsonRedisSerializer<Object> valueSerializer() {
        Jackson2JsonRedisSerializer<Object> valueSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = JsonHelper.INSTANCE.copy();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        BasicPolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder()
                // 信任 com.swj.mj 包下的类
                .allowIfBaseType("com.swj.mj.")
                .allowIfSubType("com.swj.mj.")
                // 信任 Collection、Map 等数据结构
                .allowIfSubType(Collection.class)
                .allowIfSubType(Number.class)
                .allowIfSubType(Map.class)
                .allowIfSubType(Date.class)
                .allowIfSubType(Temporal.class)
                .allowIfSubTypeIsArray()
                .build();
        om.activateDefaultTyping(validator, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        valueSerializer.setObjectMapper(om);
        return valueSerializer;
    }

    @Bean
    @Primary
    public RedisTemplate<String, Object> redisTemplate(StringRedisSerializer keySerializer,
                                                       Jackson2JsonRedisSerializer<Object> valueSerializer,
                                                       RedisConnectionFactory connectionFactory)
 
{
        RedisTemplate<String, Object> tpl = new RedisTemplate<>();
        tpl.setConnectionFactory(connectionFactory);
        tpl.setKeySerializer(keySerializer);
        tpl.setValueSerializer(valueSerializer);
        tpl.setHashKeySerializer(keySerializer);
        tpl.setHashValueSerializer(valueSerializer);
        tpl.setDefaultSerializer(keySerializer);
        tpl.setStringSerializer(keySerializer);
        return tpl;
    }

    @Bean
    @ConditionalOnMissingBean
    public CacheManager cacheManager(StringRedisSerializer keySerializer,
                                     Jackson2JsonRedisSerializer<Object> valueSerializer,
                                     RedisConnectionFactory connectionFactory)
 
{
        RedisCacheConfiguration redisCacheCfg = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer));
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
        return RedisCacheManager.builder(redisCacheWriter).cacheDefaults(redisCacheCfg).build();
    }

}

    其中的核心是com.swj.mj.jackson.configuration.CustomRedisTemplateConfiguration.valueSerializer方法,该方法定义了Redis的值序列化规则,通过BasicPolymorphicTypeValidator多态安全类型校验器设置了信任哪些包进行序列化(通过白名单控制机制规避JNDI注入漏洞)。如果某些类没在信任列表中,那么序列化时将抛出如下异常:

Caused by: com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id ‘com.swj.mj.jackson.web.Menu’ as a subtype of java.lang.Object: Configured PolymorphicTypeValidator (of type com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator) denied resolution at [Source: (byte[])”{“@class”:”com.swj.mj.jackson.web.Menu”,”id”:1,”name”:”首页”,”icon”:”aWNvbi9pbmRleC5pY28=”,”desc”:”首页”}”; line: 1, column: 11]`

    测试用例如下:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = JacksonApplication.class)
public class SpringRedisTest {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Test
    public void testRedis() throws Exception {
        ValueOperations<String, Object> valueOp = redisTemplate.opsForValue();
        String key = "mj:redis:menu:1";
        valueOp.set(key, new Menu(1"首页""icon/index.ico""首页"), 1, TimeUnit.HOURS);

        Menu menu = (Menu) valueOp.get(key);
        System.out.println(menu);
    }

}

    运行结果如下,可以看到写入到Redis时会添加一个@class属性表示对应的类是什么,并且读取时也可以正常进行类型转换。

这些Jackson用法你都会吗(下篇)
Redis集成Jackson

结束语

    本篇着重介绍了Jackson的泛型反序列化、自定义注解以及与SpringRedis集成时的实现与注意点,这些知识能够加深我们对Jackson的了解并让开发更加简单与高效,由衷期望你能够掌握这些知识。


原文始发于微信公众号(三维家技术实践):这些Jackson用法你都会吗(下篇)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/30552.html

(1)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!