一個 List.of 引發的“血案”

張哥說技術發表於2023-11-13

一個 List.of 引發的“血案”

來源:阿里雲開發者

阿里妹導讀

本文作者將分享一個使用List.of後掉進的坑以及爬坑的全過程,希望大家能引以為戒同時引起這樣的意識:在使用新技術前先搞清楚其實現的原理。

隨著卓越工程的推進,很多底層技術的升級迭代被正式投入使用,例如 JDK11 的升級。然而,當我們擁抱變化,欣喜地使用一些新特性或者語法糖的同時,也有可能正在無意識的掉入一些陷阱。
本篇文章,我將分享一個使用List.of後掉進的坑以及爬坑的全過程,希望大家能引以為戒同時引起這樣的意識:在使用新技術前先搞清楚其實現的原理。

案發現場

一句話總結:在一次後端釋出的變更後,前端解析介面返回的格式失敗。

前情提要:

  • 後端 JAVA 應用 JDK 版本11,提供 HSF 服務端介面。

  • 前端透過陸游平臺(一個 Node 視覺化邏輯編排的平臺)配置介面,內部透過 node 泛化呼叫後端的 HSF 介面,平臺解析返回介面結果。

過程回顧:

  1. 後端釋出的變更示意:


















// 釋出前public List<String> before(Long id) {    ...    if (...) {        return null;    }    ...}
// 釋出後public List<String> after(Long id) {    ...    if (...) {        return List.of();    }    ...}

這裡的核心變化點就是將預設的返回從 null 改成了 List.of() 。

為什麼可以這麼改?已知前端對null和空陣列[]做了同樣的相容邏輯。
  1. 前端獲取到介面的格式變化:










// 釋出前{  "test": null}// 釋出後{  "test": {        "tag": 1    }}

這個結構的變更直接導致了前端後續的欄位結構解析失敗,因為理論上 test 欄位需要提供一個陣列的格式(也可以是null),但是實際變成了一個物件。
所以整個環節中最離奇的是:為什麼我的List.of在前端呼叫返回的介面中變成了一個帶有tag欄位的物件,它到底經歷了怎麼樣的轉換過程?

案情推理

List.of 觸發的離奇現象讓我不得不重新審視它,一步步看下它的原始碼實現。


1. 初窺門徑:List.of
















public interface List<E> extends Collection<E> {    /**     * Returns an unmodifiable list containing zero elements.     *     * See <a href="#unmodifiable">Unmodifiable Lists</a> for details.     *     * @param <E> the {@code List}'s element type     * @return an empty {@code List}     *     * @since 9     */    static <E> List<E> of() {        return ImmutableCollections.emptyList();    }}

從官方註釋中得到3點結論:

  1. 這是一個 JDK9 之後的特性;
  2. 返回的是一個不可修改的陣列;
  3. 底層實現使用的 ImmutableCollections 的 emptyList 方法,而 ImmutableCollections 這個類是一個不可變集合的容器類;

2. 漸入佳境:ImmutableCollections.emptyList



























class ImmutableCollections {        static <E> List<E> emptyList() {        return (List<E>) ListN.EMPTY_LIST;    }
    static final class ListN<E> extends AbstractImmutableList<E>            implements Serializable {
       // EMPTY_LIST may be initialized from the CDS archive.        static @Stable List<?> EMPTY_LIST;
       static {            VM.initializeFromArchive(ListN.class);            if (EMPTY_LIST == null) {                EMPTY_LIST = new ListN<>();            }        }        ...    }
   static abstract class AbstractImmutableList<E> extends AbstractImmutableCollection<E>            implements List<E>, RandomAccess {                ...    }}

到這一步,案件的主人公終於登場了:一個新的類 ListN。但是在這段程式碼中,還有很多隱藏的細節線索:

  1. ListN 是 List 的實現類:ListN 繼承了AbstractImmutableList,而 AbstractImmutableList 實際又實現了List;
  2. ListN 中的靜態變數 EMPTY_LIST 會被初始化為一個空的 ListN 的物件;
  3. emptyList 方法中做了 List 型別的強轉,但是由於JAVA的型別轉換原則,實際仍然返回的是一個ListN物件(這是關鍵線索之一),透過排查過程中發現的阿爾薩斯監控也可以確認這一點:

一個 List.of 引發的“血案”

3. 直擊要害:node的 HSF 解析

陸游平臺調取HSF介面走的是node的泛化呼叫,預設情況下node只能解析一些基礎的java型別,例如List和Map。

一個 List.of 引發的“血案”

一個完整的型別對映表可以檢視:java-物件與-node-的對應關係以及呼叫方法

而遇到這次返回的 ListN,可以確定是這種特殊型別在序列化/反序列化的過程中出現了不同的邏輯導致。

4. 真相大白:ListN的序列化

























static final class ListN<E> extends AbstractImmutableList<E>            implements Serializable {    @Stable    private final E[] elements;
   @SafeVarargs    ListN(E... input) {        // copy and check manually to avoid TOCTOU        @SuppressWarnings("unchecked")        E[] tmp = (E[])new Object[input.length]; // implicit nullcheck of input        for (int i = 0; i < input.length; i++) {            tmp[i] = Objects.requireNonNull(input[i]);        }        elements = tmp;    }      
   private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {        throw new InvalidObjectException("not serial proxy");    }                    private Object writeReplace() {        return new CollSer(CollSer.IMM_LIST, elements);    }}
ListN實現了自定義的序列化方法 writeReplace 和反序列方法 readObject。readObject直接丟擲異常是一個防禦性措施,說明該類直接反序列化會報錯,來保證自己的不可變性。而 writeReplace 表示在序列化寫入的時候替換成另一個物件,在這裡返回的是一個內部的序列化代理物件CollSer(關鍵線索之二)。在例項化這個CollSer物件的時候,傳遞了2個變數:

  • CollSer.IMM_LIST 靜態值 = 1

  • elements 一個空的物件陣列 = new Object[0]





















final class CollSer implements Serializable {    private static final long serialVersionUID = 6309168927139932177L;
   static final int IMM_LIST = 1;    static final int IMM_SET = 2;    static final int IMM_MAP = 3;        private final int tag;
   /**     * @serial     * @since 9     */    private transient Object[] array;
   CollSer(int t, Object... a) {        tag = t;        array = a;    }}
注意這裡見到了我們眼熟的 tag 欄位,另外一個欄位 array 被 transient 標識所以序列化處理過程中會被忽略,這下我們終於知道 tag = 1 是怎麼來的了。

結案陳詞

綜上所述,當後端在HSF介面中使用了 List.of() 做返回,在 node 呼叫 HSF 序列化獲取返回結果時會解析成一個帶有tag欄位的物件,而不是預期的空陣列。這個問題其實想解決很簡單,將 List.of() 替換成我們常用的 Lists.newArrayList() 就行,本質上還是對底層實現的不清晰不瞭解導致了這整個事件。

當然在結尾處,其實還有一個疑點,在 HSF 控制檯除錯這個介面的時候,我發現它的 json 結構是可以正確解析的:

一個 List.of 引發的“血案”

懷疑可能是序列化型別的問題,hsfops 也是用了泛化呼叫,序列化型別是 hessian,可能 node 的序列化型別不一樣,這個後續研究確定後我再補充一下。

最後的反思與大家共勉:對於新技術(或者新特性)的應用一定要先搞清楚內部的實現細節,不然可能出現使用時的大坑。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2994884/,如需轉載,請註明出處,否則將追究法律責任。

相關文章