从 “JSON 字段适配噩梦” 到 “Spring Boot 优雅解决方案”,你只差这一篇

在 Spring Boot 项目开发中,前后端数据交互时,JSON 数据格式凭借其简洁、高效的特性,成为了数据传输的 "宠儿"。但在 Spring Boot 项目日常开发中,一个让无数开发者头疼不已的问题常常出现:前端传来的 JSON 数据与后端 Java 类的属性对不上号 。想象一下,你满心欢喜地准备接收前端发送的数据,却发现它们像是两个世界的产物,无法完美契合。 比如,前端提交的 JSON 数据可能是这样的:

json 复制代码
{
    "name": "lybgeek",
    "mobile": "13800000000",
    "extFields": {
        "email": "lybgeek@163.com",
        "age": 18
    }
}

又或是这样:

json 复制代码
{
    "name": "lybgeek",
    "mobile": "13800000000",
    "email": "lybgeek@163.com",
    "age": 18
}

而后端定义的 Java 类却只有简单的这几个属性:

java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

    private String name;

    private String mobile;
}

这种情况下,后端如何精准地获取 JSON 中的email和age字段呢?难道只能望 "数据" 兴叹,束手无策?当然不是!今天,就为大家带来一场知识盛宴,详细剖析多种实用到爆的解决方案,让你轻松攻克这一难题,代码写得更丝滑,开发效率直线上升!

场景痛点剖析:为什么 JSON 字段匹配问题如此棘手?

在微服务架构盛行的当下,前后端分离开发模式成为主流。前端开发人员按照业务需求构建灵活的数据结构,而后端开发人员则依据自身的业务逻辑定义 Java 实体类。这就导致了在数据交互的过程中,出现 JSON 字段与 Java 类属性不匹配的情况。这种不匹配不仅增加了开发的复杂度,还可能引发数据解析错误、类型转换异常等问题,严重影响项目的稳定性和开发进度。

实战演练:获取 JSON 中 "多余字段" 的 N 种姿势

方法一:巧用 Map,轻松接招

通过定义一个Map对象来接收和处理这些额外字段,简单又直接,就像是给你的数据处理工具箱里添加了一把万能扳手,轻松应对各种不匹配情况。

java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String name;
    private String mobile;
    protected Map<String,Object> extFields;
}

测试代码如下:

java 复制代码
 @PostMapping("json-map")
public User getUser(@RequestBody User user){
    UserUtil.print(user,"email","age");
    return user;
}

打印工具类:

java 复制代码
public final class UserUtil {

    private UserUtil() {
    }


    public static void print(User user String...key){
        System.out.println("name:" + user .getName());
        System.out.println("mobile:" + user .getMobile());

        if(ArrayUtil.isNotEmpty(key)){
            for (String k : key) {
                    System.out.println(k + ":" + user.getExtFields().get(k));
                }
        }
    }
    }

单元测试:

java 复制代码
 @Test
    @DisplayName("测试-通过Map处理JSON对象中未被Java类中属性映射的字段")
    public void testJsonMap() throws Exception{
       
        String json = """
		{
		    "name": "lybgeek",
		    "mobile": "13800000000",
		    "extFields": {
		        "email": "lybgeek@163.com",
		        "age": 18
		    }
		}
		"""

        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/user/json-map")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(json)
        ).andExpect(MockMvcResultMatchers.status().isOk()).andReturn();


    }

控制台输出:

java 复制代码
name:lybgeek
mobile:13800000000
email:lybgeek@163.com
age:18

优势: 实现简单,无需引入额外的依赖,对于临时处理少量额外字段非常方便。 局限: 在复杂的业务场景下,对Map的操作可能会使代码变得不够直观,难以维护。

方法二:借助 JsonNode,精准出击

引入 Jackson 库,使用JsonNode来处理,就如同给你的数据处理装上了高精度的瞄准镜,精准定位并提取所需字段。

java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

    private String name;
    private String mobile;
    private JsonNode extFields;
}

测试代码:

java 复制代码
 @PostMapping("json-node")
    public User getUser(@RequestBody User user){
        UserUtil.print(user,"email","age");
        return user;

    }

单元测试:

java 复制代码
 @Test
    @DisplayName("测试-通过JsonNode处理JSON对象中未被Java类中属性映射的字段")
    public void testJsonNode() throws Exception{
         String json = """
		{
		    "name": "lybgeek",
		    "mobile": "13800000000",
		    "extFields": {
		        "email": "lybgeek@163.com",
		        "age": 18
		    }
		}
		"""

        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/user/json-node")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(json)
        ).andExpect(MockMvcResultMatchers.status().isOk()).andReturn();

        

      

    }

控制台输出:

java 复制代码
name:lybgeek
mobile:13800000000
email:lybgeek@163.com
age:18

优势: 提供了灵活的 JSON 节点访问方式,能够处理复杂的 JSON 结构,对于嵌套的 JSON 数据处理得心应手。 局限: 需要对 Jackson 库有一定的了解,并且代码中对JsonNode的操作较多时,会增加代码的复杂性。

方法三:@JsonAnySetter 与 @JsonAnyGetter 双剑合璧

  • @JsonAnySetter 是 Jackson 库中的一个注解,用于处理 JSON 对象中未被 Java 类中属性映射的字段,当 JSON 数据包含不匹配类中定义的属性时,使用此注解的方法会接收这些额外的键值对。
  • @JsonAnyGetter 注解用于标记一个返回 Map<String, Object> 类型的方法,Jackson 在将 Java 对象序列化为 JSON 时,会将该方法返回的 Map 中的键值对添加到生成的 JSON 对象中,这样可以在不预先定义所有属性的情况下,动态地向 JSON 中添加属性。

使用 Jackson 库的@JsonAnySetter和@JsonAnyGetter注解,实现动态处理,就像给你的数据处理赋予了超能力,能够自动适应各种变化。

java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

    private String name;
    private String mobile;
    protected Map<String,Object> extFields;

     @JsonAnySetter
    public void addFields(String key,Object value){
        if(MapUtil.isEmpty(extFields)){
            extFields = new HashMap<>();
        }
        extFields.put(key,value);
    }

 @JsonAnyGetter
    public Map<String,Object> getExtFields(){
        return extFields;
    }
}

测试代码:

java 复制代码
 @PostMapping("json-any-setter")
    public User getUser(@RequestBody User user){
        UserUtil.print(user,"email","age");
        return user;

    }

单元测试:

java 复制代码
  @Test
    @DisplayName("测试-通过@JsonAnySetter处理JSON对象中未被Java类中属性映射的字段")
    public void testJsonAnySetter() throws Exception{

 String json = """
		{
    "name": "lybgeek",
    "mobile": "13800000000",
    "email": "lybgeek@163.com",
    "age": 18
	}
		"""

        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/user/json-any-setter")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(json)
        ).andExpect(MockMvcResultMatchers.status().isOk()).andReturn();

        System.out.println(mvcResult.getResponse().getContentAsString());

    }

控制台输出:

java 复制代码
name:lybgeek
mobile:13800000000
email:lybgeek@163.com
age:18
{"name":"lybgeek","mobile":"13800000000","email":"lybgeek@163.com","age":18}

优势: 在 Java 类中无需预先定义所有属性,能够动态地处理 JSON 中的额外字段,代码简洁且易于理解。 局限: 主要适用于处理简单的键值对形式的额外字段,如果 JSON 结构复杂,可能需要额外的处理逻辑。

方法四:自定义序列化与反序列化,深度定制

通过@JsonDeserialize和@JsonSerialize注解结合自定义序列化和反序列化类,实现更灵活的处理,就像是为你的数据处理量身定制一套专属的 "战甲",无往而不利。

自定义反序列化类:

java 复制代码
public class UserJsonDeserializer extends JsonDeserializer<User> {

    @Override
    public User deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        JsonNode jsonNode = p.getCodec().readTree(p);
        // 获取JSON对象中未被User类中属性映射的字段
        Map<String,Object> extFields = new HashMap<>();
        addFields(extFields,jsonNode,"email","age");

        User user = new User();
        user.setExtFields(extFields);
        user.setName(getValue(jsonNode,"name"));
        user.setMobile(getValue(jsonNode,"mobile"));

        return user;
    }

public String getValue(JsonNode jsonNode, String key){
        if(jsonNode.has(key)){
            return jsonNode.get(key).asText();
        }
        return null;
    }

    public void addFields(Map<String,Object> extFields, JsonNode jsonNode, String...key){
        for(String k : key){
            if(jsonNode.has(k)){
                extFields.put(k,getValue(jsonNode,k));
            }
        }
    }

自定义序列化类:

java 复制代码
public class UserJsonSerializer extends JsonSerializer<User> {
    @Override
    public void serialize(User value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        Map<String,Object> userMap = new HashMap<>();
        userMap.put("name",value.getName());
        userMap.put("mobile",value.getMobile());
        if(MapUtil.isNotEmpty(value.getExtFields())){
            userMap.putAll(value.getExtFields());
        }

        gen.writeObject(userMap);
    }
}

在实体类上添加注解:

java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonDeserialize(using = UserJsonDeserializer.class)
@JsonSerialize(using = UserJsonSerializer.class)
public class User {

    private String name;
    private String mobile;
    protected Map<String,Object> extFields;
}

测试代码:

java 复制代码
 @PostMapping("json-deserializer")
    public User getUser(@RequestBody User user){
        UserUtil.print(user,"email","age");
        return user;

    }

单元测试:

java 复制代码
  @Test
     @DisplayName("测试-通过自定义反序列化+@JsonDeserialize处理JSON对象中未被Java类中属性映射的字段")
    public void testJsonDeserializer() throws Exception{

 String json = """
		{
    "name": "lybgeek",
    "mobile": "13800000000",
    "email": "lybgeek@163.com",
    "age": 18
	}
		"""

        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/user/json-deserializer")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(json)
        ).andExpect(MockMvcResultMatchers.status().isOk()).andReturn();

        System.out.println(mvcResult.getResponse().getContentAsString());

    }

控制台输出:

java 复制代码
name:lybgeek
mobile:13800000000
email:lybgeek@163.com
age:18
{"name":"lybgeek","mobile":"13800000000","email":"lybgeek@163.com","age":18}

优势: 能够根据具体业务需求,对 JSON 数据的序列化和反序列化过程进行全面的控制,适用于复杂的业务场景。 局限: 开发成本较高,需要编写较多的代码,并且对开发者对 Jackson 库的理解要求较高。

方法五:@JsonComponent,全局掌控

使用@JsonComponent注解,将自定义的序列化器和反序列化器注册到 Jackson 的ObjectMapper中,实现全局统一处理,就像是给整个数据处理流程安排了一位 "大管家",一切井井有条。

java 复制代码
@JsonComponent
public class UserJsonComponent {

   public static class UserJsonDeserializer extends JsonDeserializer<User> {

    @Override
    public User deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        JsonNode jsonNode = p.getCodec().readTree(p);
        // 获取JSON对象中未被User类中属性映射的字段
        Map<String,Object> extFields = new HashMap<>();
        addFields(extFields,jsonNode,"email","age");

        User user = new User();
        user.setExtFields(extFields);
        user.setName(getValue(jsonNode,"name"));
        user.setMobile(getValue(jsonNode,"mobile"));

        return user;
    }

public String getValue(JsonNode jsonNode, String key){
        if(jsonNode.has(key)){
            return jsonNode.get(key).asText();
        }
        return null;
    }

    public void addFields(Map<String,Object> extFields, JsonNode jsonNode, String...key){
        for(String k : key){
            if(jsonNode.has(k)){
                extFields.put(k,getValue(jsonNode,k));
            }
        }
    }

public static class UserJsonSerializer extends JsonSerializer<User> {
    @Override
    public void serialize(User value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        Map<String,Object> userMap = new HashMap<>();
        userMap.put("name",value.getName());
        userMap.put("mobile",value.getMobile());
        if(MapUtil.isNotEmpty(value.getExtFields())){
            userMap.putAll(value.getExtFields());
        }

        gen.writeObject(userMap);
    }
}

注: 通常情况下,序列化器和反序列化器类会定义为使用 @JsonComponent 注解的类的静态内部类。因为非静态内部类依赖于外部类的实例, 而在 Jackson 序列化和反序列化过程中,不会创建外部类的实例来调用非静态内部类的方法,所以必须使用静态内部类

测试代码:

java 复制代码
 @PostMapping("json-component")
    public User getUser(@RequestBody User user){
        UserUtil.print(user,"email","age");
        return user;

    }

单元测试:

java 复制代码
  @Test
       @DisplayName("通过自定义反序列化+@UserJsonComponent全局处理JSON对象中未被Java类中属性映射的字段-单元测试")
    public void testJsonComponent() throws Exception{

 String json = """
		{
    "name": "lybgeek",
    "mobile": "13800000000",
    "email": "lybgeek@163.com",
    "age": 18
	}
		"""

        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/user/json-component")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(json)
        ).andExpect(MockMvcResultMatchers.status().isOk()).andReturn();

        System.out.println(mvcResult.getResponse().getContentAsString());

    }

控制台输出:

java 复制代码
name:lybgeek
mobile:13800000000
email:lybgeek@163.com
age:18
{"name":"lybgeek","mobile":"13800000000","email":"lybgeek@163.com","age":18}

优势: 实现了全局的统一配置,无需在每个实体类上分别添加注解,便于维护和管理。 局限: 如果项目中存在多个不同的 JSON 处理需求,可能会导致@JsonComponent注解的类变得过于庞大和复杂。

总结

本文详细介绍了在 Spring Boot 项目中处理 JSON 对象中未被 Java 类属性映射字段的多种方法,本质上都是围绕 JSON 与对象之间的序列化和反序列化展开。每种方法都有其独特的优势和适用场景,希望大家在实际开发中能够根据具体需求灵活选择。

福利:一键获取完整代码

为了方便大家学习和实践,我已经将完整的示例代码上传至 GitHub,点击下方链接即可获取: github.com/lyb-geek/sp... 觉得文章有用的话,别忘了点赞、分享和订阅哦!你的支持是我创作的最大动力!如果在实践过程中有任何问题,欢迎在评论区留言,我们一起探讨。

相关推荐
duration~34 分钟前
SpringAI实现Reread(Advisor)
java·人工智能·spring boot·spring
一 乐2 小时前
心理咨询|学生心理咨询评估系统|基于Springboot的学生心理咨询评估系统设计与实现(源码+数据库+文档)
java·数据库·spring boot·后端·论文·毕设·学生心理咨询评估系统
码神本神3 小时前
(附源码)基于Spring Boot的4S店信息管理系统 的设计与实现
java·spring boot·后端
别来无恙1493 小时前
Spring Boot文件上传功能实现详解
java·spring boot·文件上传
bing_1583 小时前
Spring Boot @Validated 和@Valid 区别
java·数据库·spring boot
你我约定有三4 小时前
SpringBoot--SpringBoot参数校验与类型转换异常
java·spring boot·后端
MrSYJ4 小时前
UsernamePasswordAuthenticationFilter中的authenticationManager到底是谁注入的
java·spring boot·后端
练习时长两年半的程序员小胡4 小时前
SpringBoot 自动配置核心机制(面试高频考点)
java·spring boot