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