微服務開發系列:開篇
微服務開發系列:為什麼選擇 kotlin
微服務開發系列:為什麼用 gradle 構建
微服務開發系列:目錄結構,保持整潔的檔案環境
微服務開發系列:服務發現,nacos 的小補充
微服務開發系列:怎樣在框架中選擇開源工具
微服務開發系列:資料庫 orm 使用
微服務開發系列:如何列印好日誌
微服務開發系列:鑑權
微服務開發系列:認識到序列化的重要性
微服務開發系列:設計一個統一的 http 介面內容形式
微服務開發系列:利用異常特性,把異常納入框架管理之中
微服務開發系列:利用 knife4j,生成最適合微服務的文件
1 先說結論
在該框架中,不再使用 fastjson 作為序列化工具,全面使用 jackson。
作為對 fastjson 靈活性的補償,在 framework:cn/framework/common/jackson
路徑下,提供了 Jackson
、 JacksonObject
、 JacksonArray
三個類作為代替,基本保留的 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 > 記憶體,等等情況。
所以請意識到序列化一致性的重要性,不要給開發增加多餘的負擔。
為了做到一致性,框架內的三個類就能解決大多數問題
framework:cn.framework.config.jackson.JacksonConfig
framework:cn.framework.config.jackson.RedisSessionConfig
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()