Java 生态里关于 JSON 的序列化与反序列化(以下简称“序列化”)是一个久经考验的话题,却常因框架繁多、配置琐碎而让初学者望而却步。本文将围绕一段极简的
JsonUtils
工具类展开,以 FastJSON 与 Jackson 两大主流实现为例,从原理到实践、从特性到隐患,做一次系统梳理。文章力求以学术写作之严谨,帮助读者在 3000 字左右完成一次由点及面的进阶。
目录
一、为什么需要“工具类”而非直接调用框架 API
示例代码段
二、FastJSON 实现细节与行为解读
2.1 序列化:统一日期格式与循环引用控制
2.2 反序列化:TypeReference 的价值
2.3 异常策略:IllegalArgumentException 而非底层异常
三、Jackson 实现细节与行为解读
3.1 ObjectMapper 的线程安全
3.2 空 Bean 与日期格式
3.3 异常处理:IOException 的简化
四、横向对比:FastJSON vs Jackson
五、从工具类到项目落地:一个完整的演进故事
5.1 迁移步骤
5.2 兼容性陷阱
六、再谈防御式编程:边界条件的“三重门”
七、小结与展望
一、为什么需要“工具类”而非直接调用框架 API
无论 FastJSON 还是 Jackson,其 API 都足够简洁:JSON.toJSONString(obj)
或 objectMapper.writeValueAsString(obj)
即可完成序列化。然而生产环境中,我们往往需要在“一致性”“防御式编程”“可追踪”“可扩展”四个维度做额外约束。
- 一致性:日期格式、空值策略、循环引用检测等行为必须全局统一。
- 防御式编程:对 null、空串、非法 JSON 的入参给出明确兜底。
- 可追踪:异常信息须携带上下文(对象类型、原始 JSON 片段)。
- 可扩展:未来切换实现(如从 FastJSON 迁移到 Jackson)时业务代码零改动。
因此,一个 JsonUtils
的存在绝非“重复造轮子”,而是对底层实现做“策略封装”。下文的两段代码正是这一思路的极简落地。
示例代码段
//FastJSON
public final class JsonUtils {private static final Logger logger = LoggerFactory.getLogger(JsonUtils.class);// ========== 构造器 ==========private JsonUtils() {}// ========== 序列化 ==========public static String toJson(Object obj) {if (obj == null) {return "null";}try {return JSON.toJSONString(obj,SerializerFeature.DisableCircularReferenceDetect,SerializerFeature.WriteDateUseDateFormat); // 统一日期格式} catch (Exception e) {logger.error("Serialize object to JSON failed. Object={}", obj, e);throw new IllegalArgumentException("JSON serialize error", e);}}// ========== 反序列化(单个对象) ==========public static <T> T fromJson(String json, Class<T> clazz) {if (json == null || json.isEmpty()) {return null;}try {return JSON.parseObject(json, clazz);} catch (Exception e) {logger.error("Deserialize JSON to {} failed. JSON={}", clazz.getSimpleName(), json, e);throw new IllegalArgumentException("JSON deserialize error", e);}}// ========== 反序列化(复杂泛型,如 List<User>) ==========public static <T> T fromJson(String json, TypeReference<T> typeRef) {if (json == null || json.isEmpty()) {return null;}try {return JSON.parseObject(json, typeRef);} catch (Exception e) {logger.error("Deserialize JSON to {} failed. JSON={}", typeRef.getType(), json, e);throw new IllegalArgumentException("JSON deserialize error", e);}}
}
//Jackson
public final class JsonUtils {private static final ObjectMapper MAPPER = new ObjectMapper().disable(SerializationFeature.FAIL_ON_EMPTY_BEANS).setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));private JsonUtils() {}public static String toJson(Object obj) {if (obj == null) return "null";try {return MAPPER.writeValueAsString(obj);} catch (JsonProcessingException e) {throw new IllegalArgumentException("Serialize error", e);}}public static <T> T fromJson(String json, Class<T> clazz) {if (json == null || json.isEmpty()) return null;try {return MAPPER.readValue(json, clazz);} catch (IOException e) {throw new IllegalArgumentException("Deserialize error", e);}}
}
二、FastJSON 实现细节与行为解读
FastJSON 由阿里巴巴开源,以“快”著称,实现上大量依赖 ASM 动态字节码生成,将反射开销降至极低。在给出的 FastJSON 版 JsonUtils
中,三条语句几乎涵盖日常 90% 的场景。
2.1 序列化:统一日期格式与循环引用控制
return JSON.toJSONString(obj,SerializerFeature.DisableCircularReferenceDetect,SerializerFeature.WriteDateUseDateFormat);
-
DisableCircularReferenceDetect
关闭循环引用检测。FastJSON 默认会为循环引用生成$ref
,这在 RESTful 返回中常因前端无法解析而踩坑。关闭后,若实际出现循环引用将直接抛JSONException
,用“快速失败”换取“数据干净”。 -
WriteDateUseDateFormat
强制使用全局日期格式(yyyy-MM-dd HH:mm:ss
)。FastJSON 内部维护一个DateFormat
线程局部变量,因此该配置对性能几乎无损耗。
2.2 反序列化:TypeReference 的价值
public static <T> T fromJson(String json, TypeReference<T> typeRef)
Java 类型擦除导致 List<User>
在运行时只剩 List
。FastJSON 的 TypeReference
借助匿名内部类保存泛型签名,绕过擦除,反序列化时即可还原完整类型。这一点在 Jackson 中对应 TypeReference
同名类,设计思路如出一辙。
2.3 异常策略:IllegalArgumentException 而非底层异常
FastJSON 抛出的 JSONException
继承自 RuntimeException
,工具类将其包装为 IllegalArgumentException
,语义上更接近“参数非法”。这一转换使得调用方无需显式捕获受检异常,同时保持日志链路完整。
三、Jackson 实现细节与行为解读
Jackson 是 Spring 生态的默认 JSON 方案,模块丰富、扩展点繁多。在 JsonUtils
的 Jackson 实现中,配置集中在静态 ObjectMapper
的初始化块。
3.1 ObjectMapper 的线程安全
官方文档明确指出:ObjectMapper
在配置完成后是线程安全的。因此工具类将其声明为 static final
,避免重复创建带来的元数据开销(SerializerProvider
、DeserializerCache
等)。但需注意,若在运行时调用 setXxx
方法修改配置,则线程安全假设将被打破。
3.2 空 Bean 与日期格式
MAPPER.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS).setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
-
FAIL_ON_EMPTY_BEANS
默认开启,当对象无任何可序列化属性时抛异常。关闭后,此类对象会被序列化为{}
,避免 DTO 在演进过程中因新增字段全部@JsonIgnore
而意外崩溃。 -
SimpleDateFormat
非线程安全,但ObjectMapper
会将其包裹成线程局部变量,因此配置一次即可。
3.3 异常处理:IOException 的简化
Jackson 的 writeValueAsString
声明抛出 JsonProcessingException
(继承 IOException
)。工具类同样将其转换为 IllegalArgumentException
,与 FastJSON 保持行为统一,降低上层心智负担。
四、横向对比:FastJSON vs Jackson
维度 | FastJSON | Jackson |
---|---|---|
性能 | 高(ASM 生成字节码) | 中高(3.x 版本已大幅优化) |
默认日期格式 | 时间戳 | 时间戳 |
循环引用处理 | 默认使用 $ref | 默认抛出 JsonMappingException |
泛型反序列化 | TypeReference | TypeReference (同名类) |
安全配置(autoType) | 曾出现 RCE 漏洞,需开启 safemode | 默认白名单机制,漏洞面更小 |
社区活跃度 | 国内高,国际一般 | 国际主流,Spring 默认 |
扩展性 | 支持 SerializeFilter 等扩展 | 模块机制丰富(Joda、Kotlin 等) |
注:性能差异在大多数业务场景下可忽略,应优先考虑可维护性与安全。
五、从工具类到项目落地:一个完整的演进故事
假设某电商系统早期采用 FastJSON,后因安全审计要求全面迁移至 Jackson。若直接使用框架 API,则改动面巨大;而借助 JsonUtils
,仅需替换实现即可。
5.1 迁移步骤
- 保留原有
JsonUtils
类签名,内部实现替换为 Jackson。 - 通过全局搜索验证无直接调用
JSON.parseXxx
的代码。 - 运行单元测试,重点观察日期格式、Long 型精度、BigDecimal 精度是否变化。
- 灰度发布,通过日志比对线上 JSON 输出差异。
5.2 兼容性陷阱
-
浮点精度:FastJSON 默认关闭
WriteNullNumberAsZero
,Jackson 需手动配置SerializationFeature.WRITE_NULL_NUMBERS_AS_ZERO
。 -
Long 精度:前端 JavaScript 最大安全整数为 2^53-1,后端 Long 超过此范围需序列化为字符串。FastJSON 可配置
BrowserCompatible
,Jackson 需自定义ToStringSerializer
。
六、再谈防御式编程:边界条件的“三重门”
工具类虽小,却肩负第一道防线。以下三点常被忽视:
-
null 与空串:FastJSON 允许
JSON.parseObject("", clazz)
返回 null,而 Jackson 会抛异常。工具类统一返回 null,避免调用方差异。 -
异常日志:必须记录原始 JSON 片段,但需脱敏(如手机号、身份证)。可引入 SPI 机制,让业务模块提供
SensitiveDataFilter
。 -
线程局部泄漏:若使用 ThreadLocal 缓存
SimpleDateFormat
,务必在 Tomcat 热部署时调用remove
,防止类加载器泄露。
七、小结与展望
序列化是“数据在 JVM 与网络之间最后一公里”的工程。FastJSON 与 Jackson 各有千秋,工具类则是屏蔽差异、沉淀团队规范的最佳载体。未来随着 Java 21 的 Vector API、Project Valhalla 的 value objects 落地,序列化的底层实现或将迎来新一轮变革。但万变不离其宗:统一配置、防御式编程、可观测三板斧,仍将长期适用。
希望这篇 3000 字左右的梳理,能为你下一次技术选型或代码审查,提供一把“小而锋利”的瑞士军刀。