在很长一段时间里,fastjson 都是 Java 后端项目里非常常见的 JSON 处理库。早期选择它,理由很直接:API 简单、序列化速度快、阿里巴巴出品,网上资料也多。直到后来在几次安全巡检和依赖升级中,我陆续发现 fastjson 在反序列化场景下的风险比想象中要高,最终决定在维护的项目里彻底禁用 fastjson,并逐步迁移到 Jackson。这中间经历了依赖排除、代码替换、兼容性测试、性能回归等一系列过程,踩了不少坑,也沉淀了一些经验。本文就围绕“为什么禁用 fastjson”这个话题,从漏洞背景、原理拆解、迁移实战和工程建议几个方面展开,希望能给正在做技术选型或准备移除 fastjson 的同学一些参考。
1. fastjson 是什么,为什么会成为争议焦点
1.1 fastjson 的基本定位
fastjson 是阿里巴巴开源的一个 Java JSON 处理库,提供了序列化和反序列化功能,可以把 Java 对象转换成 JSON 字符串,也可以把 JSON 字符串转换回 Java 对象。它最大的特点是 API 简单,比如:
String json = JSON.toJSONString(obj); User user = JSON.parseObject(json, User.class);这两行代码就能完成常见场景下的对象与 JSON 互转,非常直接。在早期 Spring Boot 生态还没有完全普及、Jackson 配置还稍显繁琐的时候,fastjson 在很多企业项目里被大量使用。
1.2 为什么 fastjson 会成为安全焦点
fastjson 之所以频繁出现在安全公告里,核心原因在于它支持一种“基于类名自动实例化”的反序列化机制。简单理解,就是 JSON 字符串里可以携带类的全限定名,fastjson 在解析时会根据这个类名创建对象,再填充字段值。这个特性原本是为了满足多态和某些框架的序列化需求,但如果传入的类名被攻击者控制,就可能触发危险的反射调用链,最终演变成远程代码执行(RCE)漏洞。
自从 fastjson 爆出第一个严重反序列化漏洞开始,后续多个版本都被发现存在绕过和新的利用链。我曾经统计过自己负责的老项目,在升级 fastjson 的过程中,前前后后经历了不下三次版本号跳跃,每次都要重新验证原有的序列化规则是否兼容。更麻烦的是,安全扫描工具对 fastjson 的版本号非常敏感,即使只是一个 demo 工程引入了低版本 fastjson,也会被标记为高风险。
1.3 禁用 fastjson 不等于否定它的价值
这里先说明一个观点:禁用 fastjson 并不是因为它完全不能使用,而是从维护成本和风险控制角度做出的选择。对于一个长期维护的项目来说,依赖库的安全性、可控性和可持续性优先级很高。fastjson 的迭代节奏虽然很快,但安全问题反复出现,每次都要消耗团队精力去跟进、升级、验证,这种隐形成本很容易被低估。相比之下,Jackson 作为 Spring Boot 默认的 JSON 框架,生态地位更稳定,社区维护更成熟,安全问题的响应也更规范。所以,迁移到 Jackson 在当前环境下是更稳妥的路线。
2. fastjson 反序列化漏洞的核心原理
2.1 反序列化漏洞的根本原因
要理解 fastjson 为什么容易出问题,需要先搞清楚反序列化的本质。反序列化就是把一串字节流或字符串恢复成内存中的对象。如果这个过程允许外部输入指定“要创建哪个类”,并且框架会自动调用某些方法,那么攻击者就有可能通过构造特殊字符串,让程序创建出原本不该创建的对象,进而触发危险操作。
fastjson 中有一个非常关键的类JSON,它解析 JSON 时支持@type字段。看下面这个例子:
String json = "{\"@type\":\"com.example.core.Runtime\",\"cmd\":\"open /System/Applications/Calculator.app\"}"; Object obj = JSON.parse(json); System.out.println(obj.getClass());这段代码只是演示思路,实际利用链会比这复杂很多。关键在于@type指定了类名,fastjson 会尝试创建该类的实例,并把 JSON 里的字段赋值给对象属性。如果这个类的某个 setter 方法或构造函数里存在危险逻辑,攻击者就能通过构造字段触发这些逻辑。历史上的多个 fastjson 漏洞利用链,基本都是在寻找可被攻击者控制的 setter 或 getter,并利用 Java 反射机制组合出能执行命令的调用链。
2.2 从 1.2.x 到 1.2.84 的绕过与修复
从漏洞演变过程来看,fastjson 的修复模式基本上是“发现问题 -> 添加黑名单/白名单 -> 被绕过 -> 再修复”。早期版本使用的是黑名单机制,也就是禁止反序列化已知的危险类。但黑名单很难覆盖所有可能的利用链,所以攻击者不断寻找黑名单之外的新类,或者通过组合已有类来绕过限制。
后来 fastjson 引入了 safeMode,在1.2.68之后用户可以配置开启。开启 safeMode 后,@type的白名单限制会非常严格,普通开发者使用parseObject(String, Class)这类方法时基本不受影响,但如果代码中使用了parseObject(String)或JSON.parse(String)这种不带目标类型的写法,就需要特殊注意。到了1.2.84版本,fastjson 进一步加严了限制,不过安全公告仍然要求使用者及时升级到最新版本,并建议开启 safeMode 或使用 AutoType 白名单。
需要提醒的是:即使你的 fastjson 版本已经升级到最新,只要代码中存在JSON.parseObject(json)这种自动类型解析的写法,风险依然存在。真正稳妥的方案不是追着某一个版本打补丁,而是从代码层面减少危险 API 的使用。
2.3 不只是 RCE,还有 DoS 和类加载风险
除了远程代码执行,fastjson 还存在拒绝服务(DoS)风险。某些特制的 JSON 字符串会导致解析时出现深层嵌套或超大对象,消耗大量 CPU 和内存,拖垮系统。还有一些漏洞利用链会加载远程类,这也是为什么很多安全策略会禁用 fastjson 的自动类型加载。
从我个人经历来说,真正促使我下定决心禁用 fastjson 的,不是某一次具体漏洞爆发,而是“漏洞报告的频率”本身。当一个库每隔几个月就要因为安全问题发布新版本,并且需要使用者手动干预才能降低风险,这个库在长期项目中的维护成本就已经过高了。
3. 环境准备与迁移前现状分析
在彻底禁用 fastjson 之前,我先是梳理了项目中的所有使用情况。这一步很关键,没有全局盘点就直接替换,很容易漏掉隐藏的调用点。
3.1 确认项目中的 fastjson 引入方式
常见的情况有两种:
- 直接在
pom.xml或build.gradle中声明了 fastjson 依赖。 - 间接通过其他第三方库传递依赖。
第一种情况比较容易排查,直接搜索fastjson关键字即可。第二种情况比较隐蔽,需要借助依赖分析工具查看完整依赖树。
以 Maven 为例,可以使用命令:
mvn dependency:tree -Dincludes=com.alibaba:fastjson输出结果类似:
[INFO] +- com.alibaba:fastjson:jar:1.2.83:compile [INFO] \- com.xxx:common-utils:jar:2.0.0:compile [INFO] \- com.alibaba:fastjson:jar:1.2.73:compile如果看到多个 fastjson 版本,说明依赖冲突已经存在。此时需要找到一个明确的上层依赖,并考虑用排除或统一版本的方式处理。
3.2 代码扫描与使用点盘点
我建议先全局搜索以下常见 API:
JSON.toJSONStringJSON.parseObjectJSON.parseArrayJSON.parseTypeReferenceJSONObjectJSONArray@JSONField@JSONTypeSerializeConfigParserConfigFeatureSerializerFeature
把这些使用点列成清单,并在表格里标注所在的模块、文件、行号、用途(序列化、反序列化、字段别名、日期格式等)。只有充分掌握全部使用点,才能制定合理的替换策略。
下面是我做迁移分析时使用的一个示例清单:
| 模块 | 文件 | 行号 | 使用场景 |
|---|---|---|---|
| order-service | OrderController.java | 45 | 返回订单 JSON |
| base-common | JsonUtils.java | 12 | 封装统一 JSON 工具类 |
| user-center | UserService.java | 89 | 解析第三方返回的用户信息 |
| gateway | LogFilter.java | 100 | 记录请求参数日志 |
3.3 制定迁移方案
在确认使用点之后,我建议分三步走:
- 第一步:在依赖层面排除 fastjson,确保不会因为传递依赖而再次引入。
- 第二步:编写或改造统一的 JSON 工具类,把业务代码对 fastjson 的调用收敛到一个门面类中。
- 第三步:分批替换各模块的代码,每替换完一个模块就进行单测和联调,避免一次性大规模改动。
这个过程中,版本环境主要取决于你项目使用的 Spring Boot 版本。Spring Boot 2.x 默认使用 Jackson 2.x,Spring Boot 3.x 使用 Jackson 2.14+ 或更高版本。如果你的项目用的就是 Spring Boot,那么迁移到 Jackson 几乎不需要额外引入依赖,因为spring-boot-starter-web中已经包含了jackson-databind。
4. Jackson 核心能力与 fastjson 替代方案
4.1 Jackson 基础用法
Jackson 的核心类是ObjectMapper,它负责所有序列化和反序列化操作。最简单的用法:
import com.fasterxml.jackson.databind.ObjectMapper; ObjectMapper objectMapper = new ObjectMapper(); // 序列化 String json = objectMapper.writeValueAsString(user); // 反序列化 User user = objectMapper.readValue(json, User.class);建议把ObjectMapper声明为 Spring Bean 或静态工具类的唯一实例,不要每次调用都new ObjectMapper(),因为它的初始化成本较高,而且复用实例可以统一定制配置。
4.2 常用配置对照
在 fastjson 中,我们经常使用SerializerFeature和Feature来控制序列化行为。Jackson 中对应的配置有的是通过ObjectMapper配置,有的是通过注解实现。
常见的对照关系如下:
| 功能 | fastjson | Jackson |
|---|---|---|
| 字段为 null 时不输出 | SerializerFeature.WriteMapNullValue控制是否输出 null | JsonInclude.Include.NON_NULL |
| 日期格式 | JSONField(format="yyyy-MM-dd HH:mm:ss") | @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") |
| 字段名映射 | @JSONField(name="user_name") | @JsonProperty("user_name") |
| 忽略未知字段 | Feature.IgnoreNotMatch | FAIL_ON_UNKNOWN_PROPERTIES设置为 false |
| 序列化字段顺序 | @JSONField(ordinal=1) | @JsonPropertyOrder或@JsonProperty(index=1) |
| 循环引用处理 | SerializerFeature.DisableCircularReferenceDetect | ObjectMapper默认通过WRITE_DATES_AS_TIMESTAMPS等配置,循环引用需要自定义处理 |
这里要特别说明一下“循环引用”。fastjson 默认开启了循环引用检测,在序列化自引用对象时会输出{"$ref":"$"}之类的引用标识。Jackson 默认情况下遇到循环引用会抛出异常,这是两者很明显的区别。如果你在迁移时遇到循环引用问题,不要直接给 Jackson 加上一个忽略字段的注解了事,最好重新设计对象结构,把父子关系理清楚,或者在序列化 DTO 层避免双向引用。
4.3 处理泛型反序列化
fastjson 中我们常用TypeReference来反序列化泛型类型,比如处理List<User>:
List<User> userList = JSON.parseObject(json, new TypeReference<List<User>>() {});Jackson 也有类似的TypeReference:
List<User> userList = objectMapper.readValue(json, new TypeReference<List<User>>() {});两者的用法几乎一致,只是包名不同。fastjson 的是com.alibaba.fastjson.TypeReference,Jackson 的是com.fasterxml.jackson.core.type.TypeReference。
4.4 处理多态与 @type
这是从 fastjson 迁移到 Jackson 时最需要留意的点。fastjson 的@type字段允许在 JSON 字符串中指定类名,Jackson 则使用@class或通过配置实现多态。默认情况下,Jackson 反序列化时不会关心 JSON 中的类名,只会按你传入的目标类型来创建对象,因此更安全。
如果你确实需要多态序列化,可以使用 Jackson 的@JsonTypeInfo注解,但要注意白名单校验。比如:
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = Cat.class, name = "cat"), @JsonSubTypes.Type(value = Dog.class, name = "dog") }) public class Animal { // ... }这样序列化时会输出type字段,反序列化时也只允许cat和dog这两个白名单类型,比 fastjson 的自动类型解析要安全得多。
5. 完整迁移实战:将 fastjson 替换为 Jackson
5.1 创建演示项目结构
为了更清晰地展示迁移过程,我创建一个简单的 Spring Boot 项目,模拟原本使用 fastjson 的接口,然后逐步替换成 Jackson。
项目结构如下:
fastjson-migration-demo/ ├── pom.xml └── src/main/java/com/example/demo/ ├── DemoApplication.java ├── controller/UserController.java ├── entity/User.java └── util/JsonUtils.java5.2 Maven 依赖处理
假设原来的pom.xml中存在 fastjson 依赖,需要先排除它。如果是直接依赖,直接删除即可;如果是传递依赖,可以通过exclusions排除。完整示例pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>fastjson-migration-demo</artifactId> <version>1.0.0</version> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>这里需要注意:spring-boot-starter-web内置了 Jackson,所以不需要额外添加jackson-databind依赖。如果你的项目不是 Spring Boot,则需要手动添加:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.4</version> </dependency>5.3 实体类改造
原实体类中使用的是 fastjson 的@JSONField注解:
package com.example.demo.entity; import com.alibaba.fastjson.annotation.JSONField; import java.util.Date; public class User { private Long id; @JSONField(name = "user_name") private String userName; @JSONField(format = "yyyy-MM-dd HH:mm:ss") private Date createTime; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } }迁移为 Jackson 注解:
package com.example.demo.entity; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Date; public class User { private Long id; @JsonProperty("user_name") private String userName; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date createTime; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } }这里@JsonFormat要比 fastjson 的format多指定timezone,否则如果服务器时区不是东八区,序列化出来的日期时间可能会差 8 个小时。
5.4 统一 JSON 工具类改造
原来的工具类:
package com.example.demo.util; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.TypeReference; import java.util.List; public class JsonUtils { private JsonUtils() { } public static String toJson(Object obj) { return JSON.toJSONString(obj); } public static <T> T parseObject(String json, Class<T> clazz) { return JSON.parseObject(json, clazz); } public static <T> List<T> parseArray(String json, Class<T> clazz) { return JSON.parseArray(json, clazz); } public static <T> T parseObject(String json, TypeReference<T> typeReference) { return JSON.parseObject(json, typeReference); } }改造为 Jackson:
package com.example.demo.util; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.List; public class JsonUtils { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); static { // 反序列化时忽略未知字段 OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 序列化时忽略为 null 的字段 OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); } private JsonUtils() { } public static String toJson(Object obj) { try { return OBJECT_MAPPER.writeValueAsString(obj); } catch (JsonProcessingException e) { throw new RuntimeException("序列化失败", e); } } public static <T> T parseObject(String json, Class<T> clazz) { try { return OBJECT_MAPPER.readValue(json, clazz); } catch (JsonProcessingException e) { throw new RuntimeException("反序列化失败", e); } } public static <T> List<T> parseArray(String json, Class<T> clazz) { try { return OBJECT_MAPPER.readValue(json, new TypeReference<List<T>>() {}); } catch (JsonProcessingException e) { throw new RuntimeException("反序列化List失败", e); } } public static <T> T parseObject(String json, TypeReference<T> typeReference) { try { return OBJECT_MAPPER.readValue(json, typeReference); } catch (JsonProcessingException e) { throw new RuntimeException("反序列化失败", e); } } }这里要注意,“忽略未知字段”是一个很常见的配置,因为在前后端对接时,前端传入了新字段而后端实体还没更新,如果不忽略未知字段,反序列化会直接报错。但“忽略为 null 的字段”要根据实际场景决定,如果接口层希望稳定返回所有的 key,就不应该配置 NON_NULL。建议工具类不要把所有配置都写死,而是让不同模块通过不同的ObjectMapper实例来处理。
5.5 Controller 层改造
原来的 Controller:
package com.example.demo.controller; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.example.demo.entity.User; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/user") public class UserController { @PostMapping("/create") public String createUser(@RequestBody String body) { JSONObject jsonObject = JSON.parseObject(body); String name = jsonObject.getString("user_name"); // 模拟业务逻辑 User user = new User(); user.setId(1L); user.setUserName(name); return JSON.toJSONString(user); } }改造后:
package com.example.demo.controller; import com.example.demo.entity.User; import com.example.demo.util.JsonUtils; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/user") public class UserController { @PostMapping("/create") public String createUser(@RequestBody String body) { Map<String, Object> map = JsonUtils.parseObject(body, Map.class); String name = (String) map.get("user_name"); // 模拟业务逻辑 User user = new User(); user.setId(1L); user.setUserName(name); return JsonUtils.toJson(user); } }这段代码的主要变化是:不再使用 fastjson 的JSONObject,而是使用Map来接收动态字段。如果你真的需要类似JSONObject的灵活容器,Jackson 也有ObjectNode和JsonNode,但日常业务中大可不必,因为定义明确的 DTO 会让代码更清晰。
5.6 运行与验证
启动 Spring Boot 应用,执行以下命令测试:
curl -X POST http://localhost:8080/user/create -H "Content-Type: application/json" -d "{\"user_name\":\"张三\"}"预期返回:
{"id":1,"user_name":"张三","createTime":null}如果配置了NON_NULL,那么createTime为 null 时不会输出,结果为:
{"id":1,"user_name":"张三"}同时我们可以在代码中加入以下测试,验证JsonUtils的泛型解析:
String jsonArray = "[{\"id\":1,\"user_name\":\"张三\"},{\"id\":2,\"user_name\":\"李四\"}]"; List<User> users = JsonUtils.parseArray(jsonArray, User.class); System.out.println(users.size());输出为2。
5.7 特殊场景处理
在迁移过程中,最常见的特殊场景有三个。
第一个是Java 8时间类型LocalDateTime。Jackson 默认能处理LocalDateTime,但输出格式比较难看,需要在ObjectMapper上注册JavaTimeModule并配置格式。示例:
ObjectMapper objectMapper = new ObjectMapper(); objectMapper.registerModule(new JavaTimeModule()); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);或者在字段上使用@JsonFormat:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime;第二个是枚举类型。fastjson 默认按枚举名称序列化和反序列化,Jackson 默认也按名称处理,但如果启用了WRITE_ENUMS_USING_TO_STRING,行为会不一样。建议在实体类中为枚举添加@JsonValue或@JsonCreator来明确映射关系。
第三个是BigDecimal精度。fastjson 在序列化BigDecimal时默认会输出科学计数法吗?实际上有版本差异。Jackson 一般会输出普通数值,但如果你在数据库查询结果中直接序列化某些复杂类型,建议先转换成 DTO 再序列化。
6. 常见问题与排错思路
在禁用 fastjson 和迁移到 Jackson 的过程中,下面这些问题非常典型,整理成表格方便查阅。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
启动报错ClassNotFoundException: com.alibaba.fastjson.JSON | 还有代码引用 fastjson,但依赖已被排除 | 全局搜索import com.alibaba.fastjson并替换 |
反序列化报错Unrecognized field | Jackson 默认拒绝未知字段 | 配置FAIL_ON_UNKNOWN_PROPERTIES=false或在类上使用@JsonIgnoreProperties(ignoreUnknown = true) |
| 日期格式变成时间戳 | 未配置JavaTimeModule或未禁用时间戳输出 | 注册JavaTimeModule,并配置disable(WRITE_DATES_AS_TIMESTAMPS) |
| 字段名变成小写开头或与预期不一致 | 实体属性命名不规范,或者缺少@JsonProperty | 检查 setter/getter 命名,建议显式使用@JsonProperty定义 JSON 字段名 |
循环引用导致Infinite recursion | 实体存在双向关联(如父子互相引用) | 在 DTO 层拆分结构,或使用@JsonIgnore忽略某个方向 |
泛型 List 解析后是LinkedHashMap | 反序列化时未使用TypeReference | 使用objectMapper.readValue(json, new TypeReference<List<User>>() {}) |
序列化LocalDateTime报错InvalidDefinitionException | 缺少jackson-datatype-jsr310依赖 | Spring Boot 项目需引入spring-boot-starter-json,或手动添加jackson-datatype-jsr310 |
如果项目中还残留很多 fastjson 调用,可以使用 IDE 的全局替换功能,先替换包的 import,再逐个处理代码。但要注意很多方法是不能直接一对一替换的,尤其是涉及JSONObject和JSONArray的地方,设计上会有差异。
7. 禁用 fastjson 后的最佳实践与工程建议
7.1 建议建立 JSON 工具类门面
不管使用什么 JSON 库,我建议项目里都维护一个统一的JsonUtils工具类,业务代码只依赖这个工具类,不直接依赖 fastjson 或 Jackson 的 API。这样做的好处是:如果未来想再次切换 JSON 库,只需要修改工具类的内部实现,业务代码完全不用动。我在迁移时因为之前的项目码已经大量直接调用JSON.toJSONString,导致替换成本很高。如果从一开始就做好门面封装,禁用 fastjson 会轻松很多。
7.2 反序列化永远不要用无界的自动类型解析
无论使用 fastjson、Jackson 还是 Gson,都应该避免将外部输入的 JSON 直接解析成不确定类型。fastjson 的JSON.parseObject(String)不带目标类型,就会出现自动类型猜测;Jackson 虽然默认没有类似风险,但如果滥用JsonNode或Map.class,也可能引入类型混乱。正确的做法是:定义明确的 DTO,并在接口边界做参数校验。
7.3 依赖检查应该纳入 CI
在持续集成流水线中,建议加入依赖检查和漏洞扫描。Maven 项目可以使用mvn dependency:tree检查依赖树,也可以使用 OWASP Dependency-Check 或项目自身的依赖扫描插件。每次构建时如果检测到 fastjson 或已知漏洞版本,应该让构建失败,而不是只在代码评审时人工确认。
7.4 生产环境变更前必须验证兼容性
这里分享一个我走过的弯路。原本以为替换 JSON 库只是 API 层面的变化,结果上线后出现了两个问题:一个是日期格式从数组变成了字符串,另一个是Map中 null 字段被序列化成了字符串"null"。这些都是 fastjson 与 Jackson 默认行为的差异。因此在迁移时,一定要对比原接口的返回样例,最好编写自动化测试来锁定 JSON 结构。涉及线上切换时,建议先灰度发布一部分实例,观察日志和调用方反馈再全量发布。
7.5 不要频繁切换技术栈
禁用 fastjson 是为了降低风险,但并意味着每出一个新 JSON 库就要追新。Jackson 当前已经是事实标准,如果项目没有特殊需求,没必要再引入其他 JSON 框架。另外,如果你的团队对 Jackson 不熟悉,建议先在公共模块中封装好常用功能,并补充使用文档,减少同事的学习成本。
8. 总结与后续学习建议
从之前犹豫要不要升级 fastjson 版本,到最终决定禁用并迁移到 Jackson,整个过程让我更深刻地理解了反序列化安全的重要性。fastjson 的 API 确实简单,历史贡献也值得肯定,但在安全维护成本、社区生态和可控性几个维度上,Jackson 目前更适合大多数项目。我在这次迁移中最大的收获不是替换了某几个 API,而是养成了依赖排查和风险前瞻的习惯。
如果你也要在项目中禁用 fastjson,建议先做依赖和代码使用点的盘点,然后封装统一工具类,逐模块替换,最后通过自动化测试和灰度发布来验证兼容性。对于使用 fastjson 的存量代码,不要指望一次性全部改完,优先处理对外接口和核心链路,再逐步清理边缘模块。这样才能在保证业务稳定的前提下,彻底摆脱 fastjson 带来的安全隐患。