从 “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... 觉得文章有用的话,别忘了点赞、分享和订阅哦!你的支持是我创作的最大动力!如果在实践过程中有任何问题,欢迎在评论区留言,我们一起探讨。

相关推荐
全职计算机毕业设计20 小时前
基于微信小程序的运动康复中心预约系统的设计与实现(SpringBoot+Vue+Uniapp)
vue.js·spring boot·微信小程序
摇滚侠1 天前
Spring Boot 3零基础教程,WEB 开发 静态资源默认配置 笔记27
spring boot·笔记·后端
wb043072011 天前
性能优化实战:基于方法执行监控与AI调用链分析
java·人工智能·spring boot·语言模型·性能优化
Chen-Edward1 天前
有了Spring为什么还有要Spring Boot?
java·spring boot·spring
magic334165631 天前
Springboot整合MinIO文件服务(windows版本)
windows·spring boot·后端·minio·文件对象存储
小学鸡!1 天前
Spring Boot实现日志链路追踪
java·spring boot·后端
番茄Salad1 天前
Spring Boot临时解决循环依赖注入问题
java·spring boot·spring cloud
摇滚侠1 天前
Spring Boot 3零基础教程,WEB 开发 自定义静态资源目录 笔记31
spring boot·笔记·后端·spring
摇滚侠1 天前
Spring Boot 3零基础教程,WEB 开发 Thymeleaf 遍历 笔记40
spring boot·笔记·thymeleaf
橘子海全栈攻城狮1 天前
【源码+文档+调试讲解】基于SpringBoot + Vue的知识产权管理系统 041
java·vue.js·人工智能·spring boot·后端·安全·spring