微服务开发系列:开篇
微服务开发系列:为什么选择 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()
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。