json序列化框架那些事
背景
json序列化和反序列化是我们日常开发中最常接触的工作。本文主要通过分析几个主流json框架的实现原理,分享下关于json的一些技术思考:
- 如何实现一个json框架
- 我们能从各大主流json框架中学到什么
如何实现一个json框架
核心功能
实现一个json序列化框架需要提供的最核心能力是:
- json string和对象之间的互相转换(对象转换):这个一般也是最常用的,需要用户预定义好类用来反序列化json string
- json string和语法树对象之间互相转换(语法树转换):这个一般用在有很强的自定义json处理需求的场景,例如对节点涉及过滤、聚合、排序等复杂操作的场景,直接使用json框架抽象好的对象,像jackson的JsonNode,可以更加细致的控制json节点。由于json本身构词的特点,一般基于递归下降的解析方式处理。
序列化实现思路
根据类型初始化特定的序列化器进行序列化即可
反序列化两种实现思路
- 流式处理:按照流式处理json中的一个个字符,优点是对于大规模的json可以按照流式处理,一部分一部分处理,而不是全部加载到内存导致内存溢出。使用起来会复杂些。一般是框架直接定义好token以及解析token的parser,然后将parser交给用户,然后用户自己遍历json field。像Spring中AbstractJackson2HttpMessageConverter就使用了jackson的流式API
- 整体对象处理:我们日常中比较常用,整体对象一起解析,实现起来也会简单些。一般步骤如下:
- 识别值对象:输入的json根据预json构词规则识别特定标识符,从而确定值对象
- 根据预定义好的parser解析对象:直接根据预定义好的parser配合value type直接解析并返回结果。可以返回具体的类对象也可以是JsonNode,因为本质上JsonNode也是一个类,只不过提供了更底层处理节点的能力。
其他能力
真正实现一个生产可用的json库其实还需要库支持很多能力,他们同样非常重要,例如:
- 序列化、反序列化配置:为了满足不同的序列化需求,需要提供一系列的配置选项,例如日期格式、空值处理、循环引用处理、类型处理等。
- 性能:例如缓存、字节码生成、对象池、流式处理等技术来提升性能
- 易用性:注解能力、接口设计
- 自定义能力:提供丰富的扩展点、自定义能力
- 功能扩展:包括生态整合、更多格式支持(XML、YML等)
- 安全:反序列化是风险很高的操作,例如基于java autotype漏洞在构造函数中嵌入危险代码就是一种常见的反序列化引发的安全漏洞
主流json框架的实现原理
jackson
序列化
主要由包含writer能力的JsonGenerator配合serializer完成对象的序列化。例如常见对象的序列化可以看BeanSerializer的代码
反序列化
Jackson应该是java生态最为成熟和使用面广的json框架了。我们按照核心流程来简单分析下他的实现原理。
- 整体对象处理:这部分实现可以在ObjectMapper那查看。用户会传递具体的class type,然后框架利用反射确定类型,配合按照类型定义好的parser即可完成解析。
- 流式处理:主要代码在JsonFactory中,基于Reader流式读取char解析token,具体参考ReaderBasedJsonParser类
jackson当中的可以学习的技术点
- 缓冲机制:尽量利用缓存机制避免开销大的读写以及创建。例如序列化器、反序列化器、generator对象等等。
- 节制的反射使用:可以用get方法获取字段就不用反射。例如BeanPropertyWriter当中缓存getter、setter信息避免重复调用
- 扩展性:提供诸多扩展点,例如序列化器、反序列化器、注解等
- 高性能的数据结构:SegmentedStringWriter通过分段存储来避免内存拷贝,处理大对象频繁的拼接字符串是个比较高性能的实现�
fastjson
fastjson是以快为特色的json框架,在系统设计上引入了不少针对性能优化的措施,例如ASM、fnvhash等,性能的提升带来了系统复杂性的提升,也为后续安全漏洞埋下了一些隐患。fastjson序列化和反序列化的大体过程也是通过自定义的序列化和反序列化器结合类型去处理对象。最核心功能实现上是比较接近的,但是细节上还是有很多差异。这边主要说下他的优缺点(相比jackson)和性能优化相关的技术吧。
缺点
- 注释: 这个jackson确实更胜一筹,可以翻下代码会有直观感受
- **漏洞问题: **虽然jackson也有漏洞问题,但是fastjson的漏洞问题主要是爆出更加频繁和影响范围更大,因此这对其口碑影响也比较大
- json标准遵循度: 为了兼容阿里自身业务场景有一些tricky的做法,一些序列化行为和规范做法有差异。例子可以参考[5]
- 社区弱于jackson:主要是作者一个人维护,毕竟一个人精力有限。jackson有着非常成熟的社区和高质量的参与者,这也一定程度上影响了两个库的发展
优点
- 相对出色的性能:fastjson在json处理上做的一些性能优化手段仍然是值得我们学习的
可以学习的性能优化手段
特别的性能优化手段包括:
- 缓存:缓存class、field、method等来降低反射开销
- fnv64位hash算法:可用于对象比较、查找,速度快并且可以减少内存分配。不过一般用在缓存场景好些,因为会用碰撞。PS. 实际场景建议自己测试下性能不要随意用这个优化。我测了下,像String默认的hashcode反而性能很好。这个优化酌情看待吧。
- int/long序列化优化:默认使用toString转成String会有额外的内存拷贝和开销。针对这种整形可以直接转成char写到输出流提升性能
- threadlocal避免内存分配:保存一个byte[]和char[]减少内存分配
- int/long反序列化优化:
- 字符串编码性能优化:主要优化String转byte[]的效率,默认getBytes效率不高。如果确定是unicode编码的,可以考虑sun.nio.cs.ArrayEncoder的实现。基本原理就是已知编码的情况下可以特化的处理,比如采用查找表、批处理(针对UTF8每次处理16个字节)等等。
- float/double序列化优化:不使用Jdk默认的parse,toString之后也是直接将字节数组写到输出流中
- ASM字节码生成避免反射调用:反射开销较大,fastjson通过ASM字字节码生成避免了通过反射来获取和设置值,这个是和jackson很大的差别。另外像对象数组也是用ASM技术直接转字节数组再转json数组有更高的效率
TIPS:基于代码生成对于其他AOT编译的编程语言来说也是实现序列化框架的重要手段,例如rust的serde�
反序列化与安全漏洞
反序列化不可信的内容本身就属于非常不安全的行为。json框架之前很多RCE的漏洞主要都是因为反序列化autotype转换引发。基本上就是反序列化了一个不可信的实例,然后在构造函数内就可以埋藏攻击代码。现在基本上都是通过白名单黑名单来减少这种风险,如果有条件的话使用auto type白名单是一种比较安全的实践。
参考资料
- [1] 自己动手实现一个简单的JSON解析器
- [2] fastjson-source-code-analysis
- [3] 初识Jackson – 世界上最好的JSON库
- [4] 妈呀,Jackson原来是这样写JSON的
- [5] fastjson 这么快,老外为什么还是热衷 jackson?
- [6] [享学Jackson] 二十二、Jackson与Fastjson的恩怨情仇(完结篇