最近后台总有人搜「fastjson 1.2.84 解析报错」「fastjson 1.2.84 和 1.2.83 区别」。1.2.84 是只修安全问题的小版本,照理说直接换版本号就行。我把 1.2.83 和 1.2.84 放在一起,对同一批写法逐条跑了一遍,结果有两处要注意。
先说结论
- 大部分写法不受影响:普通对象、
TypeReference泛型、不带白名单的@type(两版都拒)、SafeMode、全开 autoType 往返、字段声明父类 +@type子类,两版结果完全一样。 - 会报错的一种:用
addAccept("com.xxx.")这种包前缀白名单,反序列化一个实现了javax.sql.DataSource的类(比如继承 SpringAbstractRoutingDataSource的动态数据源)。1.2.83 正常,1.2.84 报autoType is not support。 - 会变慢的两种:同样是包前缀白名单,以及不开 autoType 时加了
@JSONType的类。带@type的反序列化,JDK 8 上慢约 13 倍和 23 倍,JDK 17 上两种都慢约 28 倍。 - 一个办法解决两个问题:把白名单从包前缀改成完整类名。实测报错消失,耗时也回到和 1.2.83 一样。
- 官方发布说明里没有提这两处变化。
官方改了什么
1.2.83 到 1.2.84 一共 4 个提交,主代码只动了两个文件:
modified +56/-6 src/main/java/com/alibaba/fastjson/parser/ParserConfig.java modified +43/-0 src/main/java/com/alibaba/fastjson/util/TypeUtils.java modified +20/-1 pom.xml added +147/-0 src/test/java/.../autoType/AutoTypeValidationTest.java发布说明列了 4 条修复:拒绝类型名里带:/!;白名单 hash 命中后再比一次原文;包前缀白名单不再放行ClassLoader/DataSource/RowSet这类危险基类;危险类不再在黑名单检查之前被放进缓存。
这次要修的漏洞是 CVE-2026-16723,版本该怎么看,前一篇已经按版本轴梳理过:fastjson 1.2.49 中了 CVE-2026-16723 吗?1.2.84 和 1.2.83 到底差在哪。还在 JDK 7 上的项目,1.2.84 根本加载不起来,另一篇有实测:fastjson 1.2.84 能在 JDK 7 上跑吗?。本文只讲 JDK 8 及以上升级后行为有没有变。
实测方法
- jar 直接从 Maven Central 下载,SHA1 与 Central 上的
.sha1一致:1.2.839ee94951bc107d382519975d04bc950b6c6ab297,1.2.8405773609a8ce27cc9cbfbec0009e02b1a7b83dde。 - 同一份测试代码分别对两个 jar 编译、运行。fastjson 的类缓存是全局静态的,混在一个进程里跑会互相污染,所以每个用例单独起一个 JVM。
- JDK:Zulu
1.8.0_504、Oracle17.0.4.1。
功能对照:17 种写法,4 种结果不同(归结起来是 3 处变化)
| 写法 | 1.2.83 | 1.2.84 |
|---|---|---|
JSON.VERSION | 1.2.83 | 1.2.83(没改,见第 34 注那篇) |
普通对象parseObject(json, User.class)+ 序列化 | ✅ | ✅ |
TypeReference<List<User>>泛型 | ✅ | ✅ |
带@type、默认配置(没开 autoType、没白名单) | ❌autoType is not support | ❌ 同左 |
带@type+addAccept("demo.")包前缀 | ✅ | ✅ |
白名单写内部类的二进制名addAccept("demo.Outer$Inner") | ❌autoType is not support | ✅1.2.84 修好了 |
白名单写内部类的点号名addAccept("demo.Outer.Inner") | ✅ | ✅ |
| 包前缀白名单 + 实现了 DataSource 的类(不开 autoType) | ✅ | ❌autoType is not support. demo.MyDataSource |
| 包前缀白名单 + 实现了 DataSource 的类(开 autoType) | ✅ | ❌同上 |
完整类名白名单addAccept("demo.MyDataSource") | ✅ | ✅ |
| 只开 autoType、不配白名单,反序列化 DataSource 类 | ❌ | ❌ 同左 |
DataSource 类作为普通字段类型(不靠@type) | ✅ | ✅ |
全开 autoType,WriteClassName序列化再读回 | ✅ | ✅ |
@JSONType注解类 +@type(不开 autoType) | ✅ | ✅ |
| SafeMode + 白名单 | ❌safeMode not support autoType | ❌ 同左 |
类型名带冒号demo:User(开 autoType) | ❌ClassCastException(报错很怪) | ❌autoType is not support. demo:User |
字段声明父类Animal,JSON 里@type写子类Dog | ✅ | ✅ |
另外用真实的 Spring 动态数据源测了一次(spring-jdbc 5.3.39,一个继承AbstractRoutingDataSource的类,不开 autoType):
| 白名单写法 | 1.2.83 | 1.2.84 |
|---|---|---|
addAccept("demo.") | ✅ | ❌autoType is not support. demo.DynDs |
addAccept("demo.DynDs") | ✅ | ✅ |
那条报错为什么不好查
1.2.84 拒绝这类类时,报的是autoType is not support. 类名,和「没配白名单」时的报错一字不差。白名单明明写了com.xxx.,报错却像白名单没生效,很容易往「配置没加载上」的方向查。
原因在ParserConfig.checkAutoType新加的这几行(1.2.84 源码第 1436~1444 行):
if(clazz!=null){// matching an accept prefix is not an opt-in for gadget base types, only an// accept entry naming the type in full is; keep scanning for such an entryif(i+1<className.length()&&TypeUtils.isAutoTypeDenyClass(clazz)){continue;}returnclazz;}isAutoTypeDenyClass判断的是ClassLoader/javax.sql.DataSource/javax.sql.RowSet的子类。也就是说,只有白名单写完整类名,才算你明确放行了这个类,包前缀不算。这是安全上有意的收紧,发布说明第 3 条说的就是它。
性能对照:两种写法慢了一个数量级
同一条 JSON 反序列化 100 万次,先热身 20 万次,取每次平均耗时。每组单独起 JVM,跑 3 轮取中位数,单位纳秒(ns)。这是单机微基准,看倍数,别看绝对值。
| 写法 | JDK 8:1.2.83 | JDK 8:1.2.84 | JDK 17:1.2.83 | JDK 17:1.2.84 |
|---|---|---|---|---|
普通对象(不带@type) | 46 | 44 | 73 | 78 |
完整类名白名单 +@type | 121 | 127 | 155 | 163 |
全开 autoType +@type | 159 | 185 | 206 | 206 |
包前缀白名单 +@type | 124 | 1,658(约 13 倍) | 153 | 4,326(约 28 倍) |
@JSONType类 +@type(不开 autoType) | 105 | 2,396(约 23 倍) | 159 | 4,518(约 28 倍) |
开 autoType同时配包前缀白名单 +@type | 138 | 421(约 3 倍) | 150 | 285(约 2 倍) |
三轮原始数据(1.2.84,包前缀白名单):JDK 8 是 1,642 / 1,658 / 1,687,JDK 17 是 4,326 / 3,740 / 4,535,各轮之间很稳定。
按 JDK 17 的数字算,一个响应里有 1,000 个带@type的对象,每次反序列化就多出约 4 毫秒。
慢在哪
用 JFR 在 JDK 17 上采样 1.2.84 的这两种写法,93%~94% 的采样都落在同一个方法上:com.alibaba.fastjson.util.IdentityHashMap.findClass。
这个方法会把ParserConfig里反序列化器表的每一个桶都扫一遍,桶数默认是 8192(IdentityHashMap.DEFAULT_SIZE = 8192):
publicClassfindClass(StringkeyString){for(inti=0;i<buckets.length;i++){...if(className.equals(keyString)){returnclazz;}}returnnull;}1.2.83 走不到这里,原因是类在第一次之后就进了全局缓存:
// 1.2.83:前缀白名单命中后加载类,第三个参数 true = 放进缓存clazz=TypeUtils.loadClass(typeName,defaultClassLoader,true);1.2.84 改成了只有完整类名命中才缓存:
// 1.2.84:i + 1 == className.length() 只在完整类名命中时为 trueclazz=TypeUtils.loadClass(typeName,defaultClassLoader,i+1==className.length());@JSONType那条路也一样:1.2.83 在autoTypeSupport || jsonType时缓存;1.2.84 改成loadClass(typeName, defaultClassLoader, false),之后只在开了 autoType 时才调用addMapping。
没进缓存,下一次checkAutoType先查缓存(TypeUtils.getClassFromMapping)落空,接着就调deserializers.findClass(typeName),每次都要把 8192 个桶扫一遍。
源码注释写明了不缓存是故意的(「a prefix match … must not populate the class mapping cache」),为的是不让危险类从缓存里绕过黑名单。所以这是安全收紧带来的性能代价,不是写错了。发布说明里没有提这一点。
怎么确认就是缓存的问题
做了两组对照:
| 写法 | JDK 8:1.2.84 | JDK 17:1.2.84 |
|---|---|---|
包前缀白名单,另外手动TypeUtils.addMapping("demo.User", User.class) | 114(恢复) | 164(恢复) |
@JSONType类,另外addAccept("demo.Tagged")完整类名 | 92(恢复) | 152(恢复) |
把类放回缓存,耗时就和 1.2.83 一样了。第一组只是为了验证原因,不建议在业务里这么写:按源码,checkAutoType会先查这个缓存,命中就直接返回,白名单检查就被跳过了。
升级前怎么自查
在代码里搜这几样:
grep-rn--include=*.java-e"addAccept"-e"@JSONType"-e"AutoTypeSupport"src/grep-rn--include=*.java-e"implements DataSource"-e"AbstractRoutingDataSource"src/grep-rn"fastjson.parser.autoTypeAccept".# 配置文件或启动参数 -D 里写的白名单,走的也是 addAccept对照下面这张表:
| 你的情况 | 升 1.2.84 后 | 怎么办 |
|---|---|---|
不用@type,只是普通的parseObject/toJSONString | 没变化 | 直接升 |
| 开了 SafeMode | 没变化 | 直接升 |
全开 autoType、没配白名单(比如 fastjson 自带的GenericFastJsonRedisSerializer,它内部就是setAutoTypeSupport(true)) | 功能没变化;JDK 8 上耗时中位数 159 → 185 ns,JDK 17 上持平 | 直接升。全开 autoType 本身就是这次漏洞要防的场景,能关最好关 |
addAccept("com.xxx.")包前缀白名单 | 带@type的反序列化慢一个数量级;其中 DataSource 子类直接报错 | 改成完整类名,一个类一行 |
@JSONType注解类,不开 autoType,靠@type反序列化 | 慢一个数量级 | 把这些类也用完整类名addAccept一遍 |
改法示例:
ParserConfigconfig=ParserConfig.getGlobalInstance();// 1.2.84 下会慢,DataSource 子类会被拒// config.addAccept("com.example.");// 改成完整类名config.addAccept("com.example.order.OrderDTO");config.addAccept("com.example.user.UserDTO");config.addAccept("com.example.ds.DynamicDataSource");内部类写Outer$Inner或Outer.Inner都行(1.2.84 把两种写法统一了;1.2.83 只认点号写法)。
复现代码
完整代码就两个类,核心部分如下:
// 功能对照:每个用例单独起 JVM 跑ParserConfigcfg=newParserConfig();cfg.addAccept("demo.");Objecto=JSON.parseObject("{\"@type\":\"demo.MyDataSource\",\"url\":\"x\"}",Object.class,cfg);// 1.2.83: demo.MyDataSource// 1.2.84: JSONException: autoType is not support. demo.MyDataSource// 性能对照:热身 20 万次,再计时 100 万次ParserConfigcfg=newParserConfig();cfg.addAccept("demo.");Stringjson="{\"@type\":\"demo.User\",\"id\":1,\"name\":\"tom\"}";for(inti=0;i<200_000;i++)JSON.parseObject(json,Object.class,cfg);longt0=System.nanoTime();for(inti=0;i<1_000_000;i++)JSON.parseObject(json,Object.class,cfg);System.out.println((System.nanoTime()-t0)/1_000_000+" ns/次");javac-cpfastjson-1.2.84.jar-dout src/demo/*.javajava-cp"fastjson-1.2.84.jar;out"demo.Bench accept_prefix# Linux 下分号换冒号demo.MyDataSource就是一个实现了javax.sql.DataSource、多一个url属性的普通类,方法全是空实现。
数据来源(均为 2026-10-14 读取 / 实跑)
- 1.2.83 → 1.2.84 的改动:
https://github.com/alibaba/fastjson/compare/1.2.83...1.2.84 - 发布说明:
https://github.com/alibaba/fastjson/releases/tag/1.2.84 - 源码行号:tag
1.2.84的ParserConfig.java与IdentityHashMap.java - jar:
https://repo1.maven.org/maven2/com/alibaba/fastjson/