在 Java 世界里让对象“旅行”:序列化与反序列化
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
的存在绝非“重复造轮子”,而是对底层实现做“策略封装”。下文的两段代码正是这一思路的极简落地。
示例代码段
//FastJSONpublic 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 fromJson(String json, Class 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) ========== public static T fromJson(String json, TypeReference 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); } }}
//Jacksonpublic 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 fromJson(String json, Class 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 fromJson(String json, TypeReference typeRef)
Java 类型擦除导致 List
在运行时只剩 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
$ref
JsonMappingException
TypeReference
TypeReference
(同名类)SerializeFilter
等扩展注:性能差异在大多数业务场景下可忽略,应优先考虑可维护性与安全。
五、从工具类到项目落地:一个完整的演进故事
假设某电商系统早期采用 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 字左右的梳理,能为你下一次技术选型或代码审查,提供一把“小而锋利”的瑞士军刀。