解決Gson的“多個JSON字段”異常
1. 概述
Google Gson是一個有用且靈活的 Java 中 JSON 資料綁定函式庫。在大多數情況下,Gson 無需修改即可對現有類別執行資料綁定。但是,某些類別結構可能會導致難以調試的問題。
一個有趣且可能令人困惑的異常是IllegalArgumentException
,它抱怨多個字段定義:
java.lang.IllegalArgumentException: Class <YourClass> declares multiple JSON fields named <yourField> ...
這可能特別神秘,因為 Java 編譯器不允許同一類別中的多個欄位共用名稱。在本教程中,我們將討論此異常的原因並學習如何解決它。
2. 異常原因
此異常的潛在原因與序列化(或反序列化)類別時混淆 Gson 解析器的類別結構或配置有關。
2.1. @SerializedName
衝突
Gson 提供@SerializedName
註解來允許操作序列化物件中的欄位名稱。這是一個有用的功能,但可能會導致衝突。
例如,讓我們建立一個簡單的類別Basic
Student
:
public class BasicStudent {
private String name;
private String major;
@SerializedName("major")
private String concentration;
// General getters, setters, etc.
}
在序列化期間,Gson 將嘗試使用「 major
」作為major
和concentration,
導致上面的IllegalArgumentException
:
java.lang.IllegalArgumentException: Class BasicStudent declares multiple JSON fields named 'major';
conflict is caused by fields BasicStudent#major and BasicStudent#concentration
異常訊息指向問題字段,只需更改或刪除註釋或重新命名字段即可解決該問題。
Gson 中還有其他用於排除欄位的選項,我們將在本教程後面討論。
首先,讓我們看看導致此異常的其他原因。
2.2.類別繼承層次結構
序列化為 JSON 時,類別繼承也可能是問題的根源。為了探討這個問題,我們需要更新我們的學生資料範例。
讓我們定義兩個類, StudentV1
和StudentV2,
它們擴展tudentV1
S
添加了一個額外的成員變數:
public class StudentV1 {
private String firstName;
private String lastName;
// General getters, setters, etc.
}
public class StudentV2 extends StudentV1 {
private String firstName;
private String lastName;
private String major;
// General getters, setters, etc.
}
值得注意的是, StudentV2
不僅擴展了StudentV1
,還定義了自己的一組變量,其中一些變量與StudentV1
中的變量重複。雖然這不是最佳實踐,但它對於我們的範例以及我們在使用第三方程式庫或遺留套件時在現實世界中可能遇到的情況至關重要。
讓我們建立一個StudentV2
實例並嘗試序列化它。我們可以建立一個單元測試來確認是否拋出了IllegalArgumentException
:
@Test
public void givenLegacyClassWithMultipleFields_whenSerializingWithGson_thenIllegalArgumentExceptionIsThrown() {
StudentV2 student = new StudentV2("Henry", "Winter", "Greek Studies");
Gson gson = new Gson();
assertThatThrownBy(() -> gson.toJson(student))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("declares multiple JSON fields named 'firstName'");
}
與上面的@SerializedName
衝突類似, Gson在類別層次結構中遇到重複名稱時不知道要使用哪個欄位。
3. 解決方案
這個問題有幾種解決方案,每種方案都有自己的優點和缺點,可以提供不同程度的序列化控制。
3.1.將欄位標記為transient
控制序列化欄位最簡單的方法是使用transient
欄位修飾符。我們可以從上面更新BasicStudent
:
public class BasicStudent {
private String name;
private transient String major;
@SerializedName("major")
private String concentration;
// General getters, setters, etc.
}
讓我們創建一個單元測試來嘗試在此更改後進行序列化:
@Test
public void givenBasicStudent_whenSerializingWithGson_thenTransientFieldNotSet() {
BasicStudent student = new BasicStudent("Henry Winter", "Greek Studies", "Classical Greek Studies");
Gson gson = new Gson();
String json = gson.toJson(student);
BasicStudent deserialized = gson.fromJson(json, BasicStudent.class);
assertThat(deserialized.getMajor()).isNull();
}
序列化成功,且major
字段值不包含在反序列化的實例中。
儘管這是一個簡單的解決方案,但這種方法有兩個缺點。新增transient
意味著該欄位將從所有序列化中排除,包括基本的Java序列化。此方法也假設可以修改BasicStudent
,但情況可能並非總是如此。
3.2.使用 Gson 的@Expose
註解進行序列化
如果問題類別可以修改,並且我們想要一種僅適用於 Gson 序列化的方法,那麼我們可以使用@Expose
註解。此註釋通知 Gson 在序列化、反序列化或兩者期間應公開哪些欄位。
我們可以更新StudentV2
實例以僅將其欄位明確公開給 Gson:
public class StudentV2 extends StudentV1 {
@Expose
private String firstName;
@Expose
private String lastName;
@Expose
private String major;
// General getters, setters, etc.
}
如果我們再次運行程式碼,什麼都不會改變,我們仍然會看到異常。預設情況下,Gson 在遇到@Expose
時不會改變其行為——我們需要告訴解析器它應該做什麼。
讓我們更新我們的單元測試以使用GsonBuilder
建立解析器的實例,該實例排除沒有@Expose
欄位:
@Test
public void givenStudentV2_whenSerializingWithGsonExposeAnnotation_thenSerializes() {
StudentV2 student = new StudentV2("Henry", "Winter", "Greek Studies");
Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
String json = gson.toJson(student);
assertThat(gson.fromJson(json, StudentV2.class)).isEqualTo(student);
}
序列化和反序列化現已成功。 @Expose
的優點是仍然是一個簡單的解決方案,同時只影響 Gson 序列化(並且僅當我們配置解析器來識別它時)。
然而,這種方法仍然假設我們可以編輯原始碼。它也沒有提供太多的靈活性——我們關心的所有欄位都需要註釋,而其餘欄位則被排除在序列化和反序列化之外。
3.3.使用 Gson 的ExclusionStrategy
進行序列化
幸運的是,當我們無法更改來源類別或需要更大靈活性時,Gson 提供了一個解決方案: ExclusionStrategy
。
該介面告知 Gson 如何在序列化或反序列化期間排除字段,並允許更複雜的業務邏輯。我們可以宣告一個簡單的ExclusionStrategy
實作:
public class StudentExclusionStrategy implements ExclusionStrategy {
@Override
public boolean shouldSkipField(FieldAttributes field) {
return field.getDeclaringClass() == StudentV1.class;
}
@Override
public boolean shouldSkipClass(Class<?> aClass) {
return false;
}
}
ExclusionStrategy
介面有兩個方法: shouldSkipField()
提供單一欄位層級的粒度控制, shouldSkipClass()
控制是否應跳過某種類型的所有欄位。在上面的範例中,我們從簡單開始並跳過StudentV1
中的所有欄位。
就像@Expose
一樣,我們需要告訴 Gson 如何使用這個策略。讓我們在測試中配置它:
@Test
public void givenStudentV2_whenSerializingWithGsonExclusionStrategy_thenSerializes() {
StudentV2 student = new StudentV2("Henry", "Winter", "Greek Studies");
Gson gson = new GsonBuilder().setExclusionStrategies(new StudentExclusionStrategy()).create();
assertThat(gson.fromJson(gson.toJson(student), StudentV2.class)).isEqualTo(student);
}
值得注意的是,我們使用setExclusionStrategies()
配置解析器 - 這意味著我們的策略用於序列化和反序列化。
如果我們希望在應用ExclusionStrategy
時具有更大的靈活性,我們可以以不同的方式配置解析器:
// Only exclude during serialization
Gson gson = new GsonBuilder().addSerializationExclusionStrategy(new StudentExclusionStrategy()).create();
// Only exclude during de-serialization
Gson gson = new GsonBuilder().addDeserializationExclusionStrategy(new StudentExclusionStrategy()).create();
這種方法比我們的其他兩個解決方案稍微複雜一些:我們需要聲明一個新類,並更多地考慮是什麼使得包含一個字段變得重要。在此範例中,我們讓ExclusionStrategy
中的業務邏輯保持相當簡單,但這種方法的優點是更豐富、更強大的欄位排除。最後,我們不需要更改StudentV2
或StudentV1
內部的程式碼。
4。結論
在本文中,我們討論了使用 Gson 時可能遇到的棘手但最終可修復的IllegalArgumentException
的原因。
我們發現,根據我們對簡單性、粒度和靈活性的需求,我們可以實施多種解決方案。
與往常一樣,所有程式碼都可以在 GitHub 上找到。