写在前面
虽然如今市面上有很多良好的json分析库,但 Spring默认采用Jackson分析Json。
本文将通过一系列通俗易懂的代码示例,带你逐步把握 Jackson 的基础用法、进阶技巧以及在实际项目中的应用场景。
一、Jackjson简介
Jackson 是当前用的比较广泛的,用来序列化和反序列化 json 的 Java 的开源框架。
什么是序列化和反序列化呢?
- 序列化:将 Java Bean 转换为JSON 字符串
- 反序列化:将JSON字符串转换为JavaBeen对象
GitHub地址:https://github.com/FasterXML/jackson
从GitHub 看到,目前有8.8k stars,最近更新时间是2个月前。可见其更新速度照旧比较活跃的
也是json最流行的分析器之一
二、Jackjson优点
- Jackson 所依赖的 jar 包较少,简单易用
- 其他 Java 的 json 的框架 Gson 等相比, Jackson 分析大的 json 文件速度比较快
- Jackson 运行时占用内存比较低,性能比较好
- Jackson 有灵活的 API,可以很轻易进行扩展和定制
三、Jackjson模块及依赖
① Jackson库包括三个重要的模块
- jackson-databind用于数据绑定
- jackson-core用于JSON分析和天生
- jackson-annotations用于注解支持
② 所需依赖
jackson-databind 依赖 jackson-core 和 jackson-annotations,所以可以只表现地添加jackson-databind依赖,jackson-core 和 jackson-annotations 也随之添加到 Java 项目工程中
maven- <dependency>
- <groupId>com.fasterxml.jackson.core</groupId>
- <artifactId>jackson-core</artifactId>
- <version>2.15.3</version>
- </dependency>
- <dependency>
- <groupId>com.fasterxml.jackson.core</groupId>
- <artifactId>jackson-annotations</artifactId>
- <version>2.15.2</version>
- </dependency>
- <dependency>
- <groupId>com.fasterxml.jackson.core</groupId>
- <artifactId>jackson-databind</artifactId>
- <version>2.15.2</version>
- </dependency>
复制代码 Gradle- dependencies {
- implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4'
- }
复制代码 四、快速上手
假设我们有一个简单的 User 类:- @Data
- @ToString
- @AllArgsConstructor
- @NoArgsConstructor
- public class User {
- private String name;
- private int age;
- }
复制代码 4.1 Java对象转JSON字符串
- @Test
- public void testSerializationExample() throws JsonProcessingException {
- User user = new User("小凡", 18);
- ObjectMapper objectMapper = new ObjectMapper();
- String userstr = objectMapper.writeValueAsString(user);
- System.out.println(userstr);
- }
- //输出
- {"name":"小凡","age":18}
复制代码 4.2 JSON字符串转Java对象
- @Test
- public void testDeserializationExample() throws JsonProcessingException {
- String json ="{"name":"小凡","age":18}";
- ObjectMapper objectMapper = new ObjectMapper();
- User user = objectMapper.readValue(json, User.class);
- System.out.println(user);
- }
- //输出
- User(name=小凡, age=18)
复制代码 上面例子中我们我们使用了ObjectMapper 它是 Jackson 库的焦点类,负责实现 Java 对象与 JSON 文本之间的相互转换
五、Jackjson序列化API
5.1 普通Java对象序列化
- @Test
- public void testObjectToJson() throws JsonProcessingException {
- User user = new User();
- user.setName("小凡");
- user.setAge(18);
- ObjectMapper mapper = new ObjectMapper();
- String userstr = mapper.writeValueAsString(user);
- System.out.println(userstr);
- }
- //输出
- {"name":"小凡","age":18}
复制代码 5.2 复杂对象序列化
① 构造作者出版信息对象- //书籍对象
- @Data
- public class Book {
- private String bookName;
- private String publishDate;
- private String publishHouse;
- private Double price;
- }
- //地址对象
- @Data
- public class Address {
- private String city;
- private String street;
- }
- //作者出版信息对象
- @Data
- public class PublishInfo {
- private String name;
- private String sex;
- private Integer age;
- private Address addr;
- private List<Book> books;
- }
复制代码 ② 复杂对象转JSON字符串- @Test
- public void testComplexObjectToJson() throws JsonProcessingException {
- //构造所有出版的书籍list
- ArrayList<Book> books = new ArrayList<Book>();
- Book book1 = new Book();
- book1.setBookName("Java从入门到放弃");
- book1.setPublishDate("2004-01-01");
- book1.setPublishHouse("小凡出版社");
- book1.setPrice(66.66);
- Book book2 = new Book();
- book2.setBookName("Spring从入门到入土");
- book2.setPublishDate("2024-01-01");
- book2.setPublishHouse("小凡出版社");
- book2.setPrice(88.88);
- books.add(book1);
- books.add(book2);
- //构造作者地址信息
- Address addr = new Address();
- addr.setCity("昆明");
- addr.setStreet("xxx区xxx路xxx号");
- //构造作者出版的所有书籍信息
- PublishInfo publishInfo = new PublishInfo();
- publishInfo.setName("小凡");
- publishInfo.setSex("男");
- publishInfo.setAge(18);
- publishInfo.setAddr(addr);
- publishInfo.setBooks(books);
- ObjectMapper mapper = new ObjectMapper();
- String json = mapper.writeValueAsString(publishInfo);
- System.out.println(json);
- }
- //返回
- {"name":"小凡","sex":"男","age":18,"addr":{"city":"昆明","street":"xxx区xxx路xxx号"},"books":[{"bookName":"Java从入门到放弃","publishDate":"2004-01-01","publishHouse":"小凡出版社","price":66.66},{"bookName":"Spring从入门到入土","publishDate":"2024-01-01","publishHouse":"小凡出版社","price":88.88}]}
复制代码 5.3 List集合序列化
- @Test
- public void testListToJson() throws JsonProcessingException {
- User user1 = new User();
- user1.setName("小凡001");
- user1.setAge(18);
- User user2 = new User();
- user2.setName("小凡002");
- user2.setAge(30);
- ArrayList<User> users = new ArrayList<>();
- users.add(user1);
- users.add(user2);
- ObjectMapper mapper = new ObjectMapper();
- String userstr = mapper.writeValueAsString(users);
- System.out.println(userstr);
- }
- //输出
- [{"name":"小凡001","age":18},{"name":"小凡002","age":30}]
复制代码 5.4 Map集合序列化
①- @Test
- public void testMapToJson() throws JsonProcessingException {
- User user = new User();
- user.setName("小凡");
- user.setAge(18);
- List<String> asList = Arrays.asList("抽烟", "喝酒", "烫头发");
- HashMap<String, Object> map = new HashMap<>();
- map.put("user", user);
- map.put("hobby",asList);
- ObjectMapper mapper = new ObjectMapper();
- String json = mapper.writeValueAsString(map);
- System.out.println(json);
- }
- //输出
- {"user":{"name":"小凡","age":18},"hobby":["抽烟","喝酒","烫头发"]}
复制代码 ②- @Test
- public void testMapToJsonSup() throws JsonProcessingException {
- User user1 = new User();
- user1.setName("小凡001");
- user1.setAge(18);
- User user2 = new User();
- user2.setName("小凡002");
- user2.setAge(30);
- ArrayList<User> users = new ArrayList<>();
- users.add(user1);
- users.add(user2);
- List<String> asList = Arrays.asList("抽烟", "喝酒", "烫头发");
- HashMap<String, Object> map = new HashMap<>();
- map.put("users", users);
- map.put("hobby",asList);
- map.put("name","张三");
- ObjectMapper mapper = new ObjectMapper();
- String json = mapper.writeValueAsString(map);
- System.out.println(json);
- }
- //输出
- {"name":"张三","users":[{"name":"小凡001","age":18},{"name":"小凡002","age":30}],"hobby":["抽烟","喝酒","烫头发"]}
复制代码 5.5 日期处理
默认情况下,jackjson会将日期类型属性序列化成long型值(自1970年1月1日以来的毫秒数)。显然这样格式的数据不符合人类直观查看
假设我们有个Person对象- @Data
- public class Person {
- private String name;
- private Date birthday;
- }
复制代码 ① 我们先来看看默认转换的结果- @Test
- public void testDateToJsonDefault() throws JsonProcessingException {
- Person person = new Person();
- person.setName("小凡");
- person.setBirthday(new Date());
- ObjectMapper mapper = new ObjectMapper();
- String json = mapper.writeValueAsString(person);
- System.out.println(json);
- }
- //输出
- {"name":"小凡","birthday":1712220896407}
复制代码 ② 通过SimpleDateFormat 将日期格式化成人类可看格式表现- @Test
- public void testDateToJson() throws JsonProcessingException {
- Person person = new Person();
- person.setName("小凡");
- person.setBirthday(new Date());
- ObjectMapper mapper = new ObjectMapper();
- //进行一下日期转换
- SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
- mapper.setDateFormat(dateFormat);
- String json = mapper.writeValueAsString(person);
- System.out.println(json);
- }
- //输出 这个格式就人性化多了
- {"name":"小凡","birthday":"2024-04-04"}
复制代码 六、Jackjson反序列化API
Jackson通过将JSON字段的名称与Java对象中的getter和setter方法进行匹配,将JSON对象的字段映射到Java对象中的属性。
Jackson删除了getter和setter方法名称的“ get”和“ set”部门,并将其余名称的第一个字符转换为小写。
6.1 普通JSON字符串反序列化
6.1.1 JSON字符串->Java对象
注: 这里我们照旧使用前面小节中创建的User 实体类- @Test
- public void testStrToObject() throws JsonProcessingException {
- String json ="{"name":"小凡","age":18}";
- ObjectMapper mapper = new ObjectMapper();
- User user = mapper.readValue(json, User.class);
- System.out.println(user);
- }
- //输出
- User(name=小凡, age=18)
复制代码 6.1.2 字符输入流-->Java对象
- @Test
- public void testReaderToObject() throws JsonProcessingException {
- String json ="{"name":"小医仙","age":18}";
- Reader reader = new StringReader(json);
- ObjectMapper mapper = new ObjectMapper();
- User user = mapper.readValue(json, User.class);
- System.out.println(user);
- }
- //输出
- User(name=小医仙, age=18)
复制代码 6.1.3 字节输入流->Java对象
① 创建user001.json文件,文件内容如下
②将字节输入流转换为User对象- @Test
- public void testInputStreamToObject() throws IOException {
- FileInputStream inputStream = new FileInputStream("F:\\vueworkspace\\jackjson-demo\\jackjson-demo\\src\\json\\user001.json");
- ObjectMapper mapper = new ObjectMapper();
- User user = mapper.readValue(inputStream, User.class);
- System.out.println(user);
- }
- }
- //输出
- User(name=萧炎, age=18)
复制代码 6.1.4 JSON 文件反->Java对象
①我们准备一个user.json文件,内容如下
② 读取user.json文件中内容,并转换成User 对象- @Test
- public void testJsonfileToObject() throws IOException {
- File file = new File("F:\\vueworkspace\\jackjson-demo\\jackjson-demo\\src\\json\\user.json");
- ObjectMapper mapper = new ObjectMapper();
- User user = mapper.readValue(file, User.class);
- System.out.println(user);
- }
- //输出
- User(name=萧炎, age=18)
复制代码 6.1.5 URL文件->Java对象
① 我们在网络上放一个user.json资源文件,内容如下② 通过URL(java.net.URL) 将JSON转换成User对象- @Test
- public void testUrlToObject() throws IOException {
- String url ="https://files.cnblogs.com/files/blogs/685650/user.json";
- URL url1 = new URL(url);
- ObjectMapper mapper = new ObjectMapper();
- User user = mapper.readValue(url1, User.class);
- System.out.println(user);
- }
- //输出
- User(name=紫妍, age=18)
复制代码 6.1.6 字节数组-> java对象
- @Test
- public void testByteToObject() throws IOException {
- String json ="{"name":"韩雪","age":18}";
- byte[] bytes = json.getBytes();
- ObjectMapper mapper = new ObjectMapper();
- User user = mapper.readValue(bytes, User.class);
- System.out.println(user);
- }
- //输出
- User(name=韩雪, age=18)
复制代码 6.2 JSON数组字符串 反序列化
6.2.1 JSON字符串->Map集合
- @Test
- public void testJsonStrToMap() throws JsonProcessingException {
- //{"name":"小凡","sex":"男","age":18}
- String json ="{"name":"小凡","sex":"男","age":18}";
- ObjectMapper mapper = new ObjectMapper();
- Map<String, Object> map = mapper.readValue(json, new TypeReference<Map<String, Object>>() {});
- System.out.println(map);
- }
- //输出
- {name=小凡, sex=男, age=18}
复制代码 6.2.2 JSON数组字符串->List集合
- @Test
- public void testJsonArrToList() throws JsonProcessingException {
- //[{"name":"小凡001","age":18},{"name":"小凡002","age":30}]
- String json ="[{"name":"小凡001","age":18},{"name":"小凡002","age":30}]";
- ObjectMapper mapper = new ObjectMapper();
- List<User> users = mapper.readValue(json, new TypeReference<List<User>>() {
- });
- System.out.println(users);
- }
- //输出
- [User(name=小凡001, age=18), User(name=小凡002, age=30)]
复制代码 6.2.3 JSON数组字符串->Java对象数据
- @Test
- public void testJsonArrToObjArr() throws JsonProcessingException {
- //[{"name":"小凡001","age":18},{"name":"小凡002","age":30}]
- String json ="[{"name":"小凡001","age":18},{"name":"小凡002","age":30}]";
- ObjectMapper mapper = new ObjectMapper();
- User[] users = mapper.readValue(json, User[].class);
- for (User user : users) {
- System.out.println(user);
- }
- }
- //输出
- User(name=小凡001, age=18)
- User(name=小凡002, age=30)
复制代码 七、自界说序列化反序列化
通过自界说序列化和反序列化可以使其更加灵活多变
7.1 自界说序列化
有时候,我们不需要jackjson默认的序列化方式。例如,JSON中使用与Java对象中差异的属性名称,
或者不需要Java对象中的某个字段,这时我们就需要自界说序列化器
我们先来看看User实体对象的界说如下- @Data
- public class User {
- private String name;
- private Integer age;
- }
复制代码 按照下面默认序列化代码,我们末了得到的JSON字符串如下- @Test
- public void testSerializationExample() throws JsonProcessingException {
- User user = new User("小凡", 18);
- ObjectMapper objectMapper = new ObjectMapper();
- String userstr = objectMapper.writeValueAsString(user);
- System.out.println(userstr);
- }
- //输出
- {"name":"小凡","age":18}
复制代码 而如今的需求升级了,再不创建或修改Users实体对象的情况下,我们想要得到{"username":"小凡","userage":18} 这样的字符串,
应该怎么办呢?
这时,我们就需要自界说序列化器,就可以轻松实现,具体代码如下- @Test
- public void testDefineSerialize() throws JsonProcessingException {
- SimpleModule version1Module = new SimpleModule();
- version1Module.addSerializer(User.class, new JsonSerializer<User>() {
- @Override
- public void serialize(User user, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
- jsonGenerator.writeStartObject();
- jsonGenerator.writeStringField("username", user.getName());
- jsonGenerator.writeNumberField("userage", user.getAge());
- jsonGenerator.writeEndObject();
- }
- });
- ObjectMapper objectMapper = new ObjectMapper();
- objectMapper.registerModule(version1Module);
- User user = new User("小凡", 18);
- String json = objectMapper.writeValueAsString(user); // 使用版本1的序列化器
- System.out.println( json);
- }
- //输出
- {"username":"小凡","userage":18}
复制代码 7.2 自界说反序列化器
同理,反序列化也可以自界说,具体代码如下
① 自界说一个反序列化器- public class UserDeserializer extends StdDeserializer<User> {
- public UserDeserializer() {
- this(null);
- }
- public UserDeserializer(Class<?> vc) {
- super(vc);
- }
- @Override
- public User deserialize(JsonParser jp, DeserializationContext ctxt)
- throws IOException {
- JsonNode node = jp.getCodec().readTree(jp);
- String name = node.get("name").asText();
- int age = (Integer) ((IntNode) node.get("age")).numberValue();
- return new User(name, age);
- }
- }
复制代码 ②将JSON字符串反序列化为 User 对象- @Test
- public void testUserDeserializer() throws JsonProcessingException {
- ObjectMapper objectMapper = new ObjectMapper();
- SimpleModule module = new SimpleModule();
- module.addDeserializer(User.class, new UserDeserializer());
- objectMapper.registerModule(module);
- String json = "{"name":"小凡","age":30}";
- User user = objectMapper.readValue(json, User.class);
- System.out.println(user);
- }
- //输出
- User(name=小凡, age=30)
复制代码 八、树模型
Jackson具有内置的树模型,可用于表示JSON对象。Jackson树模型由JsonNode类表示。
在处理JSON时,我们有时并不直接关心或无法直接映射到特定的Java对象,而是需要对JSON内容进行动态、灵活的操作。
这时,树模型就派上了用场。我们可以遍历、查询、更新或合并JSON数据,而无需预先界说对应的Java类
8.1 JsonNode类型概览
JsonNode家族包括以下重要子类型:
- ObjectNode:对应JSON对象,包含一组键值对,可以通过.put(key, value)添加或更新属性。
- ArrayNode:对应JSON数组,包含一系列元素,可通过.add(value)插入新元素。
- TextNode、IntNode、LongNode、DoubleNode等:分别对应JSON中的字符串、整数、长整数、浮点数等基本类型值。
- BooleanNode、NullNode:分别对应JSON中的布尔值和null值。
8.2 创建与操作JsonNode实例
下面通过一些具体的代码示例,展示如何使用JackJson的树模型API来创建、操作JsonNode对象。
示例1:创建简单JsonNode- @Test
- public void testJackJsonTreeModelExample(){
- // 创建 ObjectMapper 实例
- ObjectMapper mapper = new ObjectMapper();
- // 创建并初始化各类型 JsonNode 实例
- ObjectNode personNode = mapper.createObjectNode();
- personNode.put("name", "小凡");
- personNode.put("age", 18);
- ArrayNode hobbiesNode = mapper.createArrayNode();
- hobbiesNode.add("写代码").add("看书").add("打豆豆");
- personNode.set("hobbies", hobbiesNode);
- // 输出构建的 JSON 字符串
- System.out.println(personNode.toString());
- }
- //输出
- {"name":"小凡","age":18,"hobbies":["写代码","看书","打豆豆"]}
复制代码 上述代码起首创建了一个ObjectMapper实例,它是JackJson的焦点工具类,负责序列化和反序列化工作。接着,我们创建了一个ObjectNode代表JSON对象,设置了"name"和"age"两个属性。然后创建了一个ArrayNode存储爱好列表,并将其添加到personNode中。末了,打印输出构建的JSON字符串。
示例2:从JSON字符串反序列化为JsonNode- @Test
- public void testJackJsonTreeModelExample2() throws JsonProcessingException {
- String jsonInput = "{"name":"小凡","age":18,"hobbies":["写代码","看书","打豆豆"]}";
- ObjectMapper mapper = new ObjectMapper();
- JsonNode rootNode = mapper.readTree(jsonInput);
- System.out.println(rootNode.get("name").asText());
- System.out.println(rootNode.get("age").asInt());
- System.out.println(rootNode.get("hobbies").get(0).asText());
- System.out.println(rootNode.get("hobbies").get(1).asText());
- System.out.println(rootNode.get("hobbies").get(2).asText());
- }
- //输出
- 小凡
- 18
- 写代码
- 看书
- 打豆豆
复制代码 在此示例中,我们起首界说了一个JSON字符串。然后使用ObjectMapper的readTree()方法将其反序列化为JsonNode。接下来,通过调用.get(key)方法访问对象属性或数组元素,并使用.asText()、.asInt()等方法获取其值。
8.3 利用JackJson树模型进行深度查询、修改、合并等操作
上面给出的两个案例属于入门级操作,我们学会后可以接着进行一些高级操作
- @Test
- public void testJsonManipulationExample() throws JsonProcessingException {
- String JSON_STRING = "{"name":"小凡","age":18,"hobbies":["写代码","看书","打豆豆"]}";
- ObjectMapper mapper = new ObjectMapper();
- // 将JSON字符串转换为JsonNode对象
- JsonNode rootNode = mapper.readTree(JSON_STRING);
- // **深度查询**
- // 查询"年龄"
- int age = rootNode.get("age").asInt();
- System.out.println("Age: " + age);
- // 查询第一个兴趣爱好
- String firstHobby = rootNode.get("hobbies").get(0).asText();
- System.out.println("First hobby: " + firstHobby);
- // **修改**
- // 修改年龄为20
- ((ObjectNode) rootNode).put("age", 20);
- // 添加新的兴趣爱好:"旅行"
- ((ArrayNode) rootNode.get("hobbies")).add("旅行");
- // **合并**
- // 假设有一个新的JSON片段要与原数据合并
- String additionalJson = "{"address":"北京市","job":"程序员"}";
- JsonNode additionalNode = mapper.readTree(additionalJson);
- // 使用ObjectNode#setAll方法将新节点中的键值对合并到原始节点中
- ((ObjectNode) rootNode).setAll((ObjectNode) additionalNode);
- // 打印更新后的JSON字符串
- System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootNode));
- }
- //输出
- Age: 18
- First hobby: 写代码
- {
- "name" : "小凡",
- "age" : 20,
- "hobbies" : [ "写代码", "看书", "打豆豆", "旅行" ],
- "address" : "北京市",
- "job" : "程序员"
- }
复制代码 九、Jackson注解使用
9.1 序列化反序列化通用注解
9.1.1 @JsonIgnore
@JsonIgnore 用于在序列化或反序列化过程中忽略某个属性
① 界说实体类Person- public class Person {
- private String name;
- @JsonIgnore // 添加此注解以忽略 password 字段
- private String password;
- // 构造器、getter/setter 方法等...
- }
复制代码 ②使用示例- @Test
- public void testJsonIgnore() throws Exception{
- Person person = new Person("小凡", "123456");
- ObjectMapper mapper = new ObjectMapper();
- String json = mapper.writeValueAsString(person);
- }
- //输出
- {
- "name": "小凡"
- }
复制代码 ③ 具体表明
- @JsonIgnore 注解直接放在属性声明前,如 private String password; 行上方。
- 当 Person 类的对象被序列化为 JSON 时,password 属性将不会出如今天生的 JSON 字符串中
- 同样地,在反序列化过程中,如果 JSON 数据中包含 password 字段,Jackson 在分析时会自动忽略它,不会实行将其值设置到对应的 Person 对象属性上。
9.1.2 @JsonIgnoreProperties
与@JsonIgnore 注解相比,@JsonIgnoreProperties 用于批量指定在序列化或反序列化过程中应忽略的属性列表,特别适用于应对不明确或动态变革的输入 JSON 中可能存在但不应处理的额外字段
① 界说实体类Person
希望同时忽略 password 和 socialSecurityNumber 两个敏感属性
- @JsonIgnoreProperties({"password", "socialSecurityNumber"})
- public class Person {
- private String name;
- private String password;
- private String socialSecurityNumber;
- // 构造器、getter/setter 方法等...
- }
复制代码 ② 使用示例- @Test
- public void testJsonIgnoreProperties() throws Exception{
- Person person = new Person("小凡", "123456","233535");
- ObjectMapper mapper = new ObjectMapper();
- String json = mapper.writeValueAsString(person);
- }
- //输出
- {
- "name": "小凡"
- }
复制代码 ③ 具体表明
- @JsonIgnoreProperties 注解放置在类界说的开始处,作为类级别的注解。
- 注解内通过一个字符串数组参数列举出要忽略的属性名。在这个例子中,{"password", "socialSecurityNumber"} 指定了 password 和 socialSecurityNumber 两个属性在序列化和反序列化时应被忽略。
- 序列化 Person 对象时,天生的 JSON 字符串将不包含 password 和 socialSecurityNumber 属性。与前面 @JsonIgnore 示例类似,JSON 输出仅包含未被忽略的 name 属性:
- 反序列化时,纵然输入的 JSON 数据中包含了 password 或 socialSecurityNumber 字段,Jackson 也会忽略它们,不会实行将这些字段的值填充到相应的 Person 对象属性中。
④ @JsonIgnoreProperties 还有一个额外功能
@JsonIgnoreProperties有个可选的属性 ignoreUnknown,用于控制是否忽略 JSON 中存在但 Java 类中没有对应的未知属性。若设置为 true,则在反序列化时碰到未知属性时会自动忽略,制止抛出异常- @JsonIgnoreProperties(ignoreUnknown = true)
- public class Person {
- // ...
- }
复制代码 9.1.3 @JsonIgnoreType
@JsonIgnoreType 注解在 Jackson 库中用于指示整个类在序列化或反序列化过程中应当被忽略。这适用于那些由于某种原因(如敏感信息、内部细节等)不需要或不应该被 JSON 化处理的类
①界说CreditCardDetails 实体类- // 标记该类在序列化和反序列化时应被忽略
- @JsonIgnoreType
- public class CreditCardDetails {
- private String cardNumber;
- private String cvv;
- private String expirationDate;
- // 构造器、getter/setter 方法等...
- }
复制代码 ②界说Customer 类
Customer 类,它有一个 creditCardDetails 属性引用上述 CreditCardDetails 类型的对象:
- public class Customer {
- private String customerId;
- private String name;
- private CreditCardDetails creditCardDetails;
- // 构造器、getter/setter 方法等...
- }
复制代码 ③ 使用示例- public void testJsonIgnoreType() throws JsonProcessingException {
- Customer customer = new Customer("CUST001", "John Doe", new CreditCardDetails("1234567890123456", "123", "2024-12"));
- ObjectMapper mapper = new ObjectMapper();
- String json = mapper.writeValueAsString(customer);
- }
- //输出
- {
- "customerId": "CUST001",
- "name": "John Doe"
- }
复制代码 ④ 具体表明
- @JsonIgnoreType 注解放置在需要忽略的类界说前,作为类级别的注解。
- 当 Customer 类对象被序列化为 JSON 时,所有 CreditCardDetails 类型的属性(如 customer.creditCardDetails)都会被完全忽略,不会出如今天生的 JSON 字符串中。例如:
在此例中,天生的 json 字符串将不含 creditCardDetails 部门,仅包含 customerId 和 name:- {
- "customerId": "CUST001",
- "name": "John Doe"
- }
复制代码 同理,在反序列化过程中,如果 JSON 数据中包含 CreditCardDetails 类型的嵌套结构,Jackson 分析时会自动忽略这部门内容,不会实行创建或填充对应的 CreditCardDetails 对象
9.1.4 @JsonAutoDetect
@JsonAutoDetect 用于自界说类的属性(包括字段和 getter/setter 方法)在序列化和反序列化过程中的可见性规则。默认情况下,Jackson 只使用 public 的字段和 public 的 getter/setter 方法。通过使用 @JsonAutoDetect,你可以根据需要调整这些规则,以适应差异类的设计和数据模型。
① 界说实体类 Employee- @JsonAutoDetect(
- fieldVisibility = JsonAutoDetect.Visibility.NONE, // 不使用字段
- getterVisibility = JsonAutoDetect.Visibility.PROTECTED_AND_PUBLIC, // 允许 protected 和 public 的 getter
- setterVisibility = JsonAutoDetect.Visibility.ANY, // 允许任意访问权限的 setter
- isGetterVisibility = JsonAutoDetect.Visibility.DEFAULT, // 默认行为(public)
- creatorVisibility = JsonAutoDetect.Visibility.DEFAULT) // 默认行为(public)
- public class Employee {
- protected String id;
- String department; // package-private 字段
- // 非标准命名的 getter
- public String getIdentification() {
- return id;
- }
- // 非标准命名的 setter
- public void setIdentification(String id) {
- this.id = id;
- }
- //...标准getter setter
- }
复制代码 ②使用示例- @Test
- public void testJsonAutoDetect() throws JsonProcessingException {
- ObjectMapper mapper = new ObjectMapper();
- Employee employee = new Employee();
- employee.setId("E001");
- employee.setDepartment("Sales");
- String json = mapper.writeValueAsString(employee);
- System.out.println(json);
- }
- //输出
- {
- "identification": "E001",
- "department": "Sales"
- }
复制代码 ③ 具体表明
- 在上述 Employee 类的例子中:
- fieldVisibility = JsonAutoDetect.Visibility.NONE 指定不使用任何字段,意味着纵然有 public 字段,也不会被 Jackson 处理。
- getterVisibility = JsonAutoDetect.Visibility.PROTECTED_AND_PUBLIC 允许 Jackson 使用 protected 和 public 的 getter 方法。
- setterVisibility = JsonAutoDetect.Visibility.ANY 指定可以使用恣意访问权限的 setter 方法。
- isGetterVisibility 和 creatorVisibility 使用默认行为,即只处理 public 的 is-getter 方法和构造函数。
- 因为我们将 id 字段的 getter 和 setter 设为了非标准定名(getIdentification() 和 setIdentification()),且指定了 getterVisibility = JsonAutoDetect.Visibility.PROTECTED_AND_PUBLIC,所以 Jackson 能精确识别并处理这两个方法,尽管它们不是标准的 get 前缀定名。
9.1.5 @JsonProperty
用于指定类的属性在序列化和反序列化成 JSON 时所对应的键名
①Book 实体类
假设有一个 Book 类,此中包含 title 和 yearPublished 字段。我们希望在序列化和反序列化时,将 yearPublished 字段以 publishedYear 作为 JSON 键名。可以使用 @JsonProperty 注解进行重定名:- public class Book {
- private String title;
- @JsonProperty("publishedYear") // 重命名 yearPublished 字段为 publishedYear
- private int yearPublished;
- // 构造器、getter/setter 方法等...
- }
复制代码 ②使用示例:
如今创建一个 Book 对象并序列化为 JSON:- public void testJsonProperty() throws Exception {
- Book book = new Book();
- book.setTitle("小凡编程语录");
- book.setYearPublished(1951);
- ObjectMapper mapper = new ObjectMapper();
- String json = mapper.writeValueAsString(book);
- System.out.println(json);
- }
- //输出
- {
- "title": "小凡编程语录",
- "publishedYear": 1951
- }
复制代码 ③具体表明
在本例中,@JsonProperty("publishedYear") 表明 yearPublished 字段在 JSON 中应称为 publishedYear
反序列化时,当碰到 JSON 中的 publishedYear 键时,Jackson 会知道应该将其值赋给 Book 类的 yearPublished 字段。
9.1.6 @JsonFormat
用于指定日期、时间、日期时间以及其他数值类型在序列化和反序列化为 JSON 时的格式
① 创建 Student 类- public class Student {
- // 其他属性...
- // 使用 @JsonFormat 注解的 LocalDate 属性
- @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
- private LocalDate birthday;
- // 构造器、getter、setter 等...
- }
复制代码 ②使用示例
注:Jackson默认不支持Java 8的日期时间类型java.time.LocalDate,如果使用jdk8需要引入如下依赖.- <dependency>
- <groupId>com.fasterxml.jackson.datatype</groupId>
- <artifactId>jackson-datatype-jsr310</artifactId>
- <version>最新版本号</version>
- </dependency>
复制代码 除了引入以来外,还需要确保在使用Jackson进行序列化和反序列化时,注册JavaTimeModule
mapper.registerModule(new JavaTimeModule());- @Test
- public void testJsonFormat() throws JsonProcessingException {
- ObjectMapper mapper = new ObjectMapper(); // 创建 Jackson 的 ObjectMapper 实例
- mapper.registerModule(new JavaTimeModule());
- // 序列化示例
- Student student = new Student();
- student.setName("小凡");
- student.setAge(18);
- student.setBirthday(LocalDate.of(1990,1,1)); // 设置用户的出生日期
- String json = mapper.writeValueAsString(student); // 将用户对象转换为 JSON 字符串
- System.out.println(json); // 输出序列化后的 JSON 数据
- // 反序列化示例
- String inputJson = "{"birthday":"1990-01-01"}";
- Student deserializedUser = mapper.readValue(inputJson, Student.class); // 从 JSON 字符串反序列化为 User 对象
- System.out.println(deserializedUser.getBirthday()); // 输出反序列化后获得的出生日期
- }
- //输出
- {"name":"小凡","age":18,"birthday":"1990-01-01"}
- 1990-01-01
复制代码 ③具体表明
@JsonFormat 注解在 Jackson 中被用来精确控制日期/时间类型的属性在 JSON 序列化和反序列化过程中的格式。通过给定合适的 shape 和 pattern 参数,可以确保日期数据在 Java 类型与 JSON 文本之间准确无误地转换。
9.2 序列化注解
9.2.1 @JsonInclude
用于控制对象在序列化过程中包含哪些属性。它可以防止空或特定值的字段被序列化到JSON输出中,从而帮助您精简JSON数据结构,减少不须要的传输量或制止向客户端暴露不须要的信息
①界说PersonInclude实体类- @JsonInclude(JsonInclude.Include.NON_NULL)
- public class PersonInclude {
- /**
- * 姓名字段,由于未指定@JsonInclude注解,因此继承类级别的NON_NULL策略
- */
- private String name;
- /**
- * 职业字段,显式指定为仅在非空时才包含在序列化结果中
- */
- @JsonInclude(JsonInclude.Include.NON_EMPTY)
- private String occupation;
- /**
- * 兴趣爱好列表,遵循类级别的NON_NULL策略
- */
- private List<String> hobbies;
- // 构造函数、getter/setter等省略...
- }
复制代码 ②使用示例- @Test
- public void testJsonInclude() throws JsonProcessingException {
- PersonInclude personFull = new PersonInclude();
- personFull.setName("小凡");
- personFull.setOccupation("程序员");
- personFull.setHobbies(Arrays.asList("编程","看书","打豆豆"));
- PersonInclude personPartial = new PersonInclude();
- personPartial.setName(null);
- personPartial.setOccupation("保洁员");
- ObjectMapper mapper = new ObjectMapper();
- String json = mapper.writeValueAsString(personFull);
- System.out.println(json);
- String json1 = mapper.writeValueAsString(personPartial);
- System.out.println(json1);
- }
- //输出
- {"name":"小凡","occupation":"程序员","hobbies":["编程","看书","打豆豆"]}
- {"occupation":"保洁员"}
复制代码 ③ 具体表明
- 测试用例1:序列化后,JSON只包含name、occupation和hobbies字段都不为空,所以都表现出来
- 测试用例2:创建了一个部门字段为null或空的PersonInclude实例。为空的不表现
9.2.2 @JsonGetter
用于告诉Jackson,应该通过调用getter方法而不是通过直接字段访问来获取某个字段值
①创建PersonGetter实体类- public class PersonGetter {
- private String firstName;
- private String lastName;
- // 使用 @JsonGetter 定义一个方法,该方法将在序列化时作为 "fullName" 属性的值返回
- @JsonGetter("fullName")
- public String getFullName() {
- return this.firstName + " " + this.lastName;
- }
- // 构造函数和其他常规setter/getter方法
- }
复制代码 ②使用示例- @Test
- public void testJsonGetter() throws JsonProcessingException {
- ObjectMapper mapper = new ObjectMapper(); // 创建 Jackson 的 ObjectMapper 实例
- PersonGetter person = new PersonGetter("张", "小凡");// 创建一个 Person 对象
- String json = mapper.writeValueAsString(person); // 将 Person 对象序列化为 JSON 字符串
- System.out.println(json); // 输出序列化后的 JSON
- }
- //输出
- {"firstName":"张","lastName":"小凡","fullName":"张 小凡"}
复制代码 @JsonGetter 注解成功地将 getFullName() 方法的结果映射到 JSON 对象中的 "fullName" 属性。
9.2.3 @JsonAnyGetter
用于标记一个方法,获取除已知属性外的所有其他键值对。这些键值对通常存储在一个 Map 结构中,
以便将它们作为一个附加的对象进行序列化。
① 创建CustomData实体类- @Data
- public class CustomData {
- private final Map<String, Object> additionalProperties = new HashMap<>();
- // 使用 @JsonAnyGetter 定义一个方法,该方法将在序列化时返回所有额外属性
- @JsonAnyGetter
- public Map<String, Object> getAdditionalProperties() {
- return additionalProperties;
- }
- // 提供方法来添加或修改额外属性
- public void addProperty(String key, Object value) {
- additionalProperties.put(key, value);
- }
- }
复制代码 ②使用示例- @Test
- public void testJsonAnyGetter() throws JsonProcessingException {
- ObjectMapper mapper = new ObjectMapper(); // 创建 Jackson 的 ObjectMapper 实例
- CustomData data = new CustomData();
- data.addProperty("key1", "value1");
- data.addProperty("key2", 42);
- data.addProperty("key3", "value3");
- String json = mapper.writeValueAsString(data); // 将 CustomData 对象序列化为 JSON 字符串
- System.out.println(json); // 输出序列化后的 JSON
- }
- //输出
- {"key1":"value1","key2":42,"key3":"value3"}
复制代码 @JsonAnyGetter 注解的重要作用如下:
- 动态属性支持:通过在返回 Map 的方法上使用 @JsonAnyGetter,您可以将一个对象的动态属性集合序列化为 JSON 对象的多个键值对。这些属性可能是在运行时添加的,或者基于某些条件动态天生的。
- 简化结构:制止为每个可能的动态属性单独声明字段和 getter/setter。只需维护一个 Map,即可处理恣意数目和类型的额外属性。
- 兼容性与灵活性:当需要与未知或未来可能变革的数据结构交互时,@JsonAnyGetter 可确保 JSON 表示能够容纳未预界说的属性,从而提高系统的兼容性和适应性。
9.2.4 @JsonPropertyOrder
用于指定类中属性在序列化为 JSON 时的排序规则
①创建OrderPerson 实体类- public class OrderPerson {
- @JsonProperty("firstName")
- private String givenName;
- @JsonProperty("lastName")
- private String familyName;
- // 构造函数和其他常规setter/getter方法
- }
复制代码 ②使用示例- @Test
- public void testJsonPropertyOrder() throws JsonProcessingException {
- ObjectMapper mapper = new ObjectMapper(); // 创建 Jackson 的 ObjectMapper 实例
- OrderPerson person = new OrderPerson("John", "Doe"); // 创建一个 OrderedPerson 对象
- String json = mapper.writeValueAsString(person); // 将 OrderedPerson 对象序列化为 JSON 字符串
- System.out.println(json); // 输出序列化后的 JSON
- }
- //输出
- {"firstName":"John","lastName":"Doe"}
复制代码 @JsonPropertyOrder 注解按照指定顺序("lastName","firstName")对 JSON 对象的属性进行了排序。
9.2.5 @JsonRawValue
用于标记一个字段或方法返回值,指示Jackson在序列化时应将其原始值视为未经转义的 JSON 字符串,并直接嵌入到输出的 JSON 文档中。纵然所标记的值包含 JSON 特殊字符(如双引号、反斜杠等),也不会对其进行转义
① 创建一个包含 @JsonRawValue 注解的类- public class RawJsonValueExample {
- private String normalProperty = "这是一个正常字符串";
- // 使用 @JsonRawValue 标记 rawJsonProperty,使其内容被视为未经转义的 JSON 值
- @JsonRawValue
- private String rawJsonProperty = "{"key": "value", "array": [1, 2, 3]}";
- // 构造函数和其他常规setter/getter方法
- }
复制代码 ②使用示例- @Test
- public void testJsonRawValue() throws JsonProcessingException {
- ObjectMapper mapper = new ObjectMapper(); // 创建 Jackson 的 ObjectMapper 实例
- RawJsonValueExample example = new RawJsonValueExample(); // 创建一个 RawJsonValueExample 对象
- String json = mapper.writeValueAsString(example); // 将 RawJsonValueExample 对象序列化为 JSON 字符串
- System.out.println(json); // 输出序列化后的 JSON
- }
- //输出
- {"normalProperty":"这是一个正常字符串","rawJsonProperty":{"key": "value", "array": [1, 2, 3]}}
复制代码 rawJsonProperty 的值并未被转义,而是作为一个完整的 JSON 对象直接嵌入到父 JSON 文档中。
当您需要在 JSON 对象中嵌入另一段 JSON 文本时,使用 @JsonRawValue 可以确保这段文本以未经转义的情势出如今输出的 JSON 中,保持其原有的 JSON 结构。
9.2.6 @JsonValue
用于标记一个方法或字段,Jackson在序列化该类实例时,直接使用该方法的返回值或字段的值作为整个对象的 JSON 表示,而非按照类的通例属性进行序列化
① 创建JsonValueExample类- public class JsonValueExample {
- private LocalDate date;
- private String formattedDate;
- @JsonValue
- public String asJsonString() {
- return "{"date":"" + date.toString() + "","formatted":"" + formattedDate + ""}";
- }
- // 构造函数和其他常规setter/getter方法
- }
复制代码 ② 使用示例- @Test
- public void testJsonValue() throws JsonProcessingException {
- ObjectMapper mapper = new ObjectMapper();
- JsonValueExample example = new JsonValueExample();
- example.setDate(LocalDate.of(2024,4,6));
- example.setFormattedDate("yyyy-MM-dd");
- String json = mapper.writeValueAsString(example);
- System.out.println(json);
- }
- //输出
- "{"date":"2024-04-06","formatted":"yyyy-MM-dd"}"
复制代码 在 asJsonString() 方法上应用注解,指示序列化时应将该方法的返回值作为整个对象的 JSON 表达,忽略类中其他的属性
9.2.7 @JsonSerialize
于指定类、字段或方法在序列化过程中使用的自界说序列化逻辑
① 创建一个包含 @JsonSerialize 注解的类及其自界说序列化器- public class BooleanSerializer extends JsonSerializer<Boolean> {
- @Override
- public void serialize(Boolean aBoolean, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
- if(aBoolean){
- jsonGenerator.writeNumber(1);
- } else {
- jsonGenerator.writeNumber(0);
- }
- }
- }
- public class PersonSerializer {
- public long personId = 0;
- public String name = "John";
- @JsonSerialize(using = BooleanSerializer.class)
- public boolean enabled = false;
- // 构造函数和其他常规setter/getter方法
- }
复制代码 ②使用示例- @Test
- public void testJsonSerialize() throws JsonProcessingException {
- PersonSerializer person = new PersonSerializer();
- person.setName("小凡");
- person.setPersonId(1001);
- person.setEnabled(true);
- ObjectMapper mapper = new ObjectMapper();
- String json = mapper.writeValueAsString(person);
- System.out.println(json);
- }
- //输出
- {"personId":1001,"name":"小凡","enabled":1}
复制代码 通过上面代码测试我们可以看到,enabled被序列化为1
9.3 反序列化注解
9.3.1 @JsonSetter
用于标记一个方法或字段,指示 Jackson 在反序列化过程中应如何设置该属性的值
①创建PersonSetter 类- public class PersonSetter {
- @JsonSetter("first_name")
- private String firstName;
- @JsonSetter("last_name")
- private String lastName;
- // 其他getter setter toString方法省略...
- }
复制代码 ②使用示例- @Test
- public void testJsonSetter() throws JsonProcessingException {
- String jsonNormal = "{"first_name":"张", "last_name":"小凡"}";
- ObjectMapper mapper = new ObjectMapper();
- PersonSetter person = mapper.readValue(jsonNormal, PersonSetter.class);
- System.out.println(person);
- }
- //输出
- PersonSetter(firstName=张, lastName=小凡)
复制代码 @JsonSetter 注解提供了灵活的方式来定制Jackson在反序列化期间如何将JSON数据映射到Java对象的属性
9.3.2 @JsonAnySetter
用于处理反序列化过程中碰到的未知或额外的 JSON 键值对。当一个对象的JSON表示中包含无法直接映射到已声明属性的键时,这个注解可以帮助捕获并存储这些额外的数据
① 界说带有 @JsonAnySetter 注解的类- @ToString
- public class CustomObject {
- private String knownProperty;
- private Map<String, Object> additionalProperties = new HashMap<>();
- // 已知属性的setter方法
- public void setKnownProperty(String knownProperty) {
- this.knownProperty = knownProperty;
- }
- // 获取已知属性的getter方法省略...
- /**
- * @JsonAnySetter注解方法,用于处理反序列化过程中遇到的所有未知属性。
- *
- * @param key 未知属性的键
- * @param value 未知属性的值
- */
- @JsonAnySetter
- public void addAdditionalProperty(String key, Object value) {
- additionalProperties.put(key, value);
- }
- // 提供访问额外属性的方法
- public Map<String, Object> getAdditionalProperties() {
- return additionalProperties;
- }
- }
复制代码 ②使用示例- @Test
- public void testJsonAnySetter() throws JsonProcessingException {
- String json ="{"knownProperty":"expectedValue","extraField1":"someValue","extraField2":42,"nestedObject":{"key":"value"}}";
- ObjectMapper mapper = new ObjectMapper();
- CustomObject customObject = mapper.readValue(json, CustomObject.class);
- System.out.println(customObject);
- }
- //输出
- CustomObject(knownProperty=expectedValue, additionalProperties={extraField1=someValue, nestedObject={key=value}, extraField2=42})
复制代码 @JsonAnySetter注解来应对JSON反序列化过程中可能出现的未知属性,确保所有数据都能被妥善处理和保留。这种机制特别适用于需要兼容动态或扩展性较强的JSON输入场景。
9.3.3 @JsonCreator
用于标记一个构造器、静态工厂方法或实例方法,使其成为反序列化过程中创建对象实例的入口点。这个注解帮助 Jackson 确定如何根据 JSON 数据构建相应的 Java 对象。
① 界说带有 @JsonCreator 注解的类- public class AddressCreator {
- private String street;
- private int number;
- private String city;
- // 构造器上使用@JsonCreator注解,指示Jackson使用此构造器反序列化JSON
- @JsonCreator
- public AddressCreator(@JsonProperty("street") String street,
- @JsonProperty("number") int number,
- @JsonProperty("city") String city) {
- this.street = street;
- this.number = number;
- this.city = city;
- }
- // getter setter toString方法省略...
- }
复制代码 ② 使用示例- @Test
- public void testCreateor() throws JsonProcessingException {
- String json ="{"street":"呈贡区","number":123,"city":"昆明"}";
- ObjectMapper mapper = new ObjectMapper();
- AddressCreator addressCreator = mapper.readValue(json, AddressCreator.class);
- System.out.println(addressCreator);
- }
- //输出
- AddressCreator(street=呈贡区, number=123, city=昆明)
复制代码 9.3.4 @JacksonInject
用于在反序列化过程中自动注入依赖项或附加信息到目标对象。通常用于处理那些不能直接从 JSON 数据中获取、但又希望在反序列化完成后立即可用的信息
① 界说带有 @JacksonInject 注解的类- public class UserInject {
- private String name;
- private String email;
- // @JacksonInject注解在字段上,指示Jackson在反序列化过程中注入特定值
- @JacksonInject("defaultEmail")
- private String defaultEmail;
- // 构造器和getter setter toString方法省略...
- }
复制代码 ②使用示例- @Test
- public void testJacksonInject() throws JsonProcessingException {
- // 设置可注入的值
- InjectableValues injectables = new InjectableValues.Std().addValue("defaultEmail", "xiaofan@example.com");
- // 创建ObjectMapper并配置注入值
- ObjectMapper mapper = new ObjectMapper().setInjectableValues(injectables);
- String json = "{"name":"小凡"}";
- // 反序列化时,defaultEmail字段会被自动注入指定值
- UserInject userInject = mapper.readValue(json, UserInject.class);
- System.out.println("Name: " + userInject.getName());
- System.out.println("Email: " + userInject.getEmail());
- System.out.println("Default Email: " + userInject.getDefaultEmail());
- }
- //输出
- Name: 小凡
- Email: null
- Default Email: xiaofan@example.com
复制代码 @JacksonInject 注解在Jackson反序列化过程中用于引入外部依赖或默认值,使得这些信息能够在反序列化完成后立即可用
9.3.5 @JsonDeserialize
用于在反序列化过程中指定自界说的反序列化器来处理某个字段或类的特殊逻辑
①自界说反序列化器- public class BooleanDeserializer extends JsonDeserializer<Boolean> {
- @Override
- public Boolean deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException {
- String text = jsonParser.getText();
- if("0".equals(text)) {
- return false;
- }
- return true;
- }
- }
复制代码 ② @JsonDeserialize注解添加到要为其使用自界说反序列化器的字段- public class PersonDeserialize {
- public long id;
- public String name;
- @JsonDeserialize(using = BooleanDeserializer.class)
- public boolean enabled = false;
- //构造器和getter setter toString方法省略...
- }
复制代码 ③使用示例- {"id":1001,"name":"小凡","enabled":1}
复制代码- @Test
- public void testJsonDeserialize() throws JsonProcessingException {
- String json ="{"id":1001,"name":"小凡","enabled":1}";
- ObjectMapper mapper = new ObjectMapper();
- PersonDeserialize personDeserialize = mapper.readValue(json, PersonDeserialize.class);
- System.out.println(personDeserialize);
- }
- //输出
- PersonDeserialize(id=1001, name=小凡, enabled=true)
复制代码 我们json字符串中enabled的值是1,最终反序列化成PersonDeserialize 对象后值变成了ture
本期内容到此就结束了,希望对你有所帮助。我们下期再见 (●'◡'●)
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |