微服务开发系列:认识到序列化的重要性

源码地址

微服务开发系列:开篇
微服务开发系列:为什么选择 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> 就会遇到擦除的问题。

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

对此 fastjson 和 jackson,都有着相似的解决方法,那就是利用 TypeReference 来让泛型在生成时就被固定下来,框架中封装的 Jackson 提供了这种方法。

Jackson.parseJavaObject(test.jsonString(), object : TypeReference>>() {})

只能通过这种抽象类的方式,在生成时确定泛型的类型。

但是 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  Any.convert() = Jackson.convert(this, object : TypeReference() {})

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

这个方法能够极大的方便泛型处理,今后的类型转换,只需要一行简单的代码

val result: List> = test.jsonString().convert()

你可能感兴趣的:(微服务开发系列:认识到序列化的重要性)