Jackson工具使用教程


一、基本介绍

Jackson 是当前用的比较广泛的,用来序列化和反序列化 JsonJava 的开源框架,使用前我们先导入其相应的 maven 依赖。

Spring Boot 中默认使用的对象序列化工具即为 Jackson,因此在 Spring 工程中使用的话无需再次导入依赖,直接通过 @Autowire 注解即可。

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

1. 定义声明

Jackson 定义十分简单,可以选择通过直接 new 构建对象,如果在 Spring Boot 工程中则可通过 @Autowire 注解注入即可,因为默认 Spring 中已经依赖了 Jackson 工具。

/**
 * Spring 工程中声明
 */
@Autowire
private ObjectMapper objectMapper;

/**
 * 普通 Maven 工程声明
 */
public void demo() {
    ObjectMapper objectMapper = new ObjectMapper();
    // 下述配置可选,非必须
    // 在反序列化时忽略在 Json 中存在但 Java 对象不存在的属性
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
    // 在序列化时日期格式默认为 yyyy-MM-dd'T'HH:mm:ss.SSSZ
    objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    // 在序列化时忽略值为 null 的属性
    objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    // 忽略值为默认值的属性
    objectMapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_DEFAULT);
}

2. 注解介绍

Jackson 提供了丰富的注解可用于定于规则,常用的注解参考下表:

注解 作用
@JsonProperty 作用于字段,用于属性重命名映射。
@JsonIgnore 作用于字段,序列化时忽略该字段。
@JsonFormat 作用于字段,用于定义时间格式化。
@JsonAlias 作用于字段,用于多别名匹配。
@JsonPropertyOrder 作用于类,用于定义属性顺序。
@JsonIgnoreProperties 作用于类,用于批量定义忽略属性。

下面通过示例简单演示下如何使用以及对应的效果:

public class Foo {
    
    /**
     * 为属性取别名并定于次序
     */
    @JsonProperty(value = "name", index = 1)
    private String name;

    /**
     * 读取适配一对多关系
     */
    @JsonAlias({"sex", "gender"})
    private String gender;

    /**
     * 序列化时将忽略该字段
     */
    @JsonIgnore
    private String address;

    /**
     * 序列化时格式属性值
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date birthday;
}

二、序列化

1. 对象转化

Jackson 可以实现 Java 对象与 Json 字符串之间的相互转化。

通过 readValue()writeValueAsString() 方法实现二者之间的转换。

public void jsonTran() throws JsonProcessingException {
    User user = new User("123", "Alex", "male");
    // bean 对象 -> json 字符串
    String result = objectMapper.writeValueAsString(user1);
    System.out.println(result);

    // json 字符串 -> bean 对象
    User user2 = objectMapper.readValue(str, User.class);
    System.out.println(user2);
}

2. 复杂转化

JacksonreadValue()convertValue() 等能实现对象数据的转化,二者的主要区别如下:

  • convertValue() 适用于从已有的 Java 对象间转化。
  • readValue() 适用于外部资源(Json数据等)转为 Java 对象。
(1) List转化

在通过 Jackson 对复杂对象如 List 列表序列化时,默认 List 中对象反序列化的结果是 Map 对象,而通过 TypeReference 即可指定序列化的内容。

public void listDemo() throws JsonProcessingException {
    List<User> userList = new ArrayList<>();
    userList.add(new User("111", "Alex", "male"));
    userList.add(new User("222", "Jack", "male"));
    userList.add(new User("333", "Beth", "female"));
            
    ObjectMapper objectMapper = new ObjectMapper();
    // To json string
    String result = objectMapper.writeValueAsString(userList);
    // Json string -> List
    List<User> userList1 = objectMapper.readValue(sellerStr, new TypeReference<List<User>>() {
    });
    System.out.println(userList1);
}
(2) 对象转化

通过 convertValue()TypeReference 可以实现将 Java 对象转化为 Map 对象。

public void MapDemo() throws JsonProcessingException {
    User user1 = new User("123", "Alex", "male");
    // Bean to Map
    Map<String, String> beanMap = objectMapper.convertValue(user1, new TypeReference<Map<String, String>>() {
    });
    System.out.println("Convert value(To bean): " + beanMap);

    // Map to Bean
    User user2 = objectMapper.convertValue(beanMap, User.class);
    System.out.println("Origin value(To map): " + user2);
}

三、Json对象

1. JsonNode

JsonNodeJackson 内置提供的一种 Json 对象格式, Jackson 提供了一系列接口实现 Java 对象与其之间的转化。

方法 作用
valueToTree() 转化 Java 对象 为 Json 对象。
readTree() 转化 Json 字符串 为 Json 对象。
treeToValue() 转化 Json 对象为 Java 对象。
public void JsNodeDemo() throws JsonProcessingException {
    User user1 = new User("123", "Alex", "male");
    // bean 对象 -> json 对象
    JsonNode node1 = objectMapper.valueToTree(user1);
    // json 字符串 -> json 对象
    String result = objectMapper.writeValueAsString(user1);
    JsonNode node2 = objectMapper.readTree(result);

    // json 对象 -> bean 对象
    User user2 = objectMapper.treeToValue(node1, User.class);

    String name = node1.get("name");
    System.out.println(name);
}

2. ObjectNode

ObjectNode 继承于 JsonNode,可以理解为 Jackson 中的 Map 对象。

因此 ObjectNode 类似于 MapJsonNode 的基础上提供了 put()get() 操作,实例代码如下:

public void objNodeDemo() {
    ObjectNode objectNode = objectMapper.createObjectNode();
    objectNode.put("k-1", "v-1");
    objectNode.put("k-2", "v-2");
    System.out.println("JsonNode: " + objectNode);

    boolean empty = objectNode.isEmpty();
    if (empty) {
        System.err.println("JsonNode is empty.");
    } else {
        System.out.println("Node type: " + objectNode.getNodeType());

        JsonNode node1 = objectNode.get("k-1");
        JsonNode node2 = objectNode.findValue("k-2");
        System.out.println("get(): " + node1);
        System.out.println("findValue(): " + node2);
    }
}

3. ArrayNode

ArrayNode 同样继承于 JsonNode,可以理解为 Jackson 中的 List 对象。

同样的 ArrayNode 扩展了 JsonNode 类并提供了 add()get() 操作,示例代码如下:

public void arrayNodeDemo() {
    ArrayNode arrayNode = objectMapper.createArrayNode();
    arrayNode.add("k-3");
    arrayNode.add("k-4");
    System.out.println("Node type: " + arrayNode.getNodeType());
    
    JsonNode node = arrayNode.get(1);
    System.out.println("get(): " + node);
}

四、文件读写

1. 数据查询

在查询数据时,可通过 readTree() 读取 json 数据文件,但需要注意它读取的是完整的文件数据内容。而当作为接口服务时,通常需要配置数据过滤分页,因此此处利用 limit 限制查看的条数,为 -1 时则无限制。

其中 condition 对象为查询条件,其 key 为条件属性名,value 为属性对应值,如传入 {id: 1} 则查看文件数据中 id 值为 1 的数据对象。

public List<JsonNode> query(Integer limit, String path, Map<String, Object> condition) {
    List<JsonNode> nodeList = new ArrayList<>();
    try {
        File dbFile = new File(path);
        if (!dbFile.exists() && !dbFile.isFile()) {
            LOGGER.warn("File data {} not existed", path);
            return new ArrayList<>();
        }

        int index = 0;
        JsonNode dbNode = mapper.readTree(dbFile);
        for (JsonNode node : dbNode) {
            if (marchNode(node, condition)) {
                if (limit != -1 && index++ >= limit) {
                    break;
                }
                nodeList.add(node);
            }
        }
        return nodeList;
    } catch (Exception e) {
        LOGGER.error("list data error", e);
        throw new RuntimeException(e);
    }
}

private Boolean marchNode(JsonNode node, Map<String, Object> condition) {
    boolean march = true;
    for (Map.Entry<String, Object> item : condition.entrySet()) {
        // March the node
        String key = item.getKey();
        Object value = item.getValue();
        String target = node.get(key).asText();
        if (!Objects.equals(value, target)) {
            march = false;
            break;
        }
    }
    return march;
}

2. 手动插入

当向 Json 文件插入输入时最简单的方法是通过 readTree() 读取后更新对象再通过 writeValue() 回写,但存在一个问题即当文件过大时将整个数据读取对内存耗费较大,同时大文件回写也会耗费一定性能。

因此对于 Json 文件的插入这里选择了通过手动 IO 方式插入,其实现逻辑如下:

  • 先将文件数组符号的 ] 替换为 ,
  • 写入需插入的对象;
  • 手动再补充一个 ] 使之符合 Json 格式;

对应步骤的数据内容如下:

// 原数据
[{
    "id": 1
}]

// 步骤一, 替换 ]
[{
    "id": 1
},

// 步骤二,写入数据
[{
    "id": 1
},{
    "id": 2
}

// 步骤三,回填 ]
[{
    "id": 1
},{
    "id": 2
}]

基于上述的步骤流程,其对应的代码实现如下。需要注意一点的是,在 seek() 定位数组符号 ] 时文件末端可能存在空格或者换行符,因此需要循环倒查直至查询到 ] 符号,此时再执行替换与写入。

public <T> Boolean saveToJsonArray(T t, String path) {
    boolean success = true;
    try (RandomAccessFile raf = new RandomAccessFile(location, "rw")) {
        // change cursor to last
        long cursor = raf.length() - 1;
        raf.seek(cursor);
        // find cursor of symbol ']'
        while (cursor >= 0) {
            raf.seek(cursor);
            if (raf.readByte() == ']') {
                break;
            }
            cursor--;
        }
        raf.seek(cursor);
        // write content
        raf.write(",".getBytes());
        raf.write(mapper.writeValueAsBytes(t));
        raf.write("]".getBytes());
    } catch (IOException e) {
        success = false;
        LOGGER.error("Save data error", e);
    }
    return success;
}

3. 数据删除

删除文件中 Json 数组的指定对象原理与查询类似,同样通过 Map 作为条件先进行匹配过滤到对应数据的下标通过 remove() 删除,再将变更后的数据通过 writeValue() 回写至原文件。

需要注意的是遍历 readTree() 读取的数组对象时需要从后往前,如若从前向后遍历当执行 remove() 时将会导致后续的元素前移导致数据的遗落。

public Boolean deleteByMap(String path, Map<String, Object> condition) {
    boolean success = true;
    try {
        boolean changed = false;
        File dbFile = Paths.get(path).toFile();
        ArrayNode dbNodeTree = (ArrayNode) mapper.readTree(dbFile);
        // From back to front in case index change cost element miss
        for (int i = dbNodeTree.size() - 1; i >= 0; i--) {
            JsonNode node = dbNodeTree.get(i);
            if (marchNode(node, condition)) {
                changed = true;
                dbNodeTree.remove(i);
            }
        }
        if (changed) {
            mapper.writeValue(dbFile, dbNodeTree);
        }
    } catch (Exception e) {
        success = false;
        LOGGER.error("Delete data error", e);
    }
    return success;
}

4. 数据更新

对于 Json 文件的数据更新并没有直接的方式,可以选择先通过 readTree() 读取进行修改后再使用 writeValue() 进行回写更新,也可以进行全删全入,即先删除匹配的数据再执行插入操作,步骤和上述类似这里就不再详细介绍。

五、文件写入

1. 覆盖写入

Jackson 提供了 writeValue(File, JsonNode) 用于写入数据至文件,但其会覆盖原文件内容。

/**
 * Before: {"id": 0}
 * After:  {"id": 1}
 */
@Test
public void replaceObj() {
    try {
        JsonNode node = mapper.convertValue(Map.of("id", 1), JsonNode.class);
        // writeValue(): replace file content with new value
        mapper.writeValue(file, node);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

2. 数据拼接

通过 JsonGenerator 进行数据写入时则可以将单个 Json 对象拼接到当前文件的尾部。

/**
 * Before: [{"id": 0}]
 * After:  [{"id": 0}] {"id": 1}
 */
@Test
public void appendObj() {
    try {
        JsonFactory jsonFactory = new JsonFactory();
        try (
            FileWriter fileWriter = new FileWriter(location, true);
            JsonGenerator jsonGenerator = jsonFactory.createGenerator(fileWriter)
        ) {
            // append object to file
            jsonGenerator.writeStartObject();
            jsonGenerator.writeNumberField("id", 1);
            jsonGenerator.writeEndObject();
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

3. 数组写入

通过 SequenceWriter 即可实现与 JsonGenerator 类似效果,但其是将 Json 数组拼接到当前文件的尾部。

/**
 * Before: [{"id": 0}]
 * After:  [{"id": 0}] [{"id": 1}]
 */
@Test
public void appendArray() {
    try (
            FileWriter fileWriter = new FileWriter(file, true);
            SequenceWriter seqWriter = mapper.writer().writeValuesAsArray(fileWriter);
    ) {
        // append an object array to json file
        seqWriter.write(Map.of("id", 1));
        // when "seqWriter.close()" then put "]"
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录