上篇|Jackson注解的用法和场景,不看巨亏_分布式存储

Jackson注解一览

今天总结一下Jackson的一系列注解的用法和场景,或许能帮助你实现一些功能,总结不易,还请多多关注、点赞、转发。

@JacksonAnnotation

这个注解经常用于Jackson自定义注解中,用来标记这是一个Jackson注解,这个胖哥在Jackson脱敏一文中用过它来实现自定义的序列化注解。

@JacksonAnnotationsInside

这个注解用来标记Jackson复合注解,当你使用多个Jackson注解组合成一个自定义注解时会用到它。

/**
* 非空以及忽略未知属性
**/
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonInclude(Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public @interface NotNullAndIgnoreAnnotation {}

@JacksonInject

json属性值将在反序列化时可以被注入,我们先在属性上标记:

@Data
public final class JacksonInjectUser {
@JacksonInject(value = "dynamic")
private String name;
private Integer age;
}

然后​​name​​的值就可以在反序列化的时候动态化,不再需要去解析、拼字段。

@SneakyThrows
@Test
void jacksonInject() {
// 这个值动态化了
String dynamicValue = "some Dynamic value";
InjectableValues.Std injectableValues = new InjectableValues.Std()
// 名称和注解中声明的相同才行
.addValue("dynamic", dynamicValue);
JacksonInjectUser jacksonInjectUser = objectMapper.setInjectableValues(injectableValues)
// 空json 最后居然可以赋值
.readValue("{}", JacksonInjectUser.class);
Assertions.assertEquals(dynamicValue,jacksonInjectUser.getName());
}

注意:​​@JacksonInject​​​中提供了​​useInput​​参数进行绑定策略控制。


@JsonAlias

在反序列化的时候来对Java Bean的属性进行名称绑定,可以绑定多个json的键名。举个例子:

@SneakyThrows
@Test
void jsonAlias(){
// 两个json的类型结构是相同的 可以定义一个Bean来接收
String userJson = "{\"name\": \"felord.cn\",\"age\": 22}";
String itemJson = "{\"category\": \"coco\", \"count\": 50 }";
Domain user = objectMapper.readValue(userJson, Domain.class);
Assertions.assertEquals("felord.cn",user.getStr());
Assertions.assertEquals(22,user.getNum());
Domain item = objectMapper.readValue(itemJson, Domain.class);
Assertions.assertEquals("coco",item.getStr());
Assertions.assertEquals(50,item.getNum());
}

@Data
public class Domain{
@JsonAlias({"name","category"})
private String str;
@JsonAlias({"age","count"})
private Integer num;
}

注意:只能用于json反序列化。


@JsonAnyGetter

在json序列化时可以将Bean中的​​java.util.Map​​类型的属性“平铺展开”,举个例子:

某个Java Bean正常的json序列化结果是:

{
"name": "felord.cn",
"age": 22,
"unMatched": {
"unknown": "unknown"
}
}

但是我们需要:

{
"name": "felord.cn",
"age": 22,
"unknown": "unknown"
}

我们可以对Java Bean这么标记:

@Data
public class MapUser {
private String name;
private Integer age;
private Map<String,Object> unMatched;

@JsonAnyGetter
public Map<String, Object> getUnMatched() {
return unMatched;
}
}

然后我们来试一试:

@SneakyThrows
@Test
void jsonAnyGetter(){
MapUser mapUser = new MapUser();

mapUser.setName("felord.cn");
mapUser.setAge(22);
mapUser.setUnMatched(Collections.singletonMap("unknown","unknown"));

String json = objectMapper.writeValueAsString(mapUser);
// 获取json中unknown节点的值
Object read = JsonPath.parse(json)
.read(JsonPath.compile("$.unknown"));
Assertions.assertEquals("unknown",read);
}

不过这个注解的使用也是有条件的:


  • 不能是静态方法。
  • 必须是无参方法。
  • 方法的返回值必须是​​java.util.Map​​。
  • 一个实体中只能使用一个该注解。

@JsonAnySetter

正好和​​@JsonAnyGetter​​相反,这里就不介绍了。

@JsonAutoDetect

一般情况下,我们认为Jackson序列化对象的前提是有无参构造并且有Getter方法。事实上下面这个类依然可以序列化成json:

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class ConstructUser {
private final String name;
private final Integer age;

public ConstructUser(String name, Integer age) {
this.name = name;
this.age = age;
}
}

我们可以通过调整Java Bean中属性、getter方法、isGetter方法、setter方法、初始化实例的方法。可见级别可以分为:


  • ​DEFAULT​​: 需要根据上下文来判断,一般基于父类的可见性。
  • ​ANY​​:任何级别的都可以自动识别。
  • ​NONE​​:所有级别都不可以自动识别。
  • ​NON_PRIVATE​​:非​​private​​修饰的可以自动识别。
  • ​PROTECTED_AND_PUBLIC​​:被​​protected​​和​​public​​修饰的可以被自动识别。
  • ​PUBLIC_ONLY​​:只有被​​public​​修饰的才可以被自动识别。

@JsonBackReference

这个注解经常和另一个注解​​@JsonManagedReference​​成对出现,它为了解决递归的问题,例如两个类互相持有对方:

Info info = new Info();
Player player = new Player();
player.setId(1);
info.setPlayer(player);
player.setInfo(info);
// 直接无限递归了
String InfiniteRecursionError = objectMapper.writeValueAsString(player);

json序列化的时候直接无限递归了。如果你想得到下面的序列化结果:

// player
{"id":1,"info":{"id":0}}

就需要在类​​Player​​​的​​Info​​​属性上标记​​@JsonManagedReference​​​,同时在​​Info​​​类中的​​Player​​​属性上标记​​@JsonBackReference​​注解。

如果你想在序列化​​Player​​​时直接忽略掉​​Info​​​属性,即期望得到​​{"id":1}​​​,只需要在​​Player​​​的​​Info​​​属性上标记​​@JsonBackReference​​注解。

@JsonClassDescription

Jacksonjson schemas的支持,用来生成整个json的描述信息。

@JsonCreator

Jackson在反序列化时默认会去找Java Bean的无参构造,但是有些Bean没有无参构造,这时​​@JsonCreator​​​就派上用场了。你可以将它标记在构造方法或静态工厂方法上,通常它还需要同​​@JsonProperty​​​或​​@JacksonInject​​配合,就像这样:

@Getter
public class DescriptionUser {
private final String name;
private final Integer age;

@JsonCreator
public DescriptionUser(@JsonProperty("name") String name,
@JsonProperty("age") Integer age) {
this.name = name;
this.age = age;
}
}

对应的单元测试:

@SneakyThrows
@Test
void jsonCreator() {
String json = "{\"name\": \"felord.cn\",\"age\": 22}";
DescriptionUser user = objectMapper.readValue(json, DescriptionUser.class);
Assertions.assertEquals("felord.cn", user.getName());
}

你可以在静态初始化实例工厂方法上试试这个注解。


@JsonEnumDefaultValue

我们在定义性别枚举时往往只定义了​​男​​​和​​女​​两个性别。你不能指望用户守规矩。科学的方法是定义一个枚举用来兜底。就像这样:

public enum Gender {
/**
* Female gender.
*/
FEMALE,
/**
* Male gender.
*/
MALE,
/**
* Unknown gender.
*/
UNKNOWN
}

当用户乱填的时候都定义为​​未知​​。在jackson反序列化支持设置一个默认值来兜底。我们可以在​​Gender#UNKNOWN​​​上标记​​@JsonEnumDefaultValue​​,然后反序列化:

@SneakyThrows
@Test
void jsonEnumDefaultValue(){
// 开启未知枚举值使用默认值特性
objectMapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);
String maleJson = "{\"name\": \"felord.cn\",\"age\": 22,\"gender\":\"MALE\"}";

EnumUser male = objectMapper.readValue(maleJson, EnumUser.class);
Assertions.assertEquals(Gender.MALE,male.getGender());

String unknownJson = "{\"name\": \"felord.cn\",\"age\": 22,\"gender\":\"notClear\"}";
EnumUser unknownGender = objectMapper.readValue(unknownJson, EnumUser.class);
Assertions.assertEquals(Gender.UNKNOWN,unknownGender.getGender());
}

注意:必须手动jackson开启未知枚举值使用默认值特性。


@JsonFilter

同一个实体类根据不同的场景可能需要不同的序列化策略。比如对于A用户实体的某些字段可见,对于B用户另一些字段可见,实现动态的数据字段权限。这种情况下,jackson中其它一些静态注解就很难实现,借助于​​@JsonFilter​​反而简单了,下面是实现方法:

// 只序列化age的策略
@JsonFilter("role_a")
public class OnlyAge extends FilterUser{
}
// 不序列化age的策略
@JsonFilter("role_b")
public class OnlyNameAndGender extends FilterUser{
}

接下来定义​​role_a​​​和​​role_b​​的策略:

@SneakyThrows
@Test
void jsonFilter() {
SimpleFilterProvider simpleFilterProvider = new SimpleFilterProvider();
// role_a只展示age
SimpleBeanPropertyFilter onlyAgeFilter = SimpleBeanPropertyFilter.filterOutAllExcept("age");
// role_b只排除age
SimpleBeanPropertyFilter exceptAgeFilter = SimpleBeanPropertyFilter.serializeAllExcept("age");
simpleFilterProvider.addFilter("role_a", onlyAgeFilter);
simpleFilterProvider.addFilter("role_b", exceptAgeFilter);
objectMapper.setFilterProvider(simpleFilterProvider);

//被JsonFilter标记的类
OnlyAge onlyAgeUser = new OnlyAge();
onlyAgeUser.setName("felord.cn");
onlyAgeUser.setGender(Gender.MALE);
onlyAgeUser.setAge(22);

OnlyNameAndGender onlyNameAndGenderUser = new OnlyNameAndGender();
onlyNameAndGenderUser.setName("felord.cn");
onlyNameAndGenderUser.setGender(Gender.MALE);
onlyNameAndGenderUser.setAge(22);

String onlyAge = objectMapper.writeValueAsString(onlyAgeUser);
// 序列化的json中找不到name节点会抛出PathNotFoundException异常
Assertions.assertThrows(PathNotFoundException.class, () -> JsonPath.parse(onlyAge)
.read(JsonPath.compile("$.name")));
String onlyNameAndGender = objectMapper.writeValueAsString(onlyNameAndGenderUser);
// 序列化的json中找不到age节点会抛出PathNotFoundException异常
Assertions.assertThrows(PathNotFoundException.class, () -> JsonPath.parse(onlyNameAndGender)
.read(JsonPath.compile("$.age")));
}

思考:结合AOP甚至是Spring Security是不是有搞头?


小结

Jackson是一款非常优秀的json类库,提供了丰富的注解来满足各种场景的需要。本篇介绍了一部分注解的用法和场景。胖哥也根据日常一些场景的需要结合这些注解设计了不少动态的、可扩展的、通用的序列化和反序列化功能,用起来非常方便顺手。只有掌握了技术才能运用技术,后续计划把剩下所有的注解都梳理出来分享给大家。另外keycloak的教程也在准备中,还请多多关注和支持。


上篇|Jackson注解的用法和场景,不看巨亏_nokia_02