SpringBoot fastjson 1.x → fastjson2 2.0.63 迁移执行手册

📅 2026/8/4 7:07:39
SpringBoot fastjson 1.x → fastjson2 2.0.63 迁移执行手册
SpringBoot fastjson 1.x → fastjson2 2.0.63 迁移执行手册适用把com.alibaba:fastjson1.x迁移到com.alibaba.fastjson2:fastjson22.0.63。这份文档是可执行清单不是背景介绍。按 §1 → §7 顺序做每步都有检测命令和确定的改法。三条铁律违反其中任何一条迁移就是白做或留下静默故障坐标必须换成com.alibaba.fastjson2:fastjson2。安全扫描按groupId:artifactId匹配 CVE只升版本不换坐标告警不会消失。必须加 §4 的全局兼容配置。缺了它两个默认行为变化会造成不抛异常、结果悄悄变错的线上事故。遇到兼容问题就地解决不要退版本。目标版本恒定 2.0.63。§1 依赖改造1.1 先统一再迁移多模块项目的旧版本号通常散落在各模块 pom 里多个 1.x 版本并存。先收敛到父 pom 一处否则改完主线仍有模块在用旧包。!-- 父 pom唯一版本来源 --propertiesfastjson.version2.0.63/fastjson.version/propertiesdependencyManagementdependenciesdependencygroupIdcom.alibaba.fastjson2/groupIdartifactIdfastjson2/artifactIdversion${fastjson.version}/version/dependency/dependencies/dependencyManagement子模块只声明坐标、不写 version。有独立父 pom 的模块不继承主父 pom要单独同步。1.2 检测并处理# 旧坐标含第三方传递依赖 —— 有的话你换了自己的也白搭mvn dependency:tree-Dincludescom.alibaba:fastjson*# 各模块 pom 里的旧坐标声明grep-rnartifactIdfastjson/artifactId--includepom.xml.情况动作模块源码有 fastjson 调用换坐标version 交给父 pom模块源码零调用grep 包名无命中直接删掉依赖不要只升版本第三方库传递带入旧坐标单独评估升级该第三方或exclusions排除§2 包名与 API 替换2.1 包名映射原新com.alibaba.fastjson.JSON/JSONObject/JSONArray/JSONExceptioncom.alibaba.fastjson2.*com.alibaba.fastjson.TypeReferencecom.alibaba.fastjson2.TypeReferencecom.alibaba.fastjson.annotation.JSONField/JSONTypecom.alibaba.fastjson2.annotation.*com.alibaba.fastjson.serializer.SimplePropertyPreFiltercom.alibaba.fastjson2.filter.SimplePropertyPreFilter⛔不要用sed s/com\.alibaba\.fastjson\./com.alibaba.fastjson2./g一把梭—— 会漏掉serializer→filter的子包改名报错是「找不到符号」容易误判成缺依赖。2.2 API 替换编译期会报错的原写法改成JSONObject.toJavaObject(json, Xxx.class)静态形式JSON.parseObject(str, Xxx.class)JSONObject.toJSON(obj)JSON.toJSON(obj)JSONObject.parseArray(str)JSON.parseArray(str)JSON.toJSONString(obj, true)JSON.toJSONString(obj, JSONWriter.Feature.PrettyFormat)JSONObject.toJSONString(obj, filter)JSON.toJSONString(obj, filter)JSONType(serializeEnumAsJavaBean true)JSONType(writeEnumAsJavaBean true)2.3 JSONObject.parse()→JSON.parse()编译期不报错运行期炸这是全流程里最容易漏的一条优先处理。// ❌ 危险v1 里 parse 是从 JSON 继承来的静态方法返回 Object// v2 里被重新声明为返回 JSONObject传入数组直接抛 JSONExceptionObjectresultJSONObject.parse(text);// ✅ 正确JSON.parse 保留返回 Object 的多态语义ObjectresultJSON.parse(text);把JSONObject赋给Object是类型放宽编译器绝不会报错。运行时传入[开头的数组才炸JSONException: offset 1, character [, line 1, column 1。grep-rnJSONObject\.parse(--include*.java.# 命中即改2.4 立个规矩统一只用JSON.作为静态入口JSONObject/JSONArray只当类型用不当工具类用。v1 里JSONObject继承JSON所以JSONObject.parseObject(...)这类写法能编译看起来和JSON.xxx()等价 —— 但 v2 里有的被删、有的被收窄§2.3。统一入口能一次性消灭这整类陷阱且这批改动语义完全等价、零风险。grep-rnEJSON(Object|Array)\.(parseObject|parseArray|toJSONString|toJavaObject|toJSON|parse)\(--include*.java.§3 两个静默行为变化唯一会造成线上事故的不抛异常、不打日志只是结果悄悄变了。编译和普通单元测试都发现不了。处理方式是 §4 的全局配置 —— 一行覆盖全部实体不要指望逐个补注解。3.1 智能字段匹配默认关闭fastjson1fastjson2 原生大小写不敏感的 key→字段匹配开启关闭对接外部系统时返回的 JSON key 常是全小写或全大写msgid、roomid、tolist、ACTVNM而实体字段是驼峰且历史代码普遍没加JSONField(name...)—— 因为 v1 靠智能匹配自动对上了。v2 原生下这些字段全部静默变null后果还会二次放大key 匹配不上 → 实体业务主键为 null → 落库时唯一索引冲突 → 第一条侥幸写入后续全部失败 → 日志只有「已收到」没有任何异常排查时看到的是「数据少了、字段空了」离根因隔三层极费时。3.2 Date 默认序列化格式改变fastjson1fastjson2 原生java.util.Date毫秒时间戳1784685600000字符串2026-07-21 10:00:00发往外部系统的报文时间格式全变。自己系统察觉不到反序列化时两种格式 v2 都能读只有对端解析失败 —— 往往等对方投诉才知道。§4 必须添加的全局兼容配置直接复制。三个语句的顺序不能变。importcom.alibaba.fastjson2.JSON;importcom.alibaba.fastjson2.JSONReader;/** * fastjson2 原生模式全局兼容配置 —— 恢复 fastjson1 的默认行为。 * 缺少本配置会造成「不抛异常、结果悄悄变错」的静默故障禁止删除。 */publicclassFastjson2CompatConfig{static{applyV1CompatibleDefaults();}publicstaticvoidapplyV1CompatibleDefaults(){// [1] 必须最先执行且必须早于任何 fastjson2 调用强制初始化 MethodHandles.Lookup 类。// JDK 8 上 fastjson2 的 JDKUtils 静态块用 Unsafe 读 Lookup.IMPL_LOOKUP 拿 TRUSTED lookup// 而 Unsafe 读静态字段不触发类初始化 —— 若 Lookup 尚未初始化则读到 null// fastjson2 退化为无 PRIVATE 权限的 lookup其 lambda 访问器随即抛// LambdaConversionException: Invalid caller: Bean类// 后果是该 JVM 内所有 Bean 反序列化全部失败。// 返回值无需使用要的只是「类被初始化」这个副作用。禁止删除本行。java.lang.invoke.MethodHandles.lookup();// [2] 恢复大小写不敏感的智能字段匹配见 §3.1JSON.config(JSONReader.Feature.SupportSmartMatch);// [3] 恢复 Date - 毫秒时间戳见 §3.2// ⛔ 不要用 JSONWriter.Feature.WriterUtilDateAsMillis它是硬覆盖// 会让字段上的 JSONField(format...) 全部失效。// ✅ configWriterDateFormat 设的是「默认格式」字段级注解仍可覆盖与 v1 语义一致。JSON.configWriterDateFormat(millis);// 加一行启动日志测试/运维可据此在启动日志确认配置生效// log.info(fastjson2 兼容配置已应用: SupportSmartMatch writerDateFormatmillis);}}4.1 放在哪里情况落点理由有共享基础库所有服务都依赖放共享库注册为自动配置Spring Boot 2.xMETA-INF/spring.factories3.xMETA-INF/spring/...AutoConfiguration.imports自动配置不受各服务ComponentScan的excludeFilters影响某服务显式排除了共享库的包扫描在该服务包内再独立放一份别赌「自动配置一定不受影响」这个假设非 Spring 应用 / 独立main()入口第一行显式调用applyV1CompatibleDefaults()没有容器帮你加载配置类⚠️用静态代码块不要只靠Bean方法—— 这样类被任何方式加载时配置都会生效不依赖 Spring 是否真的实例化了它。4.2 JDK 8 的LambdaConversionException配置里第 [1] 行解决的问题现象RuntimeException: Failed to create lambda for method: public void XxxDTO.setYyy(java.lang.String) Caused by: java.lang.invoke.LambdaConversionException: Invalid caller: com.example.XxxDTO一旦出现该 JVM 内所有 Bean 反序列化全部失败不只是报错那个类。为什么表现为「时好时坏」只要在 fastjson2 初始化之前有任何代码碰过MethodHandles.Lookup就一切正常。而门槛极低 ——任何一个 lambda / 方法引用在invokedynamic引导时都必须用到Lookup一个Runnable r () - {};就够。场景是否中招Spring Boot 服务正常启动❌ 不中招框架启动过程满是 lambdaJUnit / surefire 跑测试❌ 不中招同理裸public static void main()调试类✅必中招所以典型症状是线上和测试环境都正常只有开发在 IDE 里跑main()报错—— 极易被当成本地环境问题忽略。配套动作# 确认没有类在静态初始化里调用 fastjson2那种类可能比配置类先加载预热就晚了grep-rnEstatic[[:space:]](final[[:space:]])?[A-Za-z,\[\][:space:]][[:space:]]*(JSON|JSONObject|JSONArray)\.--include*.java.# 生产代码里残留的调试用 main()这些不走 Spring 容器、加载不到配置类必然中招。建议删除grep-rnpublic static void main--include*.javasrc/main/⚠️判定要点如果生产环境出现的是「字段为 null」而不是这个异常说明 reader创建成功了只是 key 没匹配上那是 §3.1 的问题不是这里的问题。两者现象完全不同别搞混。副作用要如实记录别当无害空调用预热成功后 fastjson2 拿到的是 TRUSTED 级 lookup对任意类都有 private 访问权。可接受因为 fastjson2 既然在 classpath 上任意一个 lambda 就能让它拿到这一行没有给它原本拿不到的东西且 fastjson 系列的历史漏洞是 autoType 反序列化 gadget与 lookup 权限无关。§5 验证5.1 差分测试以 fastjson1 的真实 jar 为基线逐项比对这是唯一能发现「静默行为变化」的手段。两套 classpath一边挂 1.x jar、一边挂 2.0.63跑同一组用例逐字节比对输出。不一致的每一项都要能解释清楚不能一句「应该没影响」带过。覆盖清单可直接用作用例模板类别覆盖内容取值 APIgetString/getInteger/getLong/getBoolean/getDate/getObject/getJSONObject/getJSONArray/toJavaList边界输入key 不存在、空字符串、null入参、类型不匹配结构嵌套泛型TypeReference、JSONArray下标访问、值本身是 JSON 字符串时调getJSONArray是否自动二次解析序列化transient是否跳过、SimplePropertyPreFilter父子类作用域、JSONField(serializefalse/deserializefalse)注解组合JSONField(name)JSONField(format)与全局配置的优先级日期Date/LocalDateTime的序列化与反序列化格式已知的唯一可接受差异同一对象被重复引用时v1 输出 fastjson 私有的$ref标记v2 直接把对象写两遍。若无代码依赖$refv2 的输出对外部消费者反而更安全。5.2 留一组「必须失败」的回归用例针对 §3 的静默行为写死断言注明禁止删除用例守住的东西全小写 key 能映射到驼峰字段断言字段非 nullSupportSmartMatchDate序列化结果精确等于毫秒时间戳如{createTime:1784685600000}日期格式带JSONField(format)的字段仍按注解格式输出没误用WriterUtilDateAsMillisJSON.parse([...])返回JSONArray、JSON.parse({...})返回JSONObject§2.3 的返回类型语义5.3 单元测试的两个盲区吃过亏测试 Bean 必须用独立顶层类不要用测试类的静态内部类。Lookup.in()的规则是「同一 top-level 类内部才保留 PRIVATE 权限」嵌套类恰好绕过 §4.2 的缺陷把问题完全掩盖。依赖类初始化顺序的缺陷单元测试天生测不出。JUnit/surefire 启动本身就会初始化MethodHandles.Lookup所以 §4.2 那个缺陷在测试里永远是通过。这类问题只能靠独立 JVM 差分每个场景一个全新进程。写用例前先自问这个用例在缺陷存在时会失败吗答案是「不一定」它就不是防线只是装饰。§6 收尾审计应全部为空或人工逐条确认# 1. 残留旧包名注意末尾的点grep-rncom\.alibaba\.fastjson\.--include*.java.# 2. 残留旧坐标含传递依赖grep-rnartifactIdfastjson/artifactId--includepom.xml.mvn dependency:tree-Dincludescom.alibaba:fastjson*# 3. 返回类型被收窄的调用grep-rnJSONObject\.parse(--include*.java.# 4. 应统一为 JSON. 入口的静态调用grep-rnEJSON(Object|Array)\.(parseObject|parseArray|toJSONString|toJavaObject|toJSON|parse)\(--include*.java.# 5. 注解属性改名grep-rnserializeEnumAsJavaBean--include*.java.# 6. prettyFormat 布尔重载grep-rnEtoJSONString\([^)],[[:space:]]*(true|false)[[:space:]]*\)--include*.java.# 7. 强转 JSON.toJSON() 结果的地方确认入参是对象而非 JSON 字符串见 §7.3grep-rnJSON\.toJSON(--include*.java.# 8. 静态初始化里调用 fastjson2见 §4.2grep-rnEstatic[[:space:]](final[[:space:]])?[A-Za-z,\[\][:space:]][[:space:]]*(JSON|JSONObject|JSONArray)\.--include*.java.# 9. 生产代码里的调试 main()见 §4.2grep-rnpublic static void main--include*.javasrc/main/无法机械检测、必须人工梳理的一项所有「接收外部系统 JSON 的实体」清单 —— 靠 §4 的全局配置兜住再逐步补JSONField(name...)。6.1 发版顺序顺序错会导致服务仍在用旧包先发共享基础库含全局兼容配置的那个推私服再发直接依赖 fastjson 的服务最后发只通过共享库间接依赖的服务验收项各服务依赖树里已无旧坐标各服务启动日志能看到兼容配置那行 log看不到就说明配置没加载§3 两个坑必然复现重跑安全扫描确认告警确实消除这是整件事的目标别忘了验收按各服务 fastjson 调用文件数排测试优先级用量最大的优先深测§7 排查时会误导你的信号7.1 不要用JSON.VERSION判断版本上游有时忘了同步这个常量它可能与实际 jar 版本不符异常堆栈里打的fastjson-version也来自它。会让人误判成「IDE classpath 陈旧」白花时间清缓存、重导项目。unzip-pfastjson2-*.jar META-INF/maven/com.alibaba.fastjson2/fastjson2/pom.properties# 权威shasum fastjson2-*.jarcatfastjson2-*.jar.sha1# 校验 jar 完整性mvn dependency:tree-Dincludescom.alibaba*:fastjson*# 实际生效版本7.2 共享库改完必须重新 install否则下游模块解析到本地仓库里的旧 POM表现为「明明改完了依赖树里 fastjson 还在」。mvninstall-pl共享库模块-am-DskipTestsIDE 也要手动 Reload Maven Project否则 IDE 内的运行/调试仍用旧 classpath。7.3 别把所有异常都归因到升级迁移期间任何报错都会被当成升级引起。先用 fastjson1 的 jar 复现一遍再下结论。高频误判(JSONObject) JSON.toJSON(jsonString)抛ClassCastException。toJSON()的职责是「Java 对象→ JSON 结构」传字符串进去它原样返回 String ——v1 行为完全相同是一直存在的错误用法与迁移无关。目的正确 APIJSON 字符串 →JSONObjectJSON.parseObject(str)JSON 字符串 → 未知结构可能是数组JSON.parse(str)Java 对象 →JSONObjectJSON.toJSON(obj)Java 对象 → JSON 字符串JSON.toJSONString(obj)三句话总结编译通过 ≠ 行为一致。唯一造成线上事故的两个坑§3 智能匹配、日期格式编译期和单元测试都不报错。精力放在差分测试上。审计 API 要看返回类型和语义不能只看方法是否存在。返回类型收窄 赋值给Object能完美骗过编译器§2.3。写回归用例前先问这个用例在缺陷存在时会失败吗用嵌套静态类当测试 Bean、或测一个依赖类初始化顺序的缺陷用例永远是绿的。