背景
目前在国内环境中,应该是使用 FastJson 比较多,从国内的搜索到的文章教程中,大部分都是用的 FastJson,由此可见在国内的普及度还是较高的。
我一开始也是完全使用 FastJson 作为自己的主力库,主要有以下几点原因:
- 国人开发,国内使用的人较多,搜索起来比较方便。
- 使用起来简单方便,用起来只需要关心 JsonObject、JsonArray 两个类。
- 类型转换省心。
- Github 上 star 数很多。
- 一开始被人写文章安利,描述 FastJson 在很多场景下性能第一。
但是使用了一段时间后发现,FastJson 与其说好用,倒不如说在一些简单的小项目下使用起来较为方便,避免繁琐了的不少配置。
但是一旦项目多了起来,各个模块之间需要配合的时候,就避免不了对序列化框架进行配置,此时 FastJson 的短板开始暴露出来了。
最近研究许多了微服务的架构方面的事情,用到了 spring cloud gateway,spring boot security,spring boot data session,spring boot redis,等多个模块,从这些模块中可以看出来,很多东西都需要对数据进行序列化反序列化。
不使用的理由
1、表现一致
spring 家族默认都是使用 jackson 去做序列化工作的。
spring cloud gateway 中使用的 webflux 对数据的序列化反序列化,你希望在 spring boot mvc 中有同样的表现,包括在你的代码里面,肯定需要针对这两种情况进行统一的配置,能够在时间格式、对 Null 值的处理、枚举类型上,有相同的表现。
如果你是这么想的,并且去付诸行动,那么面临你的就是必须去同时修改 spring cloud gateway 和 spring cloud mvc 的序列化配置。
其中的代码量不必多说,要面临的还包括稳定性兼容性等一系列的问题,这些问题都需要长时间的运行测试,来保证稳定性。
而使用 jackson,希望做到一致性配置,只需要把下面的代码放在每个模块里面,利用自动装载生效,不管是 webflux 还是 webmvc 都可以使用。
@Configuration
public class JacksonConfig {
@Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}")
private String dateFormatter;
@Bean
@Primary
Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {
JavaTimeModule module = new JavaTimeModule();
LocalDateTimeDeserializer localDateTimeDeserializer = new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(dateFormatter));
LocalDateTimeSerializer localDateTimeSerializer = new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(dateFormatter));
module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer);
module.addSerializer(LocalDateTime.class, localDateTimeSerializer);
return new Jackson2ObjectMapperBuilder()
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.findModulesViaServiceLoader(true)
.modulesToInstall(module);
}
}
上面的代码能够生效的关键就是,spring 架构里面,已经默认实例化出了 ObjectMapper
这个 bean,这个 bean 有且只有一个。
2、在 spring boot security 中的兼容问题
上面说到 spring 默认都是使用 jackson,其中的 security 也不例外。
用到 spring boot security,最好是搭配 spring boot session 和 spring boot redis,这样能够将令牌直接存储 redis,但是默认情况下,序列化到 redis 中是使用 java 默认的序列化方式,但是这种方式无法直接给人查看,所以要自定义一个 RedisSerializer
去实现。
如果你使用 FastJson,有些类是没法直接序列化成实体的,比如 UserDetails 此类,security 中为了安全起见,实例化之后是没有 set 方法的,序列化只能通过构造函数来进行。
这样直接的使用 FastJson 的行不通的,只能自己来做去写相应的代码去做。
但是 security 依赖里面,直接针对 jackson 提供了 SecurityJackson2Modules
这个类,把所有你可能用到的 security 对象序列化方法都提供了。十分方便。
你还能自己仿造里面的类自己去实现扩展 UserDetails,只需要去模仿 UserMixin
和 UserDeserializer
这两个类的写法,自己写一个 modules 即可。
3、FastJson 不严谨的漏洞
这里的漏洞并非是安全上的漏洞,而是逻辑上的。
比如,我希望某个枚举类是通过自定的构造函数传入值进行序列化的,而不是通过枚举中的 valueOf 用 enum 的 ordinal 或者 name 去做。
FastJson 不是没有方法去做,而是有非常让人很难理解的低级错误存在。
我定义了一个枚举类 Status。
在枚举类上使用@JSONType(deserializer = xxx.class)
把枚举类当成字段放在其它类 MyClazz 中,字段上使用@JSONField(deserializeUsing=xxx.class)
这两种方法全部用上,再使用 JSONObject.toJavaObjec(jsonObject,
`MyClazz.class`)
,依然不会调用 xxx.class,的反序列化方法。
但是使用 JSONObject.parseObject(jsonObject.toJSONString(), MyClazz.class)
,却能够反序列化。
我跟踪过 FastJson 的源码,确实是逻辑上的疏忽,不是什么必要的原因忽略的。
更具体的原因可以去看一下 FastJson 中 TypeUtils
的源码。
这个问题,在 jackson 中完全是不存在的,只需要在枚举类中添加一个序列化和另外一个反序列化方法,分别加上注解 @JsonCreator
和 @JsonValue
,不仅不存在,使用起来还极为方面,不需要额外的类,并且在任何地方都能生效。
这种逻辑上的漏洞,出现在一个使用人数这么多的项目身上,有点不太应该。
4、FastJson 的代码质量不太好
这一点同样在源码中可以得到印证。
因为要做背景中提到的工作,时常翻看源码是肯定的,我发现 FastJson 中比较有代表性三个类。JSON、JSONObject、JSONArray
这三个开发者最为常用的类,里面的源码感觉到有一些割裂。
在 JSON
最新的代码上,你能看到类的上面有板有眼写了不少注释,往下看,就能发现方法写了大量的注释,却有不少方法一点注释都没有。
而且 JSONObject
和 JSONArray
就更为拉胯了,你甚至找不到什么注释。虽然说它们使用起来比较简单,但是一点注释都没有,有点太说不过去了。
从上面我大概能推断出来一些问题,在一开始的版本中,注释工作,代码质量都比较注重一些,但是随着时间过去了,人员的变动可能比较大,主导人员渐渐放松了对代码质量的要求,逐渐演变成了今天这个样子。由此可见维护工作需要从一而终。
5、FastJson 的设计上有缺陷
此处设计上的缺陷是我与 jackson 比较后得出的,因为我没有接触过其它的 json 库。
FastJson 的思想比较简单,就是把所有的 Object 看做嵌套 HashMap,所有的 Array 看成 ArrayList。
在 FastJson 中,你只能看到一些简单的类型,比如 string、int、double、object 等,虽然这些基础类简单,使用起来也更加容易接受,但是问题是考虑细节不够全面。比如对 null 的处理,空的处理。这种方式也没法为程序带来更多的扩展性,在基础类的基础上去扩展,只能通过多加代码去判断,而不能通过面向对象的方式去解决问题。
反观 jackson 中,所有的基础类都被包装了起来,string 对应 TextNode,int 对应 IntNode,null 对应 NullNode 等等,这样虽然使用起来麻烦,但是带来的好处确实更加多了。其中一点就是不同类型之间的相互转化,明显扩展性起来更加灵活。
使用 jackson 代替 FastJson
虽然 FastJson 不推荐使用,但是要承认的是,用起来确实非常省心方便,开箱即用,用起来也比较轻量。
不过这个优点也是能通过封装 jackson 来达到同样的目的。
具体思路就是用类似用代理的方式,定义 FastJson 中的方法,但是内部实现方式却是用 jackson。
这里提给我的项目地址,里面已经对 jackson 做了类似于 fastsjon 封装,并且提供了一些比 fastjson 更方便的函数做补充。如果对 ObjectMapper
最后
说到这里,一句话就是,使用 jackson 确实比 fastjson 要更加好。
这里再说几点我自己发现的小细节
- FastJson 项目很多地方并没有遵守 alibaba 的代码规范,不管从命名 JSONObject 还是里面的源码。
- alibaba 内部项目似乎也没有使用 FastJson,我引用了 nacos 和 arthas 两个项目,都没有发现使用 FastJson 的痕迹。使用的也是 jackson,在
nacos-common
包中的JacksonUtils
类中。 - alibaba 的开源项目并没有外人吹捧的质量那么高,我最近发现相同的一个大项目 nacos 中两个不同的小模块,一个是配置,一个是日志,源码里面读取配置的方式都不太对。并且 nacos 没有考虑过多个项目同时使用 nacos,运行在同一个环境下面,日志路径冲突的问题,我最后只能通过环境变量里面的 PID 去区分目录。