從 “JSON 字段適配噩夢(mèng)” 到 “Spring Boot 優(yōu)雅解決方案”,你只差這一篇
在 Spring Boot 項(xiàng)目開(kāi)發(fā)中,前后端數(shù)據(jù)交互時(shí),JSON 數(shù)據(jù)格式憑借其簡(jiǎn)潔、高效的特性,成為了數(shù)據(jù)傳輸?shù)?“寵兒”。但在 Spring Boot 項(xiàng)目日常開(kāi)發(fā)中,一個(gè)讓無(wú)數(shù)開(kāi)發(fā)者頭疼不已的問(wèn)題常常出現(xiàn):前端傳來(lái)的 JSON 數(shù)據(jù)與后端 Java 類的屬性對(duì)不上號(hào) 。想象一下,你滿心歡喜地準(zhǔn)備接收前端發(fā)送的數(shù)據(jù),卻發(fā)現(xiàn)它們像是兩個(gè)世界的產(chǎn)物,無(wú)法完美契合。
比如,前端提交的 JSON 數(shù)據(jù)可能是這樣的:
{
"name": "lybgeek",
"mobile": "13800000000",
"extFields": {
"email": "lybgeek@163.com",
"age": 18
}
}
又或是這樣:
{
"name": "lybgeek",
"mobile": "13800000000",
"email": "lybgeek@163.com",
"age": 18
}
而后端定義的 Java 類卻只有簡(jiǎn)單的這幾個(gè)屬性:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String name;
private String mobile;
}
這種情況下,后端如何精準(zhǔn)地獲取 JSON 中的email和age字段呢?難道只能望 “數(shù)據(jù)” 興嘆,束手無(wú)策?當(dāng)然不是!今天,就為大家?guī)?lái)一場(chǎng)知識(shí)盛宴,詳細(xì)剖析多種實(shí)用到爆的解決方案,讓你輕松攻克這一難題,代碼寫(xiě)得更絲滑,開(kāi)發(fā)效率直線上升!
場(chǎng)景痛點(diǎn)剖析:為什么 JSON 字段匹配問(wèn)題如此棘手?
在微服務(wù)架構(gòu)盛行的當(dāng)下,前后端分離開(kāi)發(fā)模式成為主流。前端開(kāi)發(fā)人員按照業(yè)務(wù)需求構(gòu)建靈活的數(shù)據(jù)結(jié)構(gòu),而后端開(kāi)發(fā)人員則依據(jù)自身的業(yè)務(wù)邏輯定義 Java 實(shí)體類。這就導(dǎo)致了在數(shù)據(jù)交互的過(guò)程中,出現(xiàn) JSON 字段與 Java 類屬性不匹配的情況。這種不匹配不僅增加了開(kāi)發(fā)的復(fù)雜度,還可能引發(fā)數(shù)據(jù)解析錯(cuò)誤、類型轉(zhuǎn)換異常等問(wèn)題,嚴(yán)重影響項(xiàng)目的穩(wěn)定性和開(kāi)發(fā)進(jìn)度。
實(shí)戰(zhàn)演練:獲取 JSON 中 “多余字段” 的 N 種姿勢(shì)
方法一:巧用 Map,輕松接招
通過(guò)定義一個(gè)Map對(duì)象來(lái)接收和處理這些額外字段,簡(jiǎn)單又直接,就像是給你的數(shù)據(jù)處理工具箱里添加了一把萬(wàn)能扳手,輕松應(yīng)對(duì)各種不匹配情況。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String name;
private String mobile;
protected Map<String,Object> extFields;
}
測(cè)試代碼如下:
@PostMapping("json-map")
public User getUser(@RequestBody User user){
UserUtil.print(user,"email","age");
return user;
}
打印工具類:
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));
}
}
}
}
單元測(cè)試:
@Test
@DisplayName("測(cè)試-通過(guò)Map處理JSON對(duì)象中未被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();
}
控制臺(tái)輸出:
name:lybgeek
mobile:13800000000
email:lybgeek@163.com
age:18
優(yōu)勢(shì): 實(shí)現(xiàn)簡(jiǎn)單,無(wú)需引入額外的依賴,對(duì)于臨時(shí)處理少量額外字段非常方便。
局限: 在復(fù)雜的業(yè)務(wù)場(chǎng)景下,對(duì)Map的操作可能會(huì)使代碼變得不夠直觀,難以維護(hù)。
方法二:借助 JsonNode,精準(zhǔn)出擊
引入 Jackson 庫(kù),使用JsonNode來(lái)處理,就如同給你的數(shù)據(jù)處理裝上了高精度的瞄準(zhǔn)鏡,精準(zhǔn)定位并提取所需字段。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String name;
private String mobile;
private JsonNode extFields;
}
測(cè)試代碼:
@PostMapping("json-node")
public User getUser(@RequestBody User user){
UserUtil.print(user,"email","age");
return user;
}
單元測(cè)試:
@Test
@DisplayName("測(cè)試-通過(guò)JsonNode處理JSON對(duì)象中未被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();
}
控制臺(tái)輸出:
name:lybgeek
mobile:13800000000
email:lybgeek@163.com
age:18
優(yōu)勢(shì): 提供了靈活的 JSON 節(jié)點(diǎn)訪問(wèn)方式,能夠處理復(fù)雜的 JSON 結(jié)構(gòu),對(duì)于嵌套的 JSON 數(shù)據(jù)處理得心應(yīng)手。
局限: 需要對(duì) Jackson 庫(kù)有一定的了解,并且代碼中對(duì)JsonNode的操作較多時(shí),會(huì)增加代碼的復(fù)雜性。
方法三:@JsonAnySetter 與 @JsonAnyGetter 雙劍合璧
- @JsonAnySetter 是 Jackson 庫(kù)中的一個(gè)注解,用于處理 JSON 對(duì)象中未被 Java 類中屬性映射的字段,當(dāng) JSON 數(shù)據(jù)包含不匹配類中定義的屬性時(shí),使用此注解的方法會(huì)接收這些額外的鍵值對(duì)。
- @JsonAnyGetter 注解用于標(biāo)記一個(gè)返回 Map<String, Object> 類型的方法,Jackson 在將 Java 對(duì)象序列化為 JSON 時(shí),會(huì)將該方法返回的 Map 中的鍵值對(duì)添加到生成的 JSON 對(duì)象中,這樣可以在不預(yù)先定義所有屬性的情況下,動(dòng)態(tài)地向 JSON 中添加屬性。
使用 Jackson 庫(kù)的@JsonAnySetter和@JsonAnyGetter注解,實(shí)現(xiàn)動(dòng)態(tài)處理,就像給你的數(shù)據(jù)處理賦予了超能力,能夠自動(dòng)適應(yīng)各種變化。
@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;
}
}
測(cè)試代碼:
@PostMapping("json-any-setter")
public User getUser(@RequestBody User user){
UserUtil.print(user,"email","age");
return user;
}
單元測(cè)試:
@Test
@DisplayName("測(cè)試-通過(guò)@JsonAnySetter處理JSON對(duì)象中未被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());
}
控制臺(tái)輸出:
name:lybgeek
mobile:13800000000
email:lybgeek@163.com
age:18
{"name":"lybgeek","mobile":"13800000000","email":"lybgeek@163.com","age":18}
優(yōu)勢(shì): 在 Java 類中無(wú)需預(yù)先定義所有屬性,能夠動(dòng)態(tài)地處理 JSON 中的額外字段,代碼簡(jiǎn)潔且易于理解。
局限: 主要適用于處理簡(jiǎn)單的鍵值對(duì)形式的額外字段,如果 JSON 結(jié)構(gòu)復(fù)雜,可能需要額外的處理邏輯。
方法四:自定義序列化與反序列化,深度定制
通過(guò)@JsonDeserialize和@JsonSerialize注解結(jié)合自定義序列化和反序列化類,實(shí)現(xiàn)更靈活的處理,就像是為你的數(shù)據(jù)處理量身定制一套專屬的 “戰(zhàn)甲”,無(wú)往而不利。
自定義反序列化類:
public class UserJsonDeserializer extends JsonDeserializer<User> {
@Override
public User deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode jsonNode = p.getCodec().readTree(p);
// 獲取JSON對(duì)象中未被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 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);
}
}
在實(shí)體類上添加注解:
@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;
}
測(cè)試代碼:
@PostMapping("json-deserializer")
public User getUser(@RequestBody User user){
UserUtil.print(user,"email","age");
return user;
}
單元測(cè)試:
@Test
@DisplayName("測(cè)試-通過(guò)自定義反序列化+@JsonDeserialize處理JSON對(duì)象中未被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());
}
控制臺(tái)輸出:
name:lybgeek
mobile:13800000000
email:lybgeek@163.com
age:18
{"name":"lybgeek","mobile":"13800000000","email":"lybgeek@163.com","age":18}
優(yōu)勢(shì): 能夠根據(jù)具體業(yè)務(wù)需求,對(duì) JSON 數(shù)據(jù)的序列化和反序列化過(guò)程進(jìn)行全面的控制,適用于復(fù)雜的業(yè)務(wù)場(chǎng)景。
局限: 開(kāi)發(fā)成本較高,需要編寫(xiě)較多的代碼,并且對(duì)開(kāi)發(fā)者對(duì) Jackson 庫(kù)的理解要求較高。
方法五:@JsonComponent,全局掌控
使用@JsonComponent注解,將自定義的序列化器和反序列化器注冊(cè)到 Jackson 的ObjectMapper中,實(shí)現(xiàn)全局統(tǒng)一處理,就像是給整個(gè)數(shù)據(jù)處理流程安排了一位 “大管家”,一切井井有條。
@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對(duì)象中未被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);
}
}
注: 通常情況下,序列化器和反序列化器類會(huì)定義為使用 @JsonComponent 注解的類的靜態(tài)內(nèi)部類。因?yàn)榉庆o態(tài)內(nèi)部類依賴于外部類的實(shí)例, 而在 Jackson 序列化和反序列化過(guò)程中,不會(huì)創(chuàng)建外部類的實(shí)例來(lái)調(diào)用非靜態(tài)內(nèi)部類的方法,所以必須使用靜態(tài)內(nèi)部類。
測(cè)試代碼:
@PostMapping("json-component")
public User getUser(@RequestBody User user){
UserUtil.print(user,"email","age");
return user;
}
單元測(cè)試:
@Test
@DisplayName("通過(guò)自定義反序列化+@UserJsonComponent全局處理JSON對(duì)象中未被Java類中屬性映射的字段-單元測(cè)試")
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());
}
控制臺(tái)輸出:
name:lybgeek
mobile:13800000000
email:lybgeek@163.com
age:18
{"name":"lybgeek","mobile":"13800000000","email":"lybgeek@163.com","age":18}
優(yōu)勢(shì): 實(shí)現(xiàn)了全局的統(tǒng)一配置,無(wú)需在每個(gè)實(shí)體類上分別添加注解,便于維護(hù)和管理。
局限: 如果項(xiàng)目中存在多個(gè)不同的 JSON 處理需求,可能會(huì)導(dǎo)致@JsonComponent注解的類變得過(guò)于龐大和復(fù)雜。
總結(jié)
本文詳細(xì)介紹了在 Spring Boot 項(xiàng)目中處理 JSON 對(duì)象中未被 Java 類屬性映射字段的多種方法,本質(zhì)上都是圍繞 JSON 與對(duì)象之間的序列化和反序列化展開(kāi)。每種方法都有其獨(dú)特的優(yōu)勢(shì)和適用場(chǎng)景,希望大家在實(shí)際開(kāi)發(fā)中能夠根據(jù)具體需求靈活選擇。
福利:一鍵獲取完整代碼
為了方便大家學(xué)習(xí)和實(shí)踐,我已經(jīng)將完整的示例代碼上傳至 GitHub,點(diǎn)擊下方鏈接即可獲?。?br>
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-dynamics-json
覺(jué)得文章有用的話,別忘了點(diǎn)贊、分享和訂閱哦!你的支持是我創(chuàng)作的最大動(dòng)力!如果在實(shí)踐過(guò)程中有任何問(wèn)題,歡迎在評(píng)論區(qū)留言,我們一起探討。

浙公網(wǎng)安備 33010602011771號(hào)