农场主的黑科技.

万能的Jackson

字数统计: 1.4k阅读时长: 8 min
2018/11/30 Share

众所周知从Springboot的rest中直接返回Object对象,Spring就会调用Jackson自动转换成Json格式后返回。但有些Jackson的使用技巧你可能还不知道,或许每次都会去谷歌。这篇文章会对Jackson的一些用得少但又超级赞的功能作总结。

首先会介绍Stack Overflow 中找到的一些技巧,随后我会总结一下这次在项目中遇到的坑。

Jackson使用技巧

会分别介绍在SpringBoot的rest中的用法 ,使用注解配置的方法,还有通过ObjectMapper手动构造JSON串时的用法。

用snake_case的JSON匹配camelCase的POJO

来自 java - Jackson overcoming underscores in favor of camel-case - Stack Overflow

SpringBoot的rest中使用

两种配置方式,通过springboot的 application.properties:

1
spring.jackson.property-naming-strategy=SNAKE_CASE

或在类上使用注解:

1
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)

配置后Controller可以取到snake_case的值并赋值给POJO。Controller返回POJO时,Jackson也会先把POJO的属性构造成snake_case的JSON再返回。

注解配置的方式使用

不推荐!

1
2
3
4
5
class User{
@JsonProperty("first_name")
protected String firstName;
protected String getFirstName(){return firstName;}
}

通过ObjectMapper手动构造JSON串时使用

对ObjectMapper调用setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)

用法示例

1
2
3
4
5
6
7
8
9
10
11
12
13
public static class Hoge {
// 这个属性对应JSON的hoge_fuga项
public String hogeFuga;
}

public static void snakeCaseJsonToCamelCasePojo() throws IOException {
ObjectMapper mapper = new ObjectMapper()
.setPropertyNamingStrategy(
PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);

Hoge hogeObj = mapper.readValue("{\"hoge_fuga\": \"piyopiyo\"}", Hoge.class);
System.out.println(hogeObj.hogeFuga);
}

不对null的POJO属性进行序列化

原文 java - How to tell Jackson to ignore a field during serialization if its value is null? - Stack Overflow

对属性或POJO类配置注解的方式使用

1
2
3
4
5
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class Fuga {
public List<String> strings = null; // 这个属性不会被序列化
public int number = 3;
}

在application.properties中统一配置的方式使用

如果是SpringBoot,这应该是最方便的方法了

1
spring.jackson.default-property-inclusion=non_null	#如果值为null,构造json时不加入

对ObjectMapper进行设置后使用

1
2
3
4
5
6
public static void ignoreNullFieldByObjectMapper() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
String json = mapper.writeValueAsString(new Fuga());
System.out.println(json);
}

Date对象序列化后的格式

来自 Date format Mapping to JSON Jackson

注解配置的方式使用

1
2
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm a z")
private Date date;

我试了下,好像必须要配时区

1
2
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", locale = "CHINA",timezone = "Asia/Shanghai")
private Date createdDate;

在application.properties中统一配置的方式使用

1
2
3
4
5
spring:
jackson:
date-format: yyyy-MM-dd HH:mm
locale: zh_CN
time-zone: Asia/Shanghai

对ObjectMapper进行设置后使用

1
2
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm a z");
myObjectMapper.setDateFormat(df);

对ObjectMapper#writeValue() 等输出的JSON进行格式化(pretty print)

对格式没需求

java - Pretty printing JSON from Jackson 2.2’s ObjectMapper - Stack Overflow

想把格式化的缩进从2格改到4格

java - Custom pretty printer using Jackson library - Stack Overflow

想把格式弄成JSON.stringify()那种

https://gist.github.com/komiya-atsushi/832a97c84ccae7cdfc2a

在项目中遇到的坑

之前不知道可以配置无视null,又觉得直接把POJO传出去太危险,毕竟会暴露表构造,所以就对很多接口都专门生成VO类。然后用BeanUtils#copyProperties在每次返回前把POJO拷贝到VO上再返回的。今天早上发现有这个功能之后马上对整个Controller层重构了一番,还是太naive啊。

还有就是对Date对象的格式化了,之前也是每次在返回前手动用DataFormat把POJO中的Date对象转成String后再拷贝个VO。像下面这样,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static <T, R> void checkDate(T po, R vo) {
BeanWrapper poWrapper = new BeanWrapperImpl(po);
BeanWrapper voWrapper = new BeanWrapperImpl(vo);
for (PropertyDescriptor pd : voWrapper.getPropertyDescriptors()) {
Object date;
String fieldName = pd.getDisplayName();
if (pd.getPropertyType() == Date.class
&& (date = poWrapper.getPropertyValue(fieldName)) != null) {

SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm");
String formatted = formatter.format(date);

voWrapper.setPropertyValue(fieldName, formatted);

}
}
}

当时为了写这段也是费了好大功夫。

还有要注意的就是DTO是服务间的数据传输对象,所以感觉还是直接用Date类传递比较好。但我又是在application.properties中同一配置的date-formmat格式,所以是拿不到Date类的。比如我用SpringCloud的FeignClient试图获取带Date的DTO,果然抛了异常

1
com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String

因为它远程调用的另一端并没有进行相同的格式化。

这时我想到,Date默认的Json格式是2018-11-30T15:08:09.955+0800这种。这种格式可以通过StdDateFormat.DATE_FORMAT_STR_ISO8601获取,还有就是注解配置比application.properties配置的优先级要高,

根据这两点,我在DTO的Date类的属性上加入以下注解

1
2
@JsonFormat(pattern = StdDateFormat.DATE_FORMAT_STR_ISO8601)
private Date registerTime;

完美解决。

也就是说,当FeignClient对这个registerTime进行反序列化的时候不会尝试使用application.properties中配置的yyyy-MM-dd HH:mm格式去解析,而是使用StdDateFormat.DATE_FORMAT_STR_ISO8601,也就是”yyyy-MM-dd’T’HH:mm:ss.SSSZ”,去解析它,而这个格式正是远程调用方进行JSON序列化时默认使用的格式,所以能反序列化成Date类。

参考文章 Jackson の痒いところ Tips

CATALOG
  1. 1. Jackson使用技巧
    1. 1.1. 用snake_case的JSON匹配camelCase的POJO
      1. 1.1.1. SpringBoot的rest中使用
      2. 1.1.2. 注解配置的方式使用
      3. 1.1.3. 通过ObjectMapper手动构造JSON串时使用
    2. 1.2. 不对null的POJO属性进行序列化
      1. 1.2.1. 对属性或POJO类配置注解的方式使用
      2. 1.2.2. 在application.properties中统一配置的方式使用
      3. 1.2.3. 对ObjectMapper进行设置后使用
    3. 1.3. Date对象序列化后的格式
      1. 1.3.1. 注解配置的方式使用
      2. 1.3.2. 在application.properties中统一配置的方式使用
      3. 1.3.3. 对ObjectMapper进行设置后使用
    4. 1.4. 对ObjectMapper#writeValue() 等输出的JSON进行格式化(pretty print)
      1. 1.4.1. 对格式没需求
      2. 1.4.2. 想把格式化的缩进从2格改到4格
      3. 1.4.3. 想把格式弄成JSON.stringify()那种
  2. 2. 在项目中遇到的坑