今天的大多数网络都以 JSON 格式交换数据。 Web 服务器、Web 和移动应用程序,甚至物联网设备都使用 JSON 相互通信。因此,一种简单灵活的处理 JSON 的方式对于任何软件在当今世界的生存都是必不可少的。
What is JSON?
JSON 代表“JavaScript Object Notation”,它是一种基于文本的格式,用于表示基于 JavaScript 对象语法的结构化数据。其动态和简单的格式使其非常受欢迎。本质上,它遵循允许嵌套对象和数组的键值映射模型:
{
"array": [
1,
2,
3
],
"boolean": true,
"color": "gold",
"null": null,
"number": 123,
"object": {
"a": "b",
"c": "d"
},
"string": "Hello World"
}
What is Jackson?
Jackson 主要被称为转换 JSON 字符串和普通旧 Java 对象 (POJO) 的库。它还支持许多其他数据格式,例如 CSV、YML 和 XML。
Jackson 因其成熟度(13 岁)以及与 Spring 等流行框架的出色集成而受到许多人的青睐。此外,它是一个由广泛社区积极开发和维护的开源项目。
在底层,Jackson 拥有三个核心包 Streaming、Databind 和 Annotations。有了这些,Jackson 为我们提供了三种处理 JSON-POJO 转换的方法:
Streaming API
这是三种方法中最快的方法,也是开销最小的方法。它以离散事件的形式读取和写入 JSON 内容。 API 提供了一个将 JSON 读入 POJO 的 JsonParser 和一个将 POJO 写入 JSON 的 JsonGenerator。
Tree Model
树模型创建 JSON 文档的内存树表示。 ObjectMapper 负责构建 JsonNode 节点树。这是最灵活的方法,因为它允许我们在 JSON 文档不能很好地映射到 POJO 时遍历节点树。
Data Binding
它允许我们使用属性访问器或使用注释在 POJO 和 JSON 文档之间进行转换。它提供两种类型的绑定:
- 简单数据绑定,它将 JSON 与 Java 映射、列表、字符串、数字、布尔值和空对象相互转换。
- 将 JSON 与任何 Java 类相互转换的完整数据绑定。
ObjectMapper(对象映射器)
ObjectMapper 是 Jackson 库中最常用的部分,因为它是 POJO 和 JSON 之间转换的最简单方法。它位于 com.fasterxml.jackson.databind 中。
readValue() 方法用于将 JSON 从字符串、流或文件解析(反序列化)为 POJO。The readValue()
method is used to parse (deserialize) JSON from a String, Stream, or File into POJOs.
另一方面,writeValue() 方法用于将 POJO 转换为 JSON(序列化)。
ObjectMapper 的工作方式是通过将 JSON 字段的名称与 POJO 中的 getter 和 setter 方法的名称进行匹配来确定哪个 JSON 字段映射到哪个 POJO 字段。
Maven Dependencies
在我们开始查看代码之前,我们需要添加 Jackson Maven 依赖 jackson-databind 进而传递性地添加 jackson-annotations 和 jackson-core
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.3</version>
</dependency>
我们还使用 Lombok 来处理 getter、setter 和构造函数的样板代码。
Jackson 的基本 JSON 序列化和反序列化
让我们通过代码示例来了解 Jackson 最重要的用例。
使用 ObjectMapper 进行基本 POJO/JSON 转换
让我们首先介绍一个名为 Employee 的简单 POJO:
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
private String firstName;
private String lastName;
private int age;
}
让我们首先将 POJO 转换为 JSON 字符串:
public class JacksonTest {
ObjectMapper objectMapper = new ObjectMapper();
@Test
void pojoToJsonString() throws JsonProcessingException {
Employee employee = new Employee("Mark", "James", 20);
String json = objectMapper.writeValueAsString(employee);
System.out.println(json);
}
}
We should see this as output:
{"firstName":"Mark","lastName":"James","age":20}
现在,让我们看看使用 ObjectMapper 将 JSON 字符串转换为 Employee 对象。
public class JacksonTest {
...
@Test
void jsonStringToPojo() throws JsonProcessingException {
String employeeJson = "{\n" +
" \"firstName\" : \"Jalil\",\n" +
" \"lastName\" : \"Jarjanazy\",\n" +
" \"age\" : 30\n" +
"}";
Employee employee = objectMapper.readValue(employeeJson, Employee.class);
assertThat(employee.getFirstName()).isEqualTo("Jalil");
}
}
ObjectMapper 还提供了丰富的 API 来将来自不同来源的 JSON 读取为不同格式,让我们来看看最重要的那些。
从 JSON 文件创建 POJO
这是使用 readValue() 方法完成的。
测试资源employee.json下的JSON文件:
{
"firstName":"Homer",
"lastName":"Simpson",
"age":44
}
public class JacksonTest {
...
@Test
void jsonFileToPojo() throws IOException {
File file = new File("src/test/resources/employee.json");
Employee employee = objectMapper.readValue(file, Employee.class);
assertThat(employee.getAge()).isEqualTo(44);
assertThat(employee.getLastName()).isEqualTo("Simpson");
assertThat(employee.getFirstName()).isEqualTo("Homer");
}
}
从 JSON 的字节数组创建 POJO
public class JacksonTest {
...
@Test
void byteArrayToPojo() throws IOException {
String employeeJson = "{\n" +
" \"firstName\" : \"Jalil\",\n" +
" \"lastName\" : \"Jarjanazy\",\n" +
" \"age\" : 30\n" +
"}";
Employee employee = objectMapper.readValue(employeeJson.getBytes(), Employee.class);
assertThat(employee.getFirstName()).isEqualTo("Jalil");
}
}
从 JSON 创建 POJO 列表
有时 JSON 文档不是一个对象,而是一个对象列表。让我们看看如何阅读它。
employeeList.json
:
[
{
"firstName":"Marge",
"lastName":"Simpson",
"age":33
},
{
"firstName":"Homer",
"lastName":"Simpson",
"age":44
}
]
public class JacksonTest {
...
@Test
void fileToListOfPojos() throws IOException {
File file = new File("src/test/resources/employeeList.json");
List<Employee> employeeList = objectMapper.readValue(file, new TypeReference<>(){});
assertThat(employeeList).hasSize(2);
assertThat(employeeList.get(0).getAge()).isEqualTo(33);
assertThat(employeeList.get(0).getLastName()).isEqualTo("Simpson");
assertThat(employeeList.get(0).getFirstName()).isEqualTo("Marge");
}
}
从 JSON 创建地图
我们可以选择将 JSON 解析为 Java Map,如果我们不知道要从我们尝试解析的 JSON 文件中期待什么,这将非常方便。 ObjectMapper 会将 JSON 中每个变量的名称转换为 Map 键,并将该变量的值转换为该键的值。
public class JacksonTest {
...
@Test
void fileToMap() throws IOException {
File file = new File("src/test/resources/employee.json");
Map<String, Object> employee = objectMapper.readValue(file, new TypeReference<>(){});
assertThat(employee.keySet()).containsExactly("firstName", "lastName", "age");
assertThat(employee.get("firstName")).isEqualTo("Homer");
assertThat(employee.get("lastName")).isEqualTo("Simpson");
assertThat(employee.get("age")).isEqualTo(44);
}
}
忽略未知的 JSON 字段
有时我们期望的 JSON 可能有一些额外的字段没有在我们的 POJO 中定义。 Jackson 的默认行为是在这种情况下抛出 UnrecognizedPropertyException
异常。然而,我们可以告诉杰克逊不要对未知领域感到压力,而只是忽略它们。这是通过将 ObjectMapper 的 FAIL_ON_UNKNOWN_PROPERTIES
配置为 false
来完成的。
employeeWithUnknownProperties.json
:
{
"firstName":"Homer",
"lastName":"Simpson",
"age":44,
"department": "IT"
}
public class JacksonTest {
...
@Test
void fileToPojoWithUnknownProperties() throws IOException {
File file = new File("src/test/resources/employeeWithUnknownProperties.json");
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Employee employee = objectMapper.readValue(file, Employee.class);
assertThat(employee.getFirstName()).isEqualTo("Homer");
assertThat(employee.getLastName()).isEqualTo("Simpson");
assertThat(employee.getAge()).isEqualTo(44);
}
}
在Jackson使用日期
日期转换可能很棘手,因为它们可以用多种格式和规范级别(秒、毫秒等)表示。
日期转为 JSON
在说Jackson 和Date 转换之前,我们需要先说一下Java 8 提供的新的Date API。它是为了解决旧的java.util.Date 和java.util.Calendar 的缺点而引入的。我们主要对使用 LocalDate 类感兴趣,它提供了一种表达日期和时间的强大方法。
为此,我们需要向 Jackson 添加一个额外的模块,以便它可以处理 LocalDate
。
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.13.3</version>
</dependency>
然后我们需要告诉 ObjectMapper 寻找并注册我们刚刚添加的新模块。
public class JacksonTest {
ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules();
...
@Test
void orderToJson() throws JsonProcessingException {
Order order = new Order(1, LocalDate.of(1900,2,1));
String json = objectMapper.writeValueAsString(order);
System.out.println(json);
}
}
Jackson 的默认行为是将日期显示为 [yyyy-MM-dd] 因此,输出将为 {“id”:1,“date”:[1900,2,1]}
然而,我们可以告诉Jackson 我们想要日期的格式。这可以使用 @JsonFormat
注释来完成
public class Order {
private int id;
@JsonFormat(pattern = "dd/MM/yyyy")
private LocalDate date;
}
@Test
void orderToJsonWithDate() throws JsonProcessingException {
Order order = new Order(1, LocalDate.of(2023, 1, 1));
String json = objectMapper.writeValueAsString(order);
System.out.println(json);
}
This should output {"id":1,"date":"01/01/2023"}
.
JSON to Date
我们可以使用上面相同的配置将 JSON 字段读入日期。
order.json
:
{
"id" : 1,
"date" : "30/04/2000"
}
public class JacksonTest {
...
@Test
void fileToOrder() throws IOException {
File file = new File("src/test/resources/order.json");
Order order = objectMapper.readValue(file, Order.class);
assertThat(order.getDate().getYear()).isEqualTo(2000);
assertThat(order.getDate().getMonthValue()).isEqualTo(4);
assertThat(order.getDate().getDayOfMonth()).isEqualTo(30);
}
}
Jackson Annotations
Jackson 中的注解在自定义 JSON/POJO 转换过程的发生方式方面发挥着重要作用。我们已经看到了一个使用 @JsonFormat
注释的日期转换示例。注释主要影响数据的读取、写入方式,甚至两者兼而有之。让我们根据它们的类别来探索其中的一些注释。
Read Annotations
它们影响 Jackson 如何将 JSON 转换为 POJO。
@JsonSetter
当我们想要将 JSON 字符串中的字段与 POJO 中名称不匹配的字段匹配时,这很有用。
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class Car {
@JsonSetter("carBrand")
private String brand;
}
{
"carBrand" : "BMW"
}
public class JacksonTest {
...
@Test
void fileToCar() throws IOException {
File file = new File("src/test/resources/car.json");
Car car = objectMapper.readValue(file, Car.class);
assertThat(car.getBrand()).isEqualTo("BMW");
}
}
@JsonAnySetter
此注释对于 JSON 包含一些未在 POJO 中声明的字段的情况很有用。它与为每个无法识别的字段调用的 setter 方法一起使用。
public class Car {
@JsonSetter("carBrand")
private String brand;
private Map<String, String> unrecognizedFields = new HashMap<>();
@JsonAnySetter
public void allSetter(String fieldName, String fieldValue) {
unrecognizedFields.put(fieldName, fieldValue);
}
}
carUnrecognized.json
file:
{
"carBrand" : "BMW",
"productionYear": 1996
}
public class JacksonTest {
...
@Test
void fileToUnrecognizedCar() throws IOException {
File file = new File("src/test/resources/carUnrecognized.json");
Car car = objectMapper.readValue(file, Car.class);
assertThat(car.getUnrecognizedFields()).containsKey("productionYear");
}
}
Write Annotations
它们影响 Jackson 如何将 POJO 转换为 JSON。
@JsonGetter
当我们想要将 POJO 字段映射到使用不同名称的 JSON 字段时,这很有用。例如,假设我们有一个带有字段名称的 Cat 类,但我们希望它的 JSON 名称是 catName。
@NoArgsConstructor
@AllArgsConstructor
public class Cat {
private String name;
@JsonGetter("catName")
public String getName() {
return name;
}
}
public class JacksonTest {
...
@Test
void catToJson() throws JsonProcessingException {
Cat cat = new Cat("Monica");
String json = objectMapper.writeValueAsString(cat);
System.out.println(json);
}
}
This will output
{
"catName":"Monica"
}
@JsonAnyGetter
此注解允许我们将 Map 对象视为 JSON 属性的来源。假设我们将此地图作为 Cat 类中的一个字段
@NoArgsConstructor
@AllArgsConstructor
public class Cat {
private String name;
@JsonAnyGetter
Map<String, String> map = Map.of(
"name", "Jack",
"surname", "wolfskin"
);
...
}
@Test
void catToJsonWithMap() throws JsonProcessingException {
Cat cat = new Cat("Monica");
String json = objectMapper.writeValueAsString(cat);
System.out.println(json);
}
Then this will output
{
"catName":"Monica",
"name":"Jack",
"surname":"wolfskin"
}
Read/Write Annotations
这些注释会影响读取和写入 JSON。
@JsonIgnore
在写入和读取 JSON 时,将忽略带注释的文件。
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class Dog {
private String name;
@JsonIgnore
private int age;
}
public class JacksonTest {
...
@Test
void dogToJson() throws JsonProcessingException {
Dog dog = new Dog("Max", 3);
String json = objectMapper.writeValueAsString(dog);
System.out.println(json);
}
}
This will print out {"name":"Max"}
The same applies to reading into a POJO as well.
Say we have this dog.json
file:
{
"name" : "bobby",
"age" : 5
}
public class JacksonTest {
...
@Test
void fileToDog() throws IOException {
File file = new File("src/test/resources/dog.json");
Dog dog = objectMapper.readValue(file, Dog.class);
assertThat(dog.getName()).isEqualTo("bobby");
assertThat(dog.getAge()).isNull();
}
}
Jackson 有更多有用的注释,可以让我们更好地控制序列化/反序列化过程。它们的完整列表可以在 Jackson 的 Github 存储库中找到。
Summary
- Jackson 是 Java 中用于 JSON 处理的最强大和最流行的库之一
- Jackson 由 Streaming API、Tree Model 和 Data Binding 三个主要模块组成
- Jackson 提供了一个高度可配置的 ObjectMapper,通过设置其属性和使用注释来满足我们的需求