Jackson是SpringMVC框架默认的json解析器,在实际开发中使用广泛。今天,我们一起来看看如何更好的使用它。
示例代码:github.com/kqcaihong/j...
一 Jackson使用不当导致生产事故
博主工作中遇到过这样一个事故:在使用Jackson的ObjectMapper
反序列化时,如果json串中存在未知属性,将抛出UnrecognizedPropertyException
。而业务代码捕获异常后仅记录本地日志即return,导致业务错误,进而带来资损。 服务间关系如下,serviice A和serviice B引用了不同版本的entity.jar,导致服务间数据交互时模型不一致。 出现上述故障,除了跨团队协作沟通不足之外,也体现出对Jackson中ObjectMapper
一些特性缺乏了解。如反序列化Json串中出现不能识别的属性时是否操作失败,默认为true。
java
// 全局配置
ObjectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 或者在Entity上使用注解
@JsonIgnoreProperties(ignoreUnknown = true)
二 初识Jackson
Jackson是用来序列化和反序列化json数据的Java的开源框架,是目前最流行的json解析器之一 ,它具有以下优点:
- Jackson 是Spring MVC 框架默认的json解析器;jar包依赖较少,简单易用;
- 解析大json文件时,相比其他Gson,fastjson,Jackson处理速度快,运行时内存占用低,性能好;
- Jackson 扩展性很好,如 CSV、XML、YAML 格式处理都对 Jackson 有相应的适配等。
SpringBoot集成了Jackson,在项目中引入spring-boot-starter-json
依赖即可。如果是web项目且引入了spring-boot-starter-web
,则已经包含了该依赖。
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.13.5</version>
</dependency>
如果是非spring项目,就需要分别引入到这些依赖:
- jackson-core,核心包,提供基于"流模式"解析的相关 API,它包括 JsonPaser 和 JsonGenerator,用于解析和生成json;
- jackson-annotations,注解包,提供标准注解功能;
- jackson-databind ,数据绑定包, 提供基于"对象绑定" 解析的相关 API ( ObjectMapper ) 和"树模型" 解析的相关 API (JsonNode);
xml
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.13.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.13.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.5</version>
</dependency>
另外,jackson-databind基于前两个包,只显示引入jackson-databind时,已经间接引入了jackson-core和jackson-annotations。
三 使用Jackson
默认情况下,Jackson不会将类中static和transient的成员变量进行序列化与反序列化操作。 为了演示定义一个User类。
java
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
private Long id;
private String name;
private int age;
private String password;
private Date createTime;
private Date birthDate;
}
3.1 序列化
序列化,即将Java对象转为json字符串。我们来看一个简单示例。
java
@Test
public void writeUser() {
User1 tom = User1.builder().id(1L).name("Tom").age(23).password("1234")
.createTime(new Date()).birthDate(new Date()).build();
try {
String json = new ObjectMapper().writeValueAsString(tom);
System.out.println(json);
} catch (JsonProcessingException e) {
// do something
}
}
3.1.1 忽略属性
如果想对User中password属性不输出,该怎么实现呢?
- 可以使用
@JsonIgnore
,用于属性上,作用是进行序列化或反序列化时忽略该属性。
java
@JsonIgnore
private String password;
- 还可使用
@JsonIgnoreProperties
,用于类上,通过value指定需要忽略的一些属性。
java
@JsonIgnoreProperties(value = {"password"})
public class User {
}
3.1.2 自定义属性名称
User中birthDate表示生日,如果想在序列化时输出为birthday,该如何实现呢?
- 使用
@JsonGetter
在getter方法上,给Java对象序列化时自定义属性名称,对应的有@JsonSetter
。
java
private LocalDate birthDate;
@JsonGetter("birthday")
public LocalDate getBirthDate() {
return birthDate;
}
- 或者使用
@JsonProperty
,用在属性上声明属性名,将同时对序列化/反序列化都生效。
java
@JsonProperty("birthDay")
private LocalDate birthDate;
3.1.3 日期序列化
在上面示例中,Data类型的createTime、birthDate,Jackson默认以时间戳格式输出Date ,难以阅读。如何想要按照如yyyy-MM-dd
格式输出?
- 使用
@JsonFormat
,用于属性上,指定pattern来声明序列化时采用的格式,还可用timezone指定时区
ini
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
- 还可以使用
ObjectMapper.setDateFormat(dateFormat)
来设置Date序列化器,来覆盖ObjectMapper的默认方式。
java
ObjectMapper mapper = new ObjectMapper();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
mapper.setDateFormat(dateFormat);
3.1.4 如何序列化Java8新日期类
当我们将User中birthDate类型修改为LocalDate
时,执行序列化时可能遇到下列报错:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type
java.time.LocalDate
not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: com.learn.more.entiry.User["birthDate"])
此处提示我们需添加jackson-datatype-jsr310的Module(即扩展包)。先引入依赖:Jackson处理JSR 310日期时间的Jar包。
xml
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.8.8</version>
</dependency>
从源码中看到,JavaTimeModule
提供了各种日期类型的序列化器和反序列化器,如 在创建ObjectMapper时,主动注册一个JavaTimeModule
实例,这样就具备了处理各种时间日期类型的能力。
java
ObjectMapper mapper = new ObjectMapper();
JavaTimeModule javaTimeModule = new JavaTimeModule();
mapper.registerModule(javaTimeModule);
可是,LocalDate序列化格式如[1992,11,5],这样也不方便阅读。此时,我们可以覆盖JavaTimeModule中默认的序列化器。
java
// 自定义LocalDate的序列化器、反序列化器
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
3.1.5 自定义序列化器
Jackson支持我们自定义对某个Java类型的序列化方式,如对Data
序列化时,继承Jackson提供的StdSerializer类,指明要处理的Java数据类型,重新serialize()方法。
java
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
public class CustomDateSerializer extends StdSerializer<Date> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public CustomDateSerializer() {
super(Date.class);
}
@Override
public void serialize(Date value, JsonGenerator gen, SerializerProvider provider) throws IOException {
Instant instant = value.toInstant();
ZoneId zoneId = ZoneId.systemDefault();
LocalDateTime localDateTime = instant.atZone(zoneId).toLocalDateTime();
gen.writeString(FORMATTER.format(localDateTime));
}
}
然后使用@JsonSerialize
引用它,标注在属性上即可。
java
@JsonSerialize(using = CustomDateSerializer.class)
private Date createTime;
3.1.6 只输出值非null的属性
Jackson序列化时,默认会输出值为null的属性。如果不需要输出,可做如下配置:
- 使用
@JsonInclude(JsonInclude.Include.NON_NULL)
,用于类上。
java
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
}
- 设置ObjectMapper的SerializationInclusion为Include.NON_NULL
java
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(Include.NON_NULL);
3.2 反序列化
3.2.1 忽略未知JSON字段
在服务间相互调用时,由于各服务引用的基础依赖版本不同,常常导致同一Java类的版本不同。如果 JSON中出现Java对象没有的属性时,默认将反序列化失败。此时,我们可以设置让ObjectMapper忽略未知字段。
java
objectMapper.configure( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
3.2.2 允许基本类型为null
Java对象中是基本数据类型如int、double等的属性,不能为null。如果JSON字符串包含基本类型为为null的字段,ObjectMapper默认会处理这种情况而不报错,对基本类型取默认值。 DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES特性默认是false。 如果我们配置为true,则可能会遇到如下异常。
java
objectMapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, true);
3.2.3 处理泛型
当返序列化带泛型的对象如List<T>
时,该如何告知Jackson目标类型呢?此时,需要使用TypeReference
类
TypeReference ref = new TypeReference<List>() { }; 如下面这个例子,我们将Json数字直接返序列化为
List<User>
对象。
java
public void readList() throws JsonProcessingException {
String userJson = "[{"id":1,"name":"Tom","age":23,"password":"1234","createTime":"2024-05-02 09:24:28"}, {"id":2,"
+ ""name":"Xiao","age":35,"password":"45775","createTime":"2024-05-02 09:24:28"}]";
ObjectMapper mapper = new ObjectMapper();
List<User> users = mapper.readValue(userJson, new TypeReference<List<User>>() {
});
System.out.println(users);
}
四 Module机制
Jackson
三大模块,支持了标准的JDK
类、自定义Java Bean
对象与Json
之间的互相转换。当需要扩展以支持新数据类型时,可以使用Module
构建自定义的模块并注册到ObjectMapper
中。
自定义Module都需要继承jackson-databind中的SimpleModule
类,源码中对于这个类有如下描述:
Vanilla Module implementation that allows registration of serializers and deserializers, bean serializer and deserializer modifiers, registration of subtypes and mix-ins as well as some other commonly needed aspects (addition of custom AbstractTypeResolvers, ValueInstantiators).
模块实现,允许注册序列化器和反序列化器,bean序列化器和反序列化器修饰器,子类型和混合的注册以及一些其他通常需要的方面
在spring-boot-starter-json
中,已经引入了3个广泛使用的模块,并且它们是由官方主导维护着:
- jackson-module-parameter-names
- jackson-datatype-jdk8:对Java8新类型的一些支持,如Optional、LongStream等;
- jackson-datatype-jsr310:对jsr310时间日期类型的支持。
java
@Test
public void jdk8Test() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
// 未注册jdk8模块
System.out.println(mapper.writeValueAsString(OptionalInt.of(1)));
System.out.println(mapper.writeValueAsString(Optional.of("hello")));
System.out.println(mapper.writeValueAsString(IntStream.of(1, 2, 3)));
System.out.println(mapper.writeValueAsString(Stream.of("1", "2", "3")));
}
当注册Jdk8Module后,将可以将Optional、Stream中数据输出。
java
mapper.registerModule(new Jdk8Module());
五 优化
在项目开发中,应该对Jackson进行封装,或者至少提供一个全局单例的ObjectMapper
对象,将特性进行合理配置。在需要序列化、反序列化时,引用该对象即可;而不是每次操作Json都去创建新的ObjectMapper
对象。
java
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
// 全局Jackson配置
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
CustomDateSerializer dateSerializer = new CustomDateSerializer();
ObjectMapper mapper = new Jackson2ObjectMapperBuilder()
.createXmlMapper(false)
.serializers(dateSerializer)
.failOnEmptyBeans(false)
.failOnUnknownProperties(false)
.modules(new JavaTimeModule(), new Jdk8Module())
.build();
// 格式化输出
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}