一、基本介绍
Jackson
是当前用的比较广泛的,用来序列化和反序列化 Json
的 Java
的开源框架,使用前我们先导入其相应的 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. 复杂转化
在 Jackson
中 readValue()
与 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
JsonNode
是 Jackson
内置提供的一种 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
类似于 Map
在 JsonNode
的基础上提供了 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);
}
}