關於對於Java中Entity以及VO,以及DTO中Request物件序列化的學習

海山了-發表於2024-05-25

關於 Serializable的探討

前提引入

是由於軟體測試上有同學提到說,什麼該欄位在程式剛執行時,導致jvm激增,所以吸引了我的注意

回顧程式碼

MybatisPlus Generator自動生成的entity中就經常帶有這個,
而且我在開發程式碼的時候VO,以及DTO常常是直接複製對應的entity,所以也保不齊我對應的VO等物件也保留了這個(慚愧表情包),印象中之前就學過,不過忘得差不多了,所以就於此複習一下

問題

  1. 為什麼要有 Serializable
  2. 其中的private static final long serialVersionUID = 1L;又有什麼用?
  3. 跟序列化有關?是跟具體的某種序列化有關?還是全部?

回答

one,two

在Java中,implements Serializable和serialVersionUID是與物件序列化(serialization)相關的概念,特別是在需要將物件的狀態持久化、傳輸或者快取時會用到。下面詳細解釋這些概念以及在MyBatis-Plus中它們的使用場景。

  1. Serializable 介面
    Serializable 是一個標記介面(marker interface),沒有任何方法定義,它的存在只是為了告訴JVM某個類的例項是可以被序列化的。序列化的主要用途包括:

持久化:將物件的狀態儲存到一個儲存介質(如檔案、資料庫)中,以便後續可以恢復。
傳輸:透過網路傳輸物件,比如在分散式系統中,從一個JVM傳遞到另一個JVM。
快取:將物件放入快取中,以便於快速恢復物件狀態。

  1. serialVersionUID 欄位
    serialVersionUID 是一個靜態的、最終的(final)長整型欄位,用於標識序列化的版本。其作用如下:

版本控制:在反序列化時,JVM會檢查傳入的位元組流中的serialVersionUID是否與本地對應類的serialVersionUID一致。如果一致,說明序列化的版本是相容的,可以安全地進行反序列化。如果不一致,會丟擲InvalidClassException,防止版本不相容帶來的問題。
手動定義serialVersionUID可以避免某些情況下因類的微小變動(如增加一個方法或欄位)導致的反序列化失敗。一個常見的定義方式是:

private static final long serialVersionUID = 1L;

three

對於不同的序列化機制,如JSON序列化、資料庫儲存以及其他的自定義序列化方案,serialVersionUID 並不起作用。下面詳細討論serialVersionUID在不同場景中的作用和侷限性,以及其他場景中的序列化方式。

  1. JDK 自帶的序列化器
    serialVersionUID 是Java內建的序列化機制的一部分,特別是用於 ObjectInputStream 和 ObjectOutputStream 時起作用。當使用這些流進行物件序列化和反序列化時,JVM會檢查類的 serialVersionUID 是否匹配,以確保類的版本一致性。
  2. JSON 序列化
    在使用JSON(如透過 Gson、Jackson 或者其他庫)進行序列化和反序列化時,serialVersionUID 並不起作用。這是因為JSON序列化器將物件轉換為JSON字串,並不保留Java類的版本資訊。因此,在這種情況下,不需要 serialVersionUID。例如:
import com.fasterxml.jackson.databind.ObjectMapper;

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

    // Getters and Setters
}

ObjectMapper objectMapper = new ObjectMapper();
User user = new User();
user.setName("John");
user.setAge(30);

// 序列化
String jsonString = objectMapper.writeValueAsString(user);

// 反序列化
User user2 = objectMapper.readValue(jsonString, User.class);
  1. Redis 儲存
    當使用Redis進行資料儲存時,通常也會使用JSON字串進行序列化和反序列化。因為Redis是一個鍵值儲存,儲存的是序列化後的資料字串,而不是Java物件本身,因此 serialVersionUID 並不起作用。
  2. 資料庫儲存
    對於MySQL等關係型資料庫,當物件被儲存時,ORM(如MyBatis、Hibernate)會將物件的欄位提取出來並生成相應的SQL語句進行儲存和查詢。在這種情況下,物件序列化是由ORM框架處理的,serialVersionUID 也並不起作用。

serialVersionUID 的適用場景
綜上所述,serialVersionUID 的適用場景主要侷限於Java內建的序列化機制。當你在分散式系統中使用Java物件的原生序列化和反序列化時,serialVersionUID 可以確保不同版本的類之間的相容性。如果你的應用程式不使用Java內建的序列化機制,而是使用JSON、XML或其他格式進行序列化,那麼 serialVersionUID 並不需要關注。

舉個我在專案中遇到的例子

CountMinSketch

當時我在實現一個OJ系統,其中有個類似github,leetcode的提交記錄等等的情況,我是懶得放到個人主頁,於是我直接放到首頁中,其中對應的資料該在後端中怎麼存呢?

我聯想到了bitmap,可惜他只能是二值,而我們需要保留提交記錄中一天提交了多少次呀,所以是不可行的,那麼bitmap不行,沒有其他的資料結構能同樣省空間了嗎?有那就CountMinSketch,但是他是機率資料結構,也就是說可能會有誤差(也就是誤差率以及誤差距離越小那麼消耗更多的空間,底層思路實現和bloomfilter類似)
這是我當時寫的小測試:

public class TestCountMinScratch {
    public static void main(String[] args) {
        CountMinSketch sketch = new CountMinSketch(0.001, 0.99, 1);

        // 對資料進行更新
        sketch.add("2024-5-16",1);
        sketch.add("2024-5-16",1);
        sketch.add("2024-5-16",1);

        // 查詢頻率
        long frequency = sketch.estimateCount("2024-5-16");
        System.out.println("Frequency of 'example': " + frequency);
    }
}

這個是當時最後沒用上的程式碼,最後選擇用了hash結合一定的編碼來進行處理,考慮到通常展示的365個天數的提交記錄應該也不會很耗時間,又能具有準確性
下面是之前使用CountMinSketch的程式碼:

@Component
public class CountMinSketchFactory {
    /**
     * 誤差率:確保與最大為真實值*(1+epsOfTotalCount)
     * 最小同理
     */
    private final static double epsOfTotalCount=0.07;
    /**
     * 置信度:置信度為 0.99 意味著我們希望在 99% 的情況下,查詢估計的誤差在指定的誤差率範圍內。
     * 也就是99%的情況在上面我們推出的範圍中
     */
    private final static double confidence=0.99;
    /**
     * 在隨機數生成和某些機率資料結構中(如 CountMinSketch),種子(seed) 是一個初始值,用於初始化隨機數生成器或雜湊函式。它的作用是確保隨機過程在相同的種子下每次執行都產生相同的結果。
     */
    private final static int seed=1;
    @Autowired
    private BitmapRedisTemplate bitmapRedisTemplate;
    public CountMinSketch getCountMinSketch(Long uid) {//todo:這裡是否適合雙檢加鎖?
        String s = bitmapRedisTemplate.opsForValue().get(RedisConstant.USER_COMMIT_STATICS + uid);
        if(s==null) {
            return new CountMinSketch(epsOfTotalCount, confidence, seed);
        }else{
            return JSONUtil.toBean(s, CountMinSketch.class);
        }
    }
    public void storeCountMinSketch(Long uid,CountMinSketch countMinSketch) {
       bitmapRedisTemplate.opsForValue().set(RedisConstant.USER_COMMIT_STATICS + uid, JSONUtil.toJsonStr(countMinSketch));
    }
}

對於這個情況我原本是打算使用redis來儲存的,畢竟是微服務專案,於是我開始兩個方案進行對比,分別是專案應用場景1年內的情況以及放大到10年內的提交次數,在序列化到redis之後,卻發現countminSketch沒有任何變化,原先我還覺得這資料結構這麼厲害,結果開啟資料一看,什麼都沒有,這可能也正是json序列化與jdk序列化的區別吧,而且在這次測試中hash在時間消耗上差距並不大,所以選用了hash

最後回到前提

每個serialVersionUID都是靜態且final修飾,而且他們也不會被GC所清理,而且消耗空間也不會特別大,除非類爆炸現象,可能當時我沒注意聽,不然這個現象不可能是一個軟體測試的問題,對了有必要的話,還是保留著,畢竟難免可能之後會用到
最後再提一嘴像那些dto什麼的以及vo什麼的就不需要了,因為你根本不會用到把他們當做一個物件傳

相關文章