尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

FastJson核心要点与避坑:序列化、时间处理与安全升级

FastJson核心要点与避坑:序列化、时间处理与安全升级 1. 为什么 FastJson 至今仍是Java后端绕不开的JSON库讲真我最早接触 FastJson 是在 2016 年那时候公司的老项目里到处都是JSON.toJSONString()和JSON.parseObject()后来自己写新服务也跟着用了几年。中途经历了几次 FastJson 爆出安全漏洞、团队投票要不要换 Jackson 的风波但直到今天我依然会在很多中小型项目中看到 FastJson 的身影。先说清楚 FastJson 到底是什么它是阿里巴巴开源的 Java JSON 处理库核心功能就两个——把 Java 对象序列化成 JSON 字符串把 JSON 字符串反序列化成 Java 对象。看起来平淡无奇但它当年在国内流行起来有三个别人比不了的原因一是 API 极其简单闭着眼睛都能写二是性能确实快官网标注的数倍于 Jackson/Gson 的说法虽然多少带点广告味但实测压测下也确实不虚三是最关键的一点它对Java 生态里的各种奇怪类型支持得非常好比如Date、BigDecimal、泛型、枚举、Optional老项目里那些乱七八糟的对象模型用 FastJson 序列化基本不用额外写配置。这篇文章我会从四个方向展开最常用的序列化/反序列化 API 到底有哪些隐藏细节时间日期转时间戳这种高频需求怎么做全局配置FastJson 和 Jackson 的选型对比到底差在哪以及版本升级和安全坑位。适合刚接触 FastJson 的新人也适合在项目里用了很久但只停留在toJSONString层面的老手。2. 最常用的API你真正吃透了吗序列化与反序列化细节FastJson 给人的第一印象是简单到没朋友很多人写了好几年代码用到的方法不超过五个JSON.toJSONString()、JSON.parseObject()、JSON.parseArray()、JSONObject、JSONArray。但真出了问题比如嵌套泛型反序列化失败、JSONObject拿到Integer结果强转报错一查一个准。2.1 基础用法背后的类型信息传递先看最常见的三种调用// 对象转JSON字符串 String json JSON.toJSONString(user); // JSON字符串转Java对象 User user JSON.parseObject(json, User.class); // JSON字符串转List ListUser users JSON.parseArray(json, User.class);这段代码每个人都会写但多数人没注意到parseObject其实有两个参数一个是 JSON 字符串一个是TypeReference。当你需要反序列化带有泛型的对象时直接传Xxx.class是会出问题的。举个例子// 错误示范泛型丢失运行时会报ClassCastException ResultPageUser result JSON.parseObject(json, Result.class); // 正确示范借助TypeReference传递完整泛型类型 ResultPageUser result JSON.parseObject(json, new TypeReferenceResultPageUser() {});这里面的原因说起来也简单Java 的泛型在编译期会被擦除Type Erasure类上的泛型参数到运行时就没了FastJson 拿到Result.class只知道是一个Result不知道里面原来装的PageUser长什么样。而TypeReference是 FastJson 设计出来专门解决这个问题的一个抽象类它通过继承时保留的泛型签名拿到完整类型信息。我见过特别多线上 bug 就出在这一步接口层明明返回的是一个包装对象内部字段是泛型结果前端拿到数据后一整个页面白屏查日志才发现 Java 这边反序列化类型不对强转报错。所以记住一条铁律只要目标类型带泛型一律用TypeReference。2.2 JSONObject 和 JSONArray 的正确使用姿势FastJson 里JSONObject本质是MapString, Object的实现JSONArray本质是ListObject的实现。很多新手从接口里拿到JSONObject后喜欢这样取值JSONObject data JSON.parseObject(json); String name (String) data.get(name); // 危险get到的不一定是String问题在于JSON 字符串里的数字、布尔值经过 FastJson 解析后具体变成Integer、Long、BigDecimal中的哪一种取决于原始字符串的字面量。你期望它是Integer结果原始字段是1.0解析出来是BigDecimal强转Integer直接抛异常。正确的取值姿势是用带类型的方法JSONObject data JSON.parseObject(json); String name data.getString(name); Integer age data.getInteger(age); BigDecimal price data.getBigDecimal(price); ListUser users data.getJSONArray(users).toJavaList(User.class);getString、getInteger、getBigDecimal这些方法内部会自己做类型转换和兼容处理比起你手工强转健壮得多。另外还有个实用小技巧拿到JSONObject之后可以用data.getObject(fieldName, Xxx.class)直接把某个字段转成一个嵌套对象内部同样是基于类型信息做的反序列化比先getJSONObject再parseObject少一步代码也更清爽。2.3 序列化时对象内部发生了什么很多人好奇JSON.toJSONString(user)到底是怎么把一个带私有字段的 Java 对象变成 JSON 的。FastJson 采用的是字段级别的反射 getter 方法探测混合策略优先通过 JavaBean 规范里的 getter 方法拿到属性值找不到 getter 则尝试直接访问字段前提是配置允许。这个策略带来一个常见基础问题类里只有 getter 没有 setter反序列化时字段会被忽略。因为 FastJson 反序列化时要么走 setter 方法设值要么走字段直接反射赋值。如果你的类写了getUserList()但没写setUserList()序列化没问题反序列化后这个字段就是 null。解决这类问题要么老老实实补上 setter要么在字段上加上JSONField(deserialize true)注解。还有一点容易被忽略FastJson 默认情况下对象内部的私有字段也能被直接序列化和反序列化这在很多严格的接口场景里会泄露不该暴露的字段。比如一个实体类里有password、secretKey之类的字段直接toJSONString会原样输出。解决方案是用注解或过滤器排除字段这个我在后面第 4 节会专门讲。3. 时间日期与时间戳转换从格式化到全局策略热搜词里fastjson 时间 转 时间戳排得非常靠前这确实是一块高频踩坑地。FastJson 对Date类型默认序列化成什么格式不同版本行为不一样这也是升级版本后最常见的行为突变问题。3.1 默认行为为什么线上出现一堆数字先记住 FastJson 的默认规则序列化java.util.Date时默认输出为时间戳毫秒值long 类型。也就是说你写JSON.toJSONString(date)得到的不是2024-05-20 10:30:00而是1716177000000这样一串数字。很多从 Jackson 切到 FastJson 的同事第一次看到这个输出都会愣住——怎么一堆数字其实这是 FastJson 刻意设计的时间戳是机器可读的、无歧义的传输效率也高。但对大部分业务接口来说前端要的是格式化字符串比如yyyy-MM-dd HH:mm:ss拿时间戳还得自己再转一次麻烦得很。这里有三种解决方案按推荐程度排序// 方案一成员变量级别指定格式化 public class User { JSONField(format yyyy-MM-dd HH:mm:ss) private Date createTime; } // 方案二在toJSONString时指定SerializerFeature String json JSON.toJSONString(user, SerializerFeature.WriteDateUseDateFormat); // 方案三全局配置一劳永逸 SerializeConfig config new SerializeConfig(); config.put(Date.class, new SimpleDateFormatSerializer(yyyy-MM-dd HH:mm:ss)); String json JSON.toJSONString(user, config);方案一适合局部字段特殊格式方案二适合一次调用临时处理方案三适合全项目统一风格。个人建议如果项目里已经统一用了 FastJson 作为全局 HTTP 消息转换器直接在配置层面把日期格式固定下来别让业务代码到处传SerializerFeature不然到后面你会看到同一接口两种日期格式并存的诡异局面。3.2 把时间戳转回Date以及时区问题反序列化方向上FastJson 默认能自动识别以下几种时间格式不需要额外配置毫秒时间戳1716177000000秒时间戳1716177000这个要注意FastJson 默认按毫秒解析秒级时间戳会被放大约 1000 倍ISO 86012024-05-20T10:30:00Z标准格式化2024-05-20 10:30:00实际项目里最常见的坑是客户端传过来的是秒级时间戳比如微信支付回调、部分第三方开放平台的数据10 位的数字结果 FastJson 把它当毫秒解析时间直接跑到 1970 年排查半天才意识到是单位问题。秒级时间戳怎么处理两个思路一是让上游先转成毫秒再传二是自定义一个反序列化器专门处理秒级时间戳。第二个方案的实现也不复杂public class SecondTimestampDeserializer extends JSONDeserializerDate { Override public Date deserialze(DefaultJSONParser parser, Type type, Object fieldName) { Object value parser.parse(); if (value null) { return null; } // 值小于等于10位按秒处理大于10位按毫秒处理 long raw Long.parseLong(value.toString()); long timestamp raw 9999999999L ? raw * 1000L : raw; return new Date(timestamp); } }再通过JSONField(deserializeUsing SecondTimestampDeserializer.class)挂到指定字段上就能做到字段级别的兼容解析。时区问题是另一大坑。FastJson 序列化Date时默认使用 JVM 默认时区TimeZone.getDefault()。如果你的服务器设置的是UTC而前端在中国那接口返回的2024-05-20 10:30:00实际是 UTC 时间前端拿来直接用会差 8 个小时。排查这类问题不要只盯代码先看服务器date命令输出对不对再看 JVM 的user.timezone参数有没有被显式指定。3.3 处理LocalDateTime 和 JDK8 时间类型很多新项目已经开始用LocalDateTime、LocalDate替换DateFastJson 对这块的支持是一个版本分水岭。在1.2.x 早期版本里LocalDateTime一律被序列化成数组形式[2024, 5, 20, 10, 30, 0]看到这种输出真的会一头雾水后来版本才支持了通过JSONField(format yyyy-MM-dd HH:mm:ss)指定格式化。如果你还在用老版本 FastJson又必须序列化LocalDateTime我的建议是把LocalDateTime统一转成Date再做序列化或者在 DTO 里直接用 String 接收前端传来的时间字符串避免让 FastJson 直接接触 JDK8 时间类型。当然最干净的方案其实是升级版本后面第 5 节会专门讲版本升级的取舍。4. FastJson 与 Jackson 对比选型到底差在哪jackson和fastjson哪个好是搜索量很高的词标题只有 FastJson但这个问题绕不开因为几乎每个项目组在技术评审时都会被问到。我不打算站队只把两边在实际项目中的差异摊开来讲。4.1 性能指标的真实差异先看模拟对比表数据来自我本地压测供参考不代表绝对情况对比维度FastJsonJackson序列化速度略快短小对象优势明显稍慢但差距不大反序列化速度中规中矩略快复杂泛型更稳内存占用轻量略高对复杂泛型的支持需要TypeReference类型丢失容易踩坑泛型处理更严格类型信息保留更好代码可读性API 简单直观上手快配置繁琐注解概念多生态与维护阿里维护版本更新时快时慢社区活跃度高Spring 官方默认集成安全历史多个反序列化漏洞记录相对较少bug 响应快说实话在大多数 CRUD 业务系统里FastJson 和 Jackson 的性能差异根本感知不出来。压测数据好看归好看真实瓶颈不会是一个 JSON 解析库多花 5 毫秒还是少花 5 毫秒数据库查询、网络 IO 才是大头。所以性能不应该是选型的首要因素。4.2 开发效率差异才是真实痛点FastJson 在开发效率上确实占据了明显优势。同样是反序列化一个带泛型的复杂对象FastJson 写new TypeReferenceResultPageUser() {}一次搞定Jackson 要写TypeFactory.constructParametricType工厂方法模板代码一大堆。FastJson 的JSONObject可以像 Map 一样随手取值、修改、再序列化Jackson 的ObjectNode虽然也能做但 API 设计明显没那么顺手。反过来Jackson 胜在规范化。它有完整的注解体系JsonProperty、JsonIgnore、JsonFormat语义非常清晰FastJson 的JSONField虽然也能做类似的事但有时候行为会和 getter/setter 命名规则产生组合冲突导致字段莫名消失。比如字段叫isActiveFastJson 默认会把它当成active处理序列化出来的 key 变成active和接口文档对不上排查时如果不知道这个坑会浪费很长时间。4.3 安全与维护是我目前最看重的维度说到安全问题FastJson 的前科不少。从 1.2.24 开始陆续爆出多个反序列化远程代码执行漏洞利用方式基本都是攻击者精心构造 JSON 字符串触发 FastJson 将恶意类反序列化进而执行系统命令。这个问题在 1.2.68 之后通过新增safeMode安全模式得到缓解但即便如此我现在给新项目的建议依然是如果能选优先用 Jackson如果老项目已经被 FastJson 绑定了务必升级到最新版并开启安全模式。这不是说 FastJson 烂而是说在一个已经被大面积证明存在攻击面的组件上继续投入风险成本远高于迁移成本。Jackson 也出过漏洞但它的修复速度和社区响应明显更快而且 Spring Boot 默认集成意味着你不需要额外引入依赖整个依赖树上少一个重量级库安全审计也少一个关注点。4.4 一些迁移场景的建议如果你是出于安全考虑从 FastJson 迁到 Jackson不建议一步到位全部改完。我的实操经验是先梳理代码里JSON.toJSONString()和JSON.parseObject()的调用点统计出涉及到的类型和特殊配置把工具类替换成ObjectMapper但JSONObject可以用MapString, Object或JsonNode临时替代尽量保持业务层接口形状不变然后跑一轮完整的回归测试重点比对日期格式、null 值处理、字段大小写和三方回包格式。整个过程最痛苦的地方在于日期格式和 null 值策略FastJson 默认是不输出 null 字段而 Jackson 默认是输出 null 字段并显示为 null。把这个坑聊明白迁移就成功了一半。Jackson 对应配置很简单ObjectMapper mapper new ObjectMapper(); // null字段不序列化对齐FastJson默认行为 mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 日期输出格式对齐FastJson配置 mapper.setDateFormat(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss));5. 版本安全与升级从版本号里的陷阱到迁移方案版本这个东西很多人不关心觉得能用就行。可 FastJson 恰好是一个不关心版本会出事的库。热搜词里fastjson版本排名靠前说明大家对版本选择确实有困惑。我先把版本线梳理清楚。5.1 版本历史与 1.2.x 时代的分水岭FastJson 有一个很有意思的版本分叉主干线是1.2.x从 1.2.0 一路发展到 1.2.83另外在 2022 年推出了2.0.x2.0.x 系列-artifact 名也发生了变化新的坐标是com.alibaba.fastjson2:fastjson2和老的com.alibaba:fastjson是两个不同的包名。这个改动相当重要。我见过有同事把fastjson2依赖加进去代码里用的还是com.alibaba.fastjson.JSON这个类结果发现项目里同时存在fastjson1和fastjson2两套实现运行时按照依赖顺序加载行为飘忽不定。后来才搞明白fastjson2 也提供了com.alibaba.fastjson.JSON这个兼容类但它内部是走 fastjson2 核心逻辑的如果你单独引入 fastjson2 依赖老的com.alibaba:fastjson还挂在依赖树上两者就会打架。我的建议是新项目直接用fastjson2坐标如下dependency groupIdcom.alibaba.fastjson2/groupId artifactIdfastjson2/artifactId version2.0.51/version /dependency老项目还在用 1.x 的至少升级到 1.2.831.x 的最后一个稳定版本并打开安全模式// 全局开启安全模式禁止反序列化任意类大幅缩小攻击面 ParserConfig.getGlobalInstance().setSafeMode(true);需要提醒的是开启安全模式后JSONType(autoTypeFilter)这类自动类型解析功能会受限如果业务里有依赖多态反序列化的场景比如父类引用指向不同子类安全模式可能直接导致解析失败。这种项目不能盲目开得先评估业务影响面再决定是用白名单还是安全模式。5.2 升级 2.x 后有哪些典型行为变化从 1.2.x 升级到 2.x不是说换个依赖就完事。两个版本在很多默认行为上不同我列几个容易踩的依赖坐标变了这是改pom.xml就完事的部分JSON类仍保留但JSONObject、JSONArray被重构个别 API 被删除或调整日期序列化默认值不同2.x 更偏向输出 ISO 格式化字符串而非纯时间戳TypeReference包名不变但一些注解如JSONType的子属性行为有变动序列化 null 字段的默认策略不同2.x 对部分集合类型的 null 处理更严格。如果你准备升级我建议用兼容包fastjson1-compatible这种方案过渡就是在fastjson2基础上引入一个兼容包它能让老代码继续用com.alibaba.fastjson包名但底层跑的是 fastjson2 引擎。这个方案适合老项目做低风险升级不过也要注意兼容包的行为差异最好让 QA 重点回归序列化结果。5.3 升级过程中的回归验证清单我不止一次吃过升级后输出格式变了没人发现的亏。做版本升级一定建一个序列化行为回归清单至少覆盖以下项检查项预期结果Date/LocalDateTime序列化格式确认格式是否变化前端是否有依赖null 字段是否输出确认新增字段或缺失字段是否影响接口兼容枚举序列化结果确认是输出枚举名还是枚举序号两端是否对齐BigDecimal序列化确认是否会被写成科学计数法嵌套泛型反序列化用TypeReference构造复杂类型做联调循环引用确认是否启用SerializerFeature.DisableCircularReferenceDetect安全模式/autoType确认多态场景是否可用这条清单列下来至少能挡住 80% 的升级回归问题。6. 性能调优与高频坑位循环引用、null值、枚举和字段过滤最后这块是我实际项目里碰到最多、也最值得拿出来讲的FastJson 用久了你不会挂在 API 记不住上而是挂在各种合理但反直觉的默认行为上。6.1 循环引用最常见的线上序列化崩溃假设你有一个对象图User持有Role集合Role又反向持有User引用。这种情况在 ORM 框架比如 JPA、MyBatis 多表关联里非常常见直接JSON.toJSONString(user)十有八九会抛StackOverflowError或出现包含$ref的奇怪输出。FastJson 默认是开启引用检测ReferenceDetect的它会在序列化循环引用时输出{$ref:$.roles[0].user}这样的引用标记而不是无限递归。这个设计起初是好的但问题在于前端拿到$ref根本不知道怎么回事很多解析器也不认识这个字段导致接口数据莫名其妙。我提供的解决思路有两种// 方案一全局关闭引用检测简单粗暴 String json JSON.toJSONString(user, SerializerFeature.DisableCircularReferenceDetect); // 方案二改造对象模型打破循环推荐 // 核心思路User 序列化时不带 roles或者 Role 里 user 字段加 JSONField(serialize false)实际项目中我更喜欢方案二的思路在 VO 层保证 DTO 对象图不出现循环引用因为循环引用本质上是领域模型的坏味道靠 JSON 库兜底只是治标。把两个方向的字段都暴露给前端还可能把多余数据泄露出去。6.2 null 字段与枚举两个默认策略你必须知道FastJson 默认不输出值为 null 的字段。比如对象有address null序列化结果里根本没有address这个 key。这在大部分场景下是干净利落的但问题出在接口对接上如果前端代码写了data.address.county而address字段缺失前端直接抛Cannot read property。要让 FastJson 输出 null 字段有两种方式// 方式一序列化时带上WriteMapNullValue String json JSON.toJSONString(user, SerializerFeature.WriteMapNullValue); // 方式二字段级别指定比如让String类型的null字段输出为空字符串 public class User { JSONField(serialzeFeatures SerializerFeature.WriteMapNullValue) private String address; }这里要敲个重点WriteMapNullValue只是让 null 字段出现在 JSON 里值仍然是null。如果你希望 null 变成空字符串还要额外加一个WriteNullStringAsEmpty。两者可以组合JSON.toJSONString(user, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullStringAsEmpty);枚举的序列化策略也是经典坑。FastJson 默认输出枚举的 name也就是RED、GREEN这种常量名如果后端枚举改了名字前端和数据库里存的旧值就全对不上了。如果你要输出枚举的序号或自定义值可以这样public enum Status { JSONField(value 1) PENDING, JSONField(value 2) APPROVED }或者更常见的场景是用枚举的 code 字段 一个JSONCreator反序列化构造方法实现接口传数字内部转枚举对象。6.3 字段过滤与脱敏你真的会排除字段吗业务中经常遇到要排除敏感字段或动态只返回部分字段的需求。FastJson 提供了多级过滤我的实战经验是按优先级整理如下最优先字段上加JSONField(serialize false)把敏感字段写死排除。接口级动态控制用PropertyPreFilter/SimplePropertyPreFilter根据请求方身份动态决定返回哪些字段。全局排除用SerializeConfig配TypeUtils全局忽略某些字段名。实际写出来大概是这样SimplePropertyPreFilter filter new SimplePropertyPreFilter(User.class, id, name, avatar); String json JSON.toJSONString(user, filter);这个 filter 放业务层做数据脱敏非常顺手比如管理员接口返回完整信息普通用户接口只返回 id、name。比在实体类上改注解灵活得多。还有个小坑如果字段名在 getter 和字段本身不一致SimplePropertyPreFilter里写的属性名要以序列化后的 key 为准不是 Java 字段名。我在这里吃过一次亏以为写了字段名就行结果过滤没生效脱敏字段还是被返出去了。6.4 我日常使用的一条基线配置说了这么多我晒一下我目前最舒服的一套 FastJson 配置新项目起步直接抄Configuration public class FastJsonConfig { Bean public HttpMessageConverters fastJsonHttpMessageConverter() { FastJsonHttpMessageConverter converter new FastJsonHttpMessageConverter(); FastJsonConfig config new FastJsonConfig(); config.setDateFormat(yyyy-MM-dd HH:mm:ss); config.setSerializerFeatures( SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullStringAsEmpty, SerializerFeature.DisableCircularReferenceDetect ); // 2.x 的 safeMode 开启方式 // JSONFactory.getDefaultObjectWriter().setSafeMode(true); converter.setFastJsonConfig(config); return new HttpMessageConverters(converter); } }这套配置能让 FastJson 输出格式更接近业务系统习惯日期可读、null 字段可见、空字符串不炸前端、循环引用不会导致栈溢出。需要特别说明DisableCircularReferenceDetect一旦开启有循环引用的对象图会真的无限递归直到栈溢出所以不要只靠它兜底真正的手段是前面说的模型改造。7. 关于FastJson我这几年的真实体会回到文章开头那个问题FastJson 到底还能不能用我的答案是可以但要懂事地用。所谓懂事第一层意思是懂版本——不要停在 1.2.x 老版本至少要升到 1.2.83 或直接用 2.x第二层意思是懂配置——日期格式、null 策略、安全模式这些要主动设置不能全靠默认第三层意思是懂边界——涉及到对外开放接口、高危敏感场景优先考虑 Jackson别把 FastJson 当成唯一选择。我在一个维护了五年的老系统里FastJson 已经深度嵌入到缓存序列化、MQ 消息体、对外接口等多个环节短时间更换不现实所以我的策略是双轨并行核心对外接口慢慢迁到 Jackson内部缓存和日志场景继续用 FastJson。这种渐近式改动比一刀切稳得多。最后分享一个小经验不管用哪个 JSON 库都建议在项目里封装一个JsonUtil工具类统一收口序列化和反序列化调用不要满代码库散落JSON.toJSONString。这样将来你要换 Jackson、要升级版本、要加统一脱敏逻辑只需要改一个类而不是全项目搜索替换。这个封装带来的收益会在你真正需要改造时被放大无数倍。
返回列表