最近后台总有人搜「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 个提交,主代码只动了两个文件:
text
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 行):
java
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 entry
if (i + 1 < className.length() && TypeUtils.isAutoTypeDenyClass(clazz)) {
continue;
}
return clazz;
}
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):
java
public Class findClass(String keyString) {
for (int i = 0; i < buckets.length; i++) {
...
if (className.equals(keyString)) {
return clazz;
}
}
return null;
}
1.2.83 走不到这里,原因是类在第一次之后就进了全局缓存:
java
// 1.2.83:前缀白名单命中后加载类,第三个参数 true = 放进缓存
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
1.2.84 改成了只有完整类名命中才缓存:
java
// 1.2.84:i + 1 == className.length() 只在完整类名命中时为 true
clazz = 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 会先查这个缓存,命中就直接返回,白名单检查就被跳过了。
升级前怎么自查
在代码里搜这几样:
bash
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 一遍 |
改法示例:
java
ParserConfig config = 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 只认点号写法)。
复现代码
完整代码就两个类,核心部分如下:
java
// 功能对照:每个用例单独起 JVM 跑
ParserConfig cfg = new ParserConfig();
cfg.addAccept("demo.");
Object o = 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
java
// 性能对照:热身 20 万次,再计时 100 万次
ParserConfig cfg = new ParserConfig();
cfg.addAccept("demo.");
String json = "{\"@type\":\"demo.User\",\"id\":1,\"name\":\"tom\"}";
for (int i = 0; i < 200_000; i++) JSON.parseObject(json, Object.class, cfg);
long t0 = System.nanoTime();
for (int i = 0; i < 1_000_000; i++) JSON.parseObject(json, Object.class, cfg);
System.out.println((System.nanoTime() - t0) / 1_000_000 + " ns/次");
bash
javac -cp fastjson-1.2.84.jar -d out src/demo/*.java
java -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/