掘金 后端 ( ) • 2024-04-26 13:16

有道无术,术尚可求,有术无道,止于术。

本系列 Jackson 版本 2.17.0

源码地址:https://gitee.com/pearl-organization/study-jackson-demo

1. 概述

在前两篇文档中,我们学习了JsonGeneratorJsonParser的简单用法,实际开发中很少用到它们,因为它们属于低级API,自由度高但用起来比较繁琐。 我们使用最多的还是ObjectMapper,它是jackson-databind数据绑定模块提供面向用户的高级API

官方注释中说明了主要的功能特性

  • 提供了从POJOJSON树模型读取和写入JSON的功能,并支持互相转换
  • 支持高度自定义,以使用不同样式的JSON内容
  • 支持更高级的对象概念,如多态泛型和对象标识
  • 充当了更高级的ObjectReaderObjectWriter类的工厂

ObjectMapper类关系图如下所示:

image.png

其中TreeCodecObjectCodec声明了树模型、对象绑定的读写操作,ObjectMapper还有一个子类JsonMapper,专用于处理JSON格式。

2. 案例演示

首先需要引入jackson-databind数据绑定模块:

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.17.0</version>
        </dependency>

2.1 创建对象

ObjectMapper提供了多个构造函数:

image.png

一般使用默认的构造函数即可,其他的方式后面会单独介绍:

        ObjectMapper objectMapper = new ObjectMapper();

2.2 写入

ObjectMapper提供了多个写入方法,支持将对象以JSON格式写入到文件、输出流等对象中,以及返回字符串、字节数组:

image.png

POJO对象转换为JSON字符串(序列化)示例如下:

        // POJO->JSON
        User user = new User();
        user.setId(1767798780627279873L);
        user.setName("坤坤");
        user.setAge(18);
        String json = objectMapper.writeValueAsString(user);
        System.out.println(json);

控制台输出如下:

{"id":1767798780627279873,"name":"坤坤","age":18,"org":null,"roleList":null}

写入到文件示例如下:

        // 写入到文件
        File file = new File("E:\\TD\\pearl\\study-jackson-demo\\jackson-core-demo\\src\\main\\java\\com\\pearl\\jacksoncore\\demo\\file\\user.json");
        objectMapper.writeValue(file,user);

2.3 读取

ObjectMapper也提供了多个读取方法,支持从多种地方读取JSON内容并转换为POJO对象:

image.png

从文件中读取JSON并转换为POJO示例:

        // 文件读取JSON -> POJO
        User readUserByFile = objectMapper.readValue(file, User.class);
        System.out.println(readUserByFile);

从字符串中读取JSON并转换为POJO(反序列化)示例:

        String jsonStr="{\"id\":1767798780627279873,\"name\":\"坤坤\",\"age\":18,\"org\":null,\"roleList\":null}";
        User userPojo = objectMapper.readValue(jsonStr, User.class);
        System.out.println(userPojo);

此外ObjectMapper实现了很多将JSON读取为树模型的方法:

image.png

树模型使用树状结构来呈现对象,例如用户对象的树模型示意图:

image.png

JSON不太好转换为某个标准的对象时,可以直接转换为统一的树模型,示例如下:

        String userJsonStr = "{\"id\":1767798780627279873,\"name\":\"坤坤\",\"age\":18,\"org\":null,\"roleList\":[{\"id\":null,\"roleName\":\"管理员\",\"roleCode\":\"ROLE_ADMIN\"},{\"id\":null,\"roleName\":\"普通用户\",\"roleCode\":\"ROLE_USER\"}]}";
        JsonNode jsonNode = objectMapper.readTree(userJsonStr);
        long userId = jsonNode.get("id").asLong();// 获取id 节点对应的值并转为 Long
        String userName = jsonNode.get("name").asText();// 获取 name 节点对应的值并转为 String
        String roleName = jsonNode.get("roleList").get(0).get("roleName").asText(); //  获取 roleList 节点对应的值,再获取第一个元素,再获取roleName 的值并转为 String

3. 泛型擦除

相信大家对JAVA中的泛型已经十分了解,其本质是参数化类型,在后台返回给前端数据时,一般都会使用一个统一的响应结果封装类,并使用泛型标识返回数据的类型:

public class R<T> {

    /**
     * 状态码
     */
    private Integer code;

    /**
     * 返回信息
     */
    private String msg;

    /**
     * 数据
     */
    private T data;

    public static <T> R<T> response(Integer code, String msg, T data) {
        R<T> result = new R<>();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }
    // 省略 getter/setter......
}    

调用示例:

R<User> response = R.response(200, "操作成功", user);

但是在反序列化(将JSON转为POJO)并获取泛型指定的对象时,示例代码:

        String jsonStr = "{\"code\":200,\"msg\":\"操作成功\",\"data\":{\"id\":1767798780627279873,\"name\":\"坤坤\",\"age\":18,\"org\":null,\"roleList\":[{\"id\":null,\"roleName\":\"管理员\",\"roleCode\":\"ROLE_ADMIN\"},{\"id\":null,\"roleName\":\"普通用户\",\"roleCode\":\"ROLE_USER\"}]}}";
        R<User> response= objectMapper.readValue(jsonStr, R.class);
        User data = response.getData();

这时会引发ClassCastException类型转换异常:

Exception in thread "main" java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.pearl.jacksoncore.demo.pojo.User (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; com.pearl.jacksoncore.demo.pojo.User is in unnamed module of loader 'app')
	at com.pearl.jacksoncore.demo.databind.ObjectMapperTest.main(ObjectMapperTest.java:89)

通过Debug可以看到,虽然我们指定了接收对象的泛型为User,但是实际的data却是LinkedHashMap类型:

image.png

这是由于JAVA中的泛型擦除机制导致的,为了兼容老版本的Java和为了性能考虑,在编译期会进行类型擦除,在运行期间JVM并不知道泛型的存在,在对JSON字符串进行解析时,JVM自然也不知道需要将其解析为哪种类型,则默认解析为LinkedHashMap,所以导致ClassCastException类型转换异常。

针对JAVA泛型擦除问题,Jackson提供了TypeReference<T>抽象类指定转换时的泛型类型,这样在反序列化时,也就知道是什么类型了,使用示例如下:

R<User> response = objectMapper.readValue(jsonStr, new TypeReference<R<User>>() { });