该这样使用Jackson吗

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;
  }
}
相关推荐
海兰12 分钟前
使用 Spring AI 打造企业级 RAG 知识库第二部分:AI 实战
java·人工智能·spring
历程里程碑29 分钟前
二叉树---二叉树的中序遍历
java·大数据·开发语言·elasticsearch·链表·搜索引擎·lua
小信丶42 分钟前
Spring Cloud Stream EnableBinding注解详解:定义、应用场景与示例代码
java·spring boot·后端·spring
无限进步_1 小时前
【C++】验证回文字符串:高效算法详解与优化
java·开发语言·c++·git·算法·github·visual studio
亚历克斯神1 小时前
Spring Cloud 2026 架构演进
java·spring·微服务
七夜zippoe1 小时前
Spring Cloud与Dubbo架构哲学对决
java·spring cloud·架构·dubbo·配置中心
海派程序猿1 小时前
Spring Cloud Config拉取配置过慢导致服务启动延迟的优化技巧
java
阿维的博客日记1 小时前
为什么不逃逸代表不需要锁,JIT会直接删掉锁
java
William Dawson1 小时前
CAS的底层实现
java
ffqws_1 小时前
Spring Boot入门:通过简单的注册功能串联Controller,Service,Mapper。(含有数据库建立,连接,及一些关键注解的讲解)
数据库·spring boot·后端