Java ♨️ POJO 中自定義特殊 get 方法導致 JSON 序列化問題

Jayden Lin
程式猿吃香蕉
Published in
5 min read2 days ago

筆者曾任職 Yahoo,現在區塊鏈產業打滾,《軟體需求溝通 ─ 從外商公司學跨部門協作開發》線上課程講師,紛絲團《程式猿吃香蕉🍌

POJO 是 Plain Old Java Object 的縮寫,是一種簡單的 Java 對象,通常用於表示資料。它的結構簡單,不依賴於特定的框架。一個基本的 POJO 範例如下所示:

public class User {
private String name;
private int age;

// name 的 getter 方法
public String getName() {
return name;
}

// name 的 setter 方法
public void setName(String name) {
this.name = name;
}

// age 的 getter 方法
public int getAge() {
return age;
}

// age 的 setter 方法
public void setAge(int age) {
this.age = age;
}

}

現在很多專案流行使用 Lombok 函式庫簡化 POJO 的編寫。以下是使用 Lombok 簡化後的 User 類別,使用 @Data 來自動產生 getter、setter 等方法:

@Data
public class User {
private String name;
private int age;
}

自定義特殊的 get 方法

在某些場景下,開發者可能會在 POJO 中添加自定義的 get 方法以提供額外的功能。例如下列程式碼自定義了一個 getNextSubId 方法來方便取得 下一個 subId。

@Data
static class Order {
AtomicInteger subId = new AtomicInteger(0);

//自定義特殊的 get 方法,方便取得下一個subId
public int getNextSubId(){
return subId.incrementAndGet();
}
}

注意!這個方法並不符合 POJO 中 get 方法的慣例 (Convention),因為調用 getNextSubId方法是有副作用 (side effect) 的,它改變了 subId 的值。

會造成 JSON 序列化問題?

POJO 用來表示資料,所以常被做序列化處理。例如:序列化為 JSON 字串。然而,某些 JSON 序列化函式庫,在序列化成 JSON 字串時,會預設調用 POJO 中所有 get 開頭命名的方法,導致自定義的特殊的 get 方法被調用。

例如:fastjson 1.2.83 版本,在序列化時就會有這個行為,範例程式碼使用 JSON.toJSONString來序列化剛才定義的 Order POJO,如下所示:

Order order = new Order();
String json1 = JSON.toJSONString(order);
System.out.println(json1);
// 輸出: {"nextSubId":1,"subId":{"value":1}}

String json2 = JSON.toJSONString(order);
System.out.println(json2);
// 輸出: {"nextSubId":2,"subId":{"value":2}}

上述程式碼中可以看到每調用一次JSON.toJSONStringsubId 就會「被遞增」,因為在序列化時 getNextSubId 被調用了。程式碼中序列化做了兩次,subId 也從 0 遞增到了 2。

這樣的 bug 可能被隱藏得很深,例如:在某些條件下才會做序列化(見以下程式碼):

if (...) {
....
if (...) {
log.info("order={}", JSON.toJSONString(order));
}
} else {
....
}

Bug 呈現的表象就會是 subId 會神奇地在某些情況下自動遞增。

而 Bug 的起因僅僅是一段 log 程式碼。

小結

看似簡單的 POJO 也可能隱藏著潛在的問題。魔鬼往往藏在細節之中,而這些細節會在最意想不到的時候引爆問題。

要避免這類陷阱,還需要回歸到軟件開發的基本功:

  1. 遵循 POJO 的最佳實踐: get 方法不應該有副作用。如果需要添加特殊功能的方法,應該選擇不同的命名方式 (不要取名 getXXX),避免被 JSON 序列化庫誤解為標準的 getter 方法。不僅提高了程式碼的可讀性,也減少了序列化過程中的意外行為。
  2. 不要心存僥倖: 如上面的例子所示,即使是看似無害的小改動 (如新增一行log),也可能引入難以察覺的 bug。在進行任何修改時,都應該仔細考慮其可能的影響,特別是在涉及序列化、併發操作或跨系統互動的場景中。
  3. 重視單元測試: 單元測試是發現潛在問題的有效方法。測試案例 (Test Case) 應該盡可能覆蓋多個分支條件和邊界情況。
  4. 選擇合適的函式庫: 在使用第三方函式庫 (如 JSON 序列化) 時,要充分了解其行為特性和潛在的陷阱。定期更新這些函式庫以獲得 bug 修復和安全修補 (patch) 也很重要。
  5. 程式碼審查 (Code Review) 和持續學習: 建立嚴格的程式碼審查流程,可以幫助團隊成員互相學習,發現潛在的問題。同時,保持對新技術、最佳實踐和常見陷阱的學習。

看似老生常談,但在軟體開發的道路上,這些看似基礎的原則往往是確保系統穩定性的關鍵。

若是喜歡我分享的內容,歡迎幫我按個拍手,可拍 50下,給我一點鼓勵,或是加入我的粉絲團《程式猿吃香蕉🍌,一起分享軟體知識與心得!

--

--

Jayden Lin
程式猿吃香蕉

曾在 Yahoo 擔任 Lead Engineer,負責廣告系統,帶團隊做跨國開發,現任職區塊鏈產業。也是《程式猿吃香蕉》團隊創辦人,喜歡將實用的軟體知識以簡單生動的方式講給大家聽 😄😄😄