jackson 是 java 编程领域应用广泛的 json 处理工具,同时也是 spring 默认的序列化实现。本文总结了开发中常用的一些 jackson 技巧,注意本文只涉及本人开发中最常用的技巧,一些冷门用法并未提供。如果日后工作中遇到一些使用场景,本文会陆续更新。
下面示例代码中的JsonUtils是包装了 ObjectMapper 的工具类,直接使用 ObjectMapper 也可以。
本文所有技巧都基于 jackson 系列 2.14.3版本
实用注解
@JsonValue
@JsonValue
注解最常用在类字段上,特别是在枚举中的使用很广泛。当字段加了@JsonValue
注解后,表明该类在序列化时会使用该字段作为序列化后的值。
例如下面代码:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
@JsonValue
private String name;
private Integer age;
}
public class JacksonTest {
public static void main(String[] args) {
String json = JsonUtils.toJson(new Person("mahaonan", 30));
System.out.println(json); //mahaonan
}
}
序列化 Person 时,会直接序列化为@JsonValue
指定的属性 name的值
特别的,对于枚举类型,@JsonValue
除了可以指定序列化的值,还可以用于反序列化。
例如下面代码:
@Getter
public enum OperatorEnum {
EQ("eq", "=", "等于"),
;
@JsonValue
private final String code;
private final String sqlOperator;
private final String description;
OperatorEnum(String code, String sqlOperator, String description) {
this.code = code;
this.sqlOperator = sqlOperator;
this.description = description;
}
}
public class JacksonTest {
public static void main(String[] args) {
String json1 = JsonUtils.toJson(OperatorEnum.EQ);
System.out.println(json1); //"eq"
OperatorEnum parse1 = JsonUtils.parse("\"eq\"", OperatorEnum.class);
System.out.println(parse1); //EQ
}
}
可见,枚举类型的序列化和反序列化都依托于该注解。
注意
枚举反序列化在该版本(2.14.3)的jackson 中是支持的,旧版本可能不支持,如果发现不支持,请升级版本。
除了用于属性上,@JsonValue
还可用于方法上,用法类似,当作用于方法时,代表将方法的返回值作为序列化的结果。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
private String name;
private Integer age;
@JsonValue
public String serialize() {
return "name:" + name + ", age:" + age;
}
}
public class JacksonTest {
public static void main(String[] args) {
String json1 = JsonUtils.toJson(new Person("zhangsan", 18));
System.out.println(json1); // "name:zhangsan, age:18"
}
}
需要注意的是,无论@JsonValue作用于哪里,在一个类中最多只能有一个该注解。
@JsonProperty
@JsonProperty
通常用于类字段上,用于标明该字段在序列化/反序列化时的名称。
使用场景多见于 json 和 java 实体类字段定义不一致的时候。
例如如下代码:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
@JsonProperty("_name")
private String name;
private Integer age;
}
public class JacksonTest {
public static void main(String[] args) {
String json1 = JsonUtils.toJson(new Person("zhangsan", 18));
System.out.println(json1); // {"age":18,"_name":"zhangsan"}
}
}
@JsonIgnore
@JsonIgnore
通常用于类字段上,用于标明该字段在序列化/反序列化时忽略。
例如如下代码:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
@JsonIgnore
private String name;
private Integer age;
}
public class JacksonTest {
public static void main(String[] args) {
String json1 = JsonUtils.toJson(new Person("zhangsan", 18));
System.out.println(json1); //{"age":18}
}
}
@JsonInclude
@JsonInclude
用来控制 不同字段值情况下输出 json 的格式。
也就是说,假如你需要某个字段为 null 的时候不序列化,那么可以使用该注解。
该注解可以用在属性和类上,如果用于类,表明该类下所有字段都采用该策略。
JsonInclude.Include
枚举值有 7 种,这里只介绍最常用的几种,其余的可以参考源码注释。
ALWAYS:默认策略,任何情况下都包含该属性。
NON_NULL:只有当属性不为 null 时才会被包含。
CUSTOM:使用自定义的 ValueFilter 来确定是否序列化。
[!warning] 注意
其他的例如NON_EMPTY,NON_DEFAULT使用时一定要注意影响范围,建议多尝试下,可能会排除一些你认为无需排除的值。
以上三种是开发中最常用的策略,一般情况下有NON_NULL就足够了。如果有一些特别的定制化逻辑,推荐使用自定义的ValueFilter来进行判断。
NON_NULL使用见如下代码:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
@JsonInclude(JsonInclude.Include.NON_NULL)
private String name;
private Integer age;
}
public class JacksonTest {
public static void main(String[] args) {
String json1 = JsonUtils.toJson(new Person(null, 18));
System.out.println(json1); // {"age":18}
}
}
假如我们有这样的需求,名称为 null 或者包含zhangsan的不序列化,那么可以使用ValueFilter。见如下代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
@JsonInclude(value = JsonInclude.Include.CUSTOM, valueFilter = FilterNameInclude.class)
private String name;
private Integer age;
}
public class FilterNameInclude {
@Override
public boolean equals(Object obj) {
if (obj == null) {
return true;
}
if (obj instanceof String) {
String str = (String) obj;
return str.contains("zhangsan");
}
return false;
}
}
public class JacksonTest {
public static void main(String[] args) {
String json1 = JsonUtils.toJson(new Person("zhangsan", 18));
System.out.println(json1); // {"age":18}
String json2 = JsonUtils.toJson(new Person("zhangsan", 18));
System.out.println(json2); // {"age":18}
String json3 = JsonUtils.toJson(new Person("lisi", 18));
System.out.println(json3); // {"name":"lisi","age":18}
}
}
可见,只需要在某个类中定义 equals逻辑,如果返回 true,说明不序列化。
@JsonFormatter
用于控制时间序列化/反序列化时的格式,常见的用法如下:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date birthday;
常用场景
全局设置常用参数
在开发中,我们通常会全局定义一个 ObjectMapper对象,然后配置一些默认的规则,这样就不用每次都新建一个。除此之外,配置好一些规则后,一些注解也可以不再使用(例如@JsonFormatter)。
下面是我日常开发中常用的一些配置项:
通用配置
public static void fillDefaultProperty(ObjectMapper mapper) {
//支持单引号
mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
//反序列化时忽略未知属性字段
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//空字符串反序列化到Object时,设置为NULL
mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
//空数组反序列化到Object时,设置为NULL
mapper.configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true);
//支持单值的对象反序列到数组
mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
//忽略无法转换的对象
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
}
如果需要默认开启为 null 不序列化,可以设置如下属性
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
日期配置
public static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DATE_FORMAT = "yyyy-MM-dd";
/**
* 适配localDateTime
* * @param mapper
*/
public static void adapterLocalDate(ObjectMapper mapper) {
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);
SimpleModule javaTimeModule = new SimpleModule();
javaTimeModule.addSerializer(Date.class, new DateSerializer(false, new SimpleDateFormat(DATE_TIME_FORMAT)));
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_FORMAT)));
javaTimeModule.addDeserializer(Date.class, new CustomDateDeserializer());
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_FORMAT)));
mapper.registerModule(javaTimeModule);
mapper.setTimeZone(TimeZone.getDefault());
}
在使用了该日期配置后,除了支持LocalTime和LocalDateTime之外,也无需在字段上添加@JsonFormatter即可实现默认的yyyy-MM-dd HH:mm:ss格式。
[!NOTE] Tips
如果是 Springboot 项目,可以在修改内置的 ObjectMapper 使其适配日期,这样前后端交互的 DTO 中就不需要设置日期格式了!
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.forEach(converter -> {
if (converter instanceof MappingJackson2HttpMessageConverter) {
MappingJackson2HttpMessageConverter jacksonConverter =
(MappingJackson2HttpMessageConverter) converter;
ObjectMapper mapper = jacksonConverter.getObjectMapper();
JacksonUtils.fillDefaultProperty(mapper);
JacksonUtils.adapterLocalDate(mapper);
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
((MappingJackson2HttpMessageConverter) converter).setObjectMapper(mapper);
}
});
}
序列化时保留类型信息
在实际开发中,我们有时候会有这样的情况,属性是一个 Object对象。此时序列化时是没有问题的。但是反序列化的时候,由于 jackson 并不知道这个 json 对应的实际类型是啥,因此就会按默认的策略执行:
数组反序列化为 ArrayList
对象反序列化为 LinkedHashMap
其他基本类型按预设反序列化
这样我们就会丢掉之前的对象信息,可能会导致一些问题。此时我们就可以设置序列化时保留类型信息,这样反序列化的时候就可以反序列化到我们之前的对象。
/**
* 序列化时保留实际类型信息
*
* @param mapper
*/
public static void enableDefaultTyping(ObjectMapper mapper) {
BasicPolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Object.class)
.build();
mapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
}
父子类多态序列化
在实际开发中,有时候会有这样的设计,有一个父类,然后会有一堆子类拥有各自的属性,不同的字段。父类中有一个类型的枚举,用于区分不同的子类型。例如在报表设计中,父类是通用的图表定义,子类可能有折线图,柱状图,饼图等等。如下代码示例:
@Data
public class ChartAxisConfigDTO {
@ApiModelProperty("图表类型")
private ChartTypeEnum chartType;
}
/**
* 折线图轴配置
* @author mahaonan
*/@EqualsAndHashCode(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Data
public class LineAxisConfigDTO extends ChartAxisConfigDTO{
/**
* 类别轴/维度
*/
@JsonProperty("xAxis")
private List<ChartFieldDTO> xAxis;
/**
* 子类别/维度
*/
@JsonProperty("xSubAxis")
private List<ChartFieldDTO> xSubAxis;
/**
* 值轴/指标
*/
@JsonProperty("yAxis")
private List<ChartFieldDTO> yAxis;
}
/**
* 饼图轴配置
* @author mahaonan
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class PieAxisConfigDTO extends ChartAxisConfigDTO {
/**
* 扇区标签/维度
*/
@JsonProperty("xAxis")
private List<ChartFieldDTO> xAxis;
/**
* 扇区标签/指标
*/
@JsonProperty("yAxis")
private List<ChartFieldDTO> yAxis;
}
此时,序列化时是没问题的,jackson 会自动根据对象的实际类型来进行序列化。但是反序列化时,jackson 并不知道要序列化成哪个子对象。例如对于如下的 json 字符串,反序列化时只能反序列化为父类,导致丢失属性。
{
"chartType": 3,
"xAxis": [
{}
],
"yAxis": [
{}
]
}
chartType=3 对应饼图,我们期望是序列化成饼图对应的类。
这种情况下,可以使用如下技巧。
jackson 提供了两个注解@JsonTypeInfo和@JsonSubTypes
@JsonTypeInfo用于处理多态类型,可以指定类型信息的包含方式,以及如何使用该信息来选择合适的类进行反序列化。
@JsonSubTypes则是配合@JsonTypeInfo,用于指明什么情况下用什么类进行操作。
对于上述的场景,我们只需要按如下代码设置:
@Data
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "chartType", visible = true, include = JsonTypeInfo.As.EXISTING_PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = ChartAxisConfigDTO.class, name = "1"),
@JsonSubTypes.Type(value = LineAxisConfigDTO.class, name = "2"),
@JsonSubTypes.Type(value = PieAxisConfigDTO.class, name = "3"),
})
public class ChartAxisConfigDTO {
@ApiModelProperty("图表类型")
private ChartTypeEnum chartType;
}
@JsonTypeInfo各种设置,就是告诉 jackson 我们使用了名为chartType的类型信息,并且要求在反序列化为 java 对象后保留该字段(visible = true)。同时,这个字段是我们预先就设置的,不需要额外添加该类型信息。
@JsonSubTypes 则规定了 chartType 的值是多少时,用哪个子类进行反序列化。
泛型处理
泛型的反序列化是实际开发中很头疼也很常见的,jackson 在这方面的使用也很方便。
此时需要用到 Jackson 提供的 TypeFactory
public JavaType constructParametricType(Class<?> parametrized, Class<?>... parameterClasses) {
int len = parameterClasses.length;
JavaType[] pt = new JavaType[len];
for (int i = 0; i < len; ++i) {
pt[i] = _fromClass(null, parameterClasses[i], EMPTY_BINDINGS);
}
return constructParametricType(parametrized, pt);
}
parametrized
:类型擦除类型,也就是外面的类型,例如 List.class
parameterClasses
:泛型对象参数,也就是<>里面的类型,例如 Person.class,有多个泛型类型就传入多个
集合类型
public class JacksonTest {
public static final ObjectMapper mapper = new ObjectMapper();
public static JavaType constructGenericType(Class<?> genericClass, Class<?>... parameterClasses) {
return mapper.getTypeFactory().constructParametricType(genericClass, parameterClasses);
}
public static void main(String[] args) throws Exception{
// 将数组饭反序列化为List<Person>
List<Person> personList = mapper.readValue("[{\"name\":\"zhangsan\",\"age\":18,\"birthday\":\"2021-08-01 00:00:00\"}]", constructGenericType(List.class, Person.class));
}
}
多层嵌套
对于嵌套的 list,需要用如下的方法构造
/**
* 构造嵌套List类型
*/
public static JavaType constructNestListType(Class<?> elementClass) {
TypeFactory typeFactory = mapper.getTypeFactory();
JavaType innerListType = typeFactory.constructCollectionType(List.class, elementClass);
return typeFactory.constructCollectionType(List.class, innerListType);
}
public class JacksonTest {
public static final ObjectMapper mapper = new ObjectMapper();
public static void main(String[] args) throws Exception{
List<List<Person>> personList2 = mapper.readValue("[[{\"name\":\"zhangsan\",\"age\":18,\"birthday\":\"2021-08-01 00:00:00\"}]]", constructNestListType(Person.class));
System.out.println(personList2);
}
}
Res统一响应
开发中常常会定义统一的返回值包装,例如
@Data
public class Res<T> {
private Integer code;
private String message;
private T data;
}
对于这种对象的反序列化也很简单,和上面一致。如果 T 是一个简单类型,一层包装即可。如果是 List 这种集合类型,嵌套一层即可。总之就一个原则,JavaType需要构建出匹配的泛型对象和参数对象。
public static void main(String[] args) throws Exception{
JavaType javaType = constructGenericType(Res.class, constructGenericType(List.class, Person.class).getRawClass());
Res<List<Person>> res = mapper.readValue("{\"code\":200,\"message\":\"success\",\"data\":[{\"name\":\"zhangsan\",\"age\":18,\"birthday\":\"2021-08-01 00:00:00\"}]}", javaType);
System.out.println(res);
}
或者,还有下面一种办法,利用TypeReference实现。
public static void main(String[] args) throws Exception{
Res<List<Person>> res2 = mapper.readValue("{\"code\":200,\"message\":\"success\",\"data\":[{\"name\":\"lisi\",\"age\":20,\"birthday\":\"2021-08-01 00:00:00\"}]}", new TypeReference<Res<List<Person>>>() {
});
System.out.println(res2);
}
这种方法更加直接,什么样的嵌套对象,就创建什么样的TypeReference
泛型即可。
总之,对于泛型的处理,用好 TypeFactory
即可。