微服務開發系列:認識到序列化的重要性

zxdposter發表於2022-11-24

原始碼地址

微服務開發系列:開篇
微服務開發系列:為什麼選擇 kotlin
微服務開發系列:為什麼用 gradle 構建
微服務開發系列:目錄結構,保持整潔的檔案環境
微服務開發系列:服務發現,nacos 的小補充
微服務開發系列:怎樣在框架中選擇開源工具
微服務開發系列:資料庫 orm 使用
微服務開發系列:如何列印好日誌
微服務開發系列:鑑權
微服務開發系列:認識到序列化的重要性
微服務開發系列:設計一個統一的 http 介面內容形式
微服務開發系列:利用異常特性,把異常納入框架管理之中
微服務開發系列:利用 knife4j,生成最適合微服務的文件

1 先說結論

在該框架中,不再使用 fastjson 作為序列化工具,全面使用 jackson。

作為對 fastjson 靈活性的補償,在 framework:cn/framework/common/jackson 路徑下,提供了 JacksonJacksonObjectJacksonArray 三個類作為代替,基本保留的 fastjson 的操作習慣,不用自己新建 ObjectMapper,而且比原先的 fastjson 提供的類更為靈活,功能也更加強大。

2 為什麼不使用 fastjson

序列化可能在單個專案中被認為不是多麼重要的事情,這也造成很多開發人員被 fastjson 迷惑了,認為序列化不就是一個簡單的透過 get set 方法去處理 json 資料的方式嗎,最多多一個複雜類巢狀的處理。

但是當你使用解決過 spring boot 對日期處理的型別問題時,你會發現 fastjson 中的配置是不生效的。

當你使用了一個列舉類在裡面加上一些複雜的建構函式時,你會發現 fastjson 糟糕的使用體驗。

如果你希望使用 fastjson 代替 spring boot 到 cloud 架構中的所有需要使用序列化的地方,我只能說基本上不太可能,即使勉強替換上了,也不知道哪天會出現問題,具體的情況在文章《為什麼不應該再使用FastJson》中。

因此,在該架構中,禁止使用除了 jackson 以外的序列化工具。

就算在 Alibaba 的專案 nacos 裡面也是使用的 jackson。

3 序列化在微服務框架中的一致性

作為微服務,服務多是不可避免的,那麼服務與服務之間通訊資料的一致性尤為必要,總不能 A 服務說法語,B 服務說德語,通訊還要叫上一個翻譯官。

你肯定希望看到一個資料在 A 服務序列化完畢之後,在 B 服務能夠順利的反序列化成目標物件。

這僅僅是服務與服務之間,還有記憶體與 redis 之間,還有記憶體 > spring security > redis,還有記憶體 > redis > rpc > 記憶體,等等情況。

所以請意識到序列化一致性的重要性,不要給開發增加多餘的負擔。

為了做到一致性,框架內的三個類就能解決大多數問題

  1. framework:cn.framework.config.jackson.JacksonConfig
  2. framework:cn.framework.config.jackson.RedisSessionConfig
  3. framework:cn.framework.config.redisson.RedissonConfig
補充 ,spring boot 2.7 版本出現了變化,需要 cn.business.foundation.config.jackson.MvcJacksonConfigurer 這個類,來使自定義的序列化配置生效。

3.1 JacksonConfig

JacksonConfig 利用了 spring boot 框架中的 Jackson2ObjectMapperBuilder,用過 jackson 的都知道,使用 jackson 需要生成 ObjectMapper,這個類就是幫助生成的工廠類。

spring cloud gateway 剛好支援Jackson2ObjectMapperBuilder,所以節省了一部分程式碼。

在這個類裡面,還配置了 spring.jackson.date-format,作為統一的時間格式配置,預設為 yyyy-MM-dd HH:mm:ss

針對時間的處理,在處理 elasticsearch 的多時間格式支援啟發,還引申出了多時間格式處理類 MultipleLocalDateTimeSerializer

它的作用是能夠配置多個時間格式,能夠將時間格式的字串,對格式進行解析,如果第一個格式失敗了,就嘗試下一個。

如果有什麼特殊的類需要做反序列化配置,可以在這裡增加。

3.2 RedisSessionConfig

RedisSessionConfig 配置了 spring security 儲存 session 到 redis 的序列化類 RedisSerializer,所用到的 ObjectMapper 也是來自於 JacksonConfig 配置的 Jackson2ObjectMapperBuilder 生成的。

在這裡你可以看到使用了 SecurityJackson2Modules 這個類,這是 spring security 預設提供的,支援將 spring security 中的一些安全類反序列化的模組,很方便。

RedisSessionConfig 也註冊了 UserDeserializer 這個反序列化類,反序列化了 User,擴充套件自 spring security User 類,參考自 org.springframework.security.jackson2.UserDeserializer,如果還需要擴充套件使用者屬性,在 User 上擴充套件,並且在 UserDeserializer 中做相應的設定即可。

3.3 RedissonConfig

此類是對 redisson 的配置類。

對序列化的配置是這一行程式碼 Codec codecIns = new JsonJacksonCodec(jackson2ObjectMapperBuilder.build());,目的是使用系統中配置的 ObjectMapper 進行序列化的操作,這樣就能夠保持統一。

4 序列化泛型

由於 java 中泛型擦除的問題存在,在處理巢狀的複雜的型別物件時,一般的手段都會失效。

例如下面這段程式碼,如果將 test 轉換為 json string,再轉換回 List<Map<String, User>> 就會遇到擦除的問題。

    data class User(
        val name: String,
        val age: Int
    )
    val test = mutableListOf<Map<String, User>>()
    test.add(mapOf("a" to User("a", 10)))

對此 fastjson 和 jackson,都有著相似的解決方法,那就是利用 TypeReference 來讓泛型在生成時就被固定下來,框架中封裝的 Jackson 提供了這種方法。

Jackson.parseJavaObject(test.jsonString(), object : TypeReference<List<Map<String, User>>>() {})

只能透過這種抽象類的方式,在生成時確定泛型的型別。

但是 jackson 除此之外,還提供了一種更加靈活的方式,JavaType

val mapType: JavaType = TypeFactory.defaultInstance().constructParametricType(Map::class.java, String::class.java, User::class.java)
    val type: JavaType = TypeFactory.defaultInstance().constructParametricType(List::class.java, mapType)
    Jackson.parseJavaObject(test.jsonString(), type)

它能夠讓你動態生成複雜泛型型別,這也是 jackson 比 fastjson 強大的地方之一。

如今由於使用了 kotlin,泛型有了更好的處理方式。

Jackson 類中,擴充套件了 convert 的方法,該方法利用了 kotlin 的 reified 關鍵字,來處理泛型擦除的問題

inline fun <reified T> Any.convert() = Jackson.convert(this, object : TypeReference<T>() {})

fun <T> convert(value: Any, typeReference: TypeReference<T>): T {
            if (value is String) {
                return OBJECT_MAPPER.readValue(value, typeReference)
            }
            return OBJECT_MAPPER.convertValue(value, typeReference)
        }

這個方法能夠極大的方便泛型處理,今後的型別轉換,只需要一行簡單的程式碼

val result: List<Map<String, User>> = test.jsonString().convert()

相關文章