该这样使用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;
  }
}
相关推荐
Freak嵌入式15 分钟前
全网最适合入门的面向对象编程教程:50 Python函数方法与接口-接口和抽象基类
java·开发语言·数据结构·python·接口·抽象基类
前端小马25 分钟前
解决IDEA出现:java: 程序包javax.servlet不存在的问题
java·servlet·intellij-idea
白总Server35 分钟前
MongoDB解说
开发语言·数据库·后端·mongodb·golang·rust·php
Snowbowღ1 小时前
OpenAI / GPT-4o:Python 返回结构化 / JSON 输出
python·json·openai·api·gpt-4o·pydantic·结构化输出
计算机学姐1 小时前
基于python+django+vue的家居全屋定制系统
开发语言·vue.js·后端·python·django·numpy·web3.py
IH_LZH1 小时前
Broadcast:Android中实现组件及进程间通信
android·java·android studio·broadcast
去看全世界的云1 小时前
【Android】Handler用法及原理解析
android·java
.Net Core 爱好者1 小时前
Redis实践之缓存:设置缓存过期策略
java·redis·缓存·c#·.net
晚睡早起₍˄·͈༝·͈˄*₎◞ ̑̑1 小时前
苍穹外卖学习笔记(五)
java·笔记·学习
码上一元1 小时前
【百日算法计划】:每日一题,见证成长(017)
java·算法