上篇我们介绍了Jackson
的配置、官方注解与自定义序列化器和反序列化器等功能,这些功能都比较常规并且有很强的实用性。本篇将关注Jackson
的一些相对高级的功能,赶紧让我们来看看吧。
泛型反序列化
针对一般的JSON
反序列化,通过ObjectMapper.readValue(String, Class<T>)
即可正常处理反序列化操作,但是如果反序列化的目标是一个泛型类,如下所示:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class Box<T> implements 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());
}
}
JavaType
另一种是使用com.fasterxml.jackson.databind.JavaType
,这是Jackson
提供的专门用于定义参数化泛型的类,该类一般需要通过com.fasterxml.jackson.databind.type.TypeFactory
类型工厂生成,TypeFactory
可以由ObjectMapper
获取得到,其提供了很多构造参数化泛型的方法,参见下图:
这里我们介绍几种常用的方法:
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());
}
}
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());
}
}
constructParametricType
这个方法用于创建参数化泛型,对泛型的约束是最小的,有非常不错的通用性,第一个参数表示泛型类型,第二个参数表示泛型参数类型。示例如下:
public class JacksonDeserializeTest {
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
static class Triple<F, S, T> implements 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());
}
}
其他方法
除了以上几个常用的方法外,其他方法的使用都十分相似,使用上非常容易上手,可以直接通过com.fasterxml.jackson.databind.JavaType.getGenericSignature()
方法打印出定义的泛型是否符合自己想要的。比如constructArrayType
可以构造泛型数组;constructType
可以直接从类或者TypeReference
构造成JavaType
,该方法是连接TypeReference
和JavaType
的关键。
延伸扩展
除了TypeReference
和JavaType
可以构造参数化泛型外,实际上JDK
中的sun
包也提供了一个sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl.make
静态方法可以创建参数化泛型,当然生成的ParameterizedTypeImpl
无法直接被ObjectMapper
使用(需要经过TypeFactory.constructType(java.lang.reflect.Type)
转换成JavaType
)。而在Gson
中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
的序列化和反序列化。
通过内省器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
中,运行用例的结果同样说明了自定义注解确实生效了。
两种实现的比较
两种实现方法各有优点,通过@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(0, new 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(0, new MappingJackson2HttpMessageConverter(om));
}
}
如上,新增该配置类并且让其能够被Spring
扫描到即可,这样进行JSON
消息转换时就会使用我们自定义的转换器。由于比较简单,这里就不进一步验证了。
与Redis集成
考虑到现在的Java
项目基本都是基于Spring
全家桶,这里Jackson
与Redis
的集成我们也就基于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
: ConfiguredPolymorphicTypeValidator
(of typecom.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
的泛型反序列化、自定义注解以及与Spring
和Redis
集成时的实现与注意点,这些知识能够加深我们对Jackson
的了解并让开发更加简单与高效,由衷期望你能够掌握这些知识。
原文始发于微信公众号(三维家技术实践):这些Jackson用法你都会吗(下篇)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/30552.html