JSON(JavaScript Object Notation)是 Java 后端开发中最常见的数据交换格式之一。本文以 Jackson 为主线,结合 Spring Boot 中的实际使用场景,梳理 JSON 序列化与反序列化的基本原理、常用配置、典型坑点以及与 Fastjson2、Gson 等库的选型差异。
一、什么是序列化与反序列化
序列化是将内存中的 Java 对象转换为可传输或可存储的格式(如 JSON 字符串、字节流)的过程,反序列化则是逆向操作。
javascript
Java Object ──序列化──► JSON 字符串 / 字节流
Java Object ◄──反序列化── JSON 字符串 / 字节流
在分布式系统中,JSON 序列化几乎无处不在:HTTP 接口(Spring MVC @RequestBody/@ResponseBody)、RPC 调用(Dubbo 的 JSON 协议)、消息队列(RocketMQ 消息体)、缓存(Redis 存储对象)、日志打印等场景。
二、Java 生态三大主流库对比
| 库 | 维护方 | 性能 | 功能丰富度 | 典型应用场景 |
|---|---|---|---|---|
| Jackson | FasterXML | 高 | 最全 | Spring Boot 默认、企业级应用 |
| Fastjson / Fastjson2 | 阿里巴巴 | 极高 | 丰富 | 国内互联网公司、高性能场景 |
| Gson | 中 | 一般 | Android、简单场景 |
在 Spring Boot 2.x/3.x 生态中,Jackson 是默认且最常用的 JSON 处理方案,尤其适合企业级 Web 接口、配置转换和通用 DTO 序列化场景。需要注意的是,本文主要基于 Jackson 2.x 体系展开;如果项目升级到 Spring Boot 4.x,则需要关注 Jackson 3 的包名、配置方式和自动装配变化。
Fastjson 1.x 曾因 AutoType 机制引发过多次反序列化安全问题,Fastjson2 对安全模型和实现进行了重构。实际选型时,不建议只看"性能更高"这一个指标,还要结合 Spring 生态兼容性、团队熟悉度、安全策略和压测结果综合判断。
三、Jackson 核心原理
1. 三个核心组件
markdown
ObjectMapper ─ 入口类,管理配置、缓存 Serializer/Deserializer
│
├── JsonFactory ─ 创建 JsonParser / JsonGenerator
├── SerializerProvider ─ 查找并缓存序列化器
└── DeserializationContext ─ 查找并缓存反序列化器
ObjectMapper 是相对重量级的对象,内部会缓存类型信息、序列化器和反序列化器。生产环境中应尽量将其作为单例 Bean 复用 ,而不是在每次请求或每次工具方法调用时反复 new ObjectMapper()。需要注意的是,ObjectMapper 的线程安全前提是:所有配置应在首次序列化/反序列化之前完成;一旦开始使用,就不建议再动态修改配置。若只是临时调整输出格式,优先使用不可变且线程安全的 ObjectReader / ObjectWriter。
2. 序列化流程
BeanSerializerFactory] D --> E E --> F[JsonGenerator 写入 Token] F --> G([JSON String]) %% 定义样式类 (配色方案) %% io: 输入/输出节点 (淡蓝色) classDef io fill:#E1F5FE,stroke:#0288D1,stroke-width:2px,color:#01579B %% process: 常规处理步骤 (浅灰色) classDef process fill:#F5F5F5,stroke:#9E9E9E,stroke-width:2px,color:#424242 %% decision: 条件判断节点 (淡橙色) classDef decision fill:#FFF3E0,stroke:#F57C00,stroke-width:2px,color:#E65100 %% core: 核心执行机制 (淡绿色) classDef core fill:#E8F5E9,stroke:#388E3C,stroke-width:2px,color:#1B5E20 %% 绑定节点与样式 class A,G io class B,F process class C decision class D,E core
Jackson 首次序列化某类型时,通过反射(BeanIntrospector)分析字段、getter、注解,构建 BeanSerializer 并缓存。后续同类型直接复用,这是 Jackson 高性能的关键。
3. 基础用法
java
ObjectMapper mapper = new ObjectMapper();
// 序列化
User user = new User("wei", 30);
String json = mapper.writeValueAsString(user);
// {"name":"wei","age":30}
// 反序列化
User u = mapper.readValue(json, User.class);
// 泛型集合反序列化(必须用 TypeReference)
List<User> list = mapper.readValue(json, new TypeReference<List<User>>(){});
为什么泛型要用 TypeReference ? Java 泛型编译后会擦除,List<User>.class 在运行时只是 List.class,Jackson 无法知道元素类型。TypeReference 通过匿名内部类保留泛型信息(利用 Class.getGenericSuperclass() 获取 ParameterizedType)。
四、常用注解详解
java
public class Order {
@JsonProperty("order_id") // 改字段名(驼峰 <-> 下划线常见场景)
private Long orderId;
@JsonIgnore // 序列化/反序列化都忽略
private String password;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime; // 格式化日期
@JsonInclude(JsonInclude.Include.NON_NULL) // null 不参与序列化
private String remark;
@JsonSerialize(using = MoneySerializer.class) // 自定义序列化器
private BigDecimal amount;
@JsonAlias({"user_name", "userName"}) // 反序列化时多个名称都能识别
private String name;
@JsonCreator // 指定反序列化使用的构造器
public Order(@JsonProperty("order_id") Long orderId) { ... }
}
@JsonInclude 可以放在类上全局生效,Spring Boot 中常通过全局配置统一处理。
五、全局配置(Spring Boot)
说明:本节配置主要面向 Spring Boot 2.x/3.x 与 Jackson 2.x。如果项目使用 Spring Boot 4.x,需要关注 Jackson 3 的包名、自动配置类和定制入口变化,不能直接照搬 Jackson 2.x 的所有写法。
在 Spring Boot 项目中,强烈不建议 直接通过 @Bean public ObjectMapper objectMapper() 的方式去完全覆盖默认配置。因为这可能绕过 Spring Boot 已经提供的自动配置能力(如参数名发现模块 ParameterNamesModule、JDK8 模块 Jdk8Module)以及 application.yml 中的 spring.jackson.* 配置全部失效。
更优雅且符合生产规范的做法是实现 Jackson2ObjectMapperBuilderCustomizer 进行增量定制:
java
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
return builder -> {
// 命名策略:驼峰转下划线
builder.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
// null 值不序列化
builder.serializationInclusion(JsonInclude.Include.NON_NULL);
// 未知字段不抛异常(前后端字段不对齐时非常重要)
builder.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
// 时间不以时间戳形式输出
builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// 注:Spring Boot 默认已注册 JavaTimeModule,若需对 LocalDateTime 局部/全局指定特定格式,可在此定制
builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
};
}
}
在普通业务接口中,通常建议关闭 FAIL_ON_UNKNOWN_PROPERTIES,以提升前后端字段演进的兼容性;但在强契约、配置解析、安全敏感接口中,可以保留严格校验,避免未知字段被静默忽略。
六、核心坑点与实战问题
1. Long 类型精度丢失
JavaScript Number 采用 IEEE 754 双精度,能精确表示的整数范围是 253。后端 Long(雪花 ID 常见 19 位)传到前端会丢精度。
java
// 全局将 Long 序列化为 String(不推荐盲目全局配置)
SimpleModule module = new SimpleModule();
module.addSerializer(Long.class, ToStringSerializer.instance);
module.addSerializer(Long.TYPE, ToStringSerializer.instance);
mapper.registerModule(module);
注意: 盲目全局把 Long 转 String 属于粗暴治标。这会导致分页参数(如 total、pageSize)、时间戳(timestamp)、状态枚举等不需要解决精度的字段全部变成字符串,极易引发前端框架的类型校验报错。
工程最佳实践 :采用局部精准打击。只在雪花 ID 或超长业务主键字段上单独挂载注解:
java
// 方式一:直接转 String
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
// 方式二:通过指定 Shape 属性转换为 String
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long orderId;
2. BigDecimal 科学计数法与金额精度
BigDecimal 常用于金额、汇率、计量值等高精度场景。序列化时有时会输出类似 1.0E+2、5E-10 这样的科学计数法,虽然语义上仍然是数字,但可能影响前端展示、签名验签或第三方接口字段校验。
java
mapper.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);
需要注意的是,BigDecimal 不建议为了展示方便先转成 double,否则会重新引入浮点数精度问题。更稳妥的做法是:计算层保留 BigDecimal,展示层根据业务需要控制小数位和字符串格式。
3. LocalDateTime 序列化/反序列化问题
Jackson 2.x 的核心包本身不直接处理 Java 8 时间类型,项目中通常需要引入 jackson-datatype-jsr310,并注册 JavaTimeModule。在 Spring Boot 2.x/3.x 项目中,如果使用 spring-boot-starter-web,通常已经由自动配置完成了模块注册;但如果自己完全覆盖了 ObjectMapper,或者在非 Spring 环境中手动创建 ObjectMapper,就需要自己注册模块。
java
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
如果只是希望统一 LocalDateTime 的输出格式,可以优先通过 Spring Boot 配置或 Jackson2ObjectMapperBuilderCustomizer 增量定制,而不是重新声明一个全新的 ObjectMapper。
4. 循环引用导致栈溢出
双向关联(如 User 有 List,Order 有 User)会无限递归。
java
// 方案 1:@JsonManagedReference + @JsonBackReference
// 方案 2:@JsonIgnore 打断一方
// 方案 3:@JsonIdentityInfo(使用对象 ID 引用)
5. 反序列化时多态类型丢失
接收端无法知道 JSON 对应的具体子类。
java
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = WechatMessage.class, name = "wechat"),
@JsonSubTypes.Type(value = SmsMessage.class, name = "sms")
})
public abstract class Message { ... }
注意:Fastjson 1.x 历史漏洞的根源就是默认开启 AutoType,允许 JSON 中指定任意类(@type)然后反射创建,攻击者可构造恶意类实现 RCE。Jackson 的 @JsonTypeInfo 是显式白名单,相对安全,但也不要用 Id.CLASS(等价于 AutoType)。
反序列化 RCE(Deserialization Remote Code Execution)是指攻击者利用系统在把外部传入的数据(如 JSON 字符串)转换回内存对象(反序列化)时的安全漏洞,强迫服务器执行恶意代码的过程。
6. ObjectMapper 线程安全但配置不是
ObjectMapper 一旦开始使用(序列化/反序列化)就不要再修改配置。需要不同配置时,用 mapper.copy() 创建副本,或使用 ObjectReader/ObjectWriter(不可变,线程安全)。
java
ObjectWriter writer = mapper.writer().with(SerializationFeature.INDENT_OUTPUT);
String pretty = writer.writeValueAsString(obj);
七、性能考量
- ObjectMapper 单例化:最重要的一条。
- 避免使用
JsonNode遍历大对象 :JsonNode是 DOM 模型,全部加载到内存。处理大 JSON 流用JsonParser(SAX 风格,Token 流)。 - 字段尽量少:序列化成本和字段数量成正比,DTO 按需定义,不要直接返回 PO。
- 热点对象可以评估 Afterburner / Blackbird 模块:这类模块通过减少反射开销来提升序列化/反序列化性能。其中 Blackbird 更适合 JDK 11+ 环境。
java
mapper.registerModule(new BlackbirdModule()); // JDK 11+
// 或 mapper.registerModule(new AfterburnerModule()); // 老版本
在性能敏感场景中,建议使用项目真实 DTO、真实字段规模和真实 QPS 进行压测。不同 JSON 库在不同数据结构下的表现可能不同,不能只根据单一 benchmark 就决定技术选型。
八、Fastjson2 简要对照
如果团队在用 Fastjson2,API 设计接近 Fastjson 1.x,迁移成本低:
java
// 序列化
String json = JSON.toJSONString(user);
// 反序列化
User u = JSON.parseObject(json, User.class);
List<User> list = JSON.parseArray(json, User.class); // 泛型不需要 TypeReference
// 特性通过 JSONWriter.Feature / JSONReader.Feature 控制
String json2 = JSON.toJSONString(user,
JSONWriter.Feature.WriteNulls,
JSONWriter.Feature.PrettyFormat);
Fastjson2 默认关闭 AutoType,安全性大幅提升,性能在大部分场景优于 Jackson。
选型建议:新项目优先 Jackson(生态广、Spring 原生支持);性能敏感或已有 Fastjson 基础的项目可用 Fastjson2。
九、总结
JSON 序列化虽然是 Java 后端开发中的基础能力,但在实际项目中很容易引发接口兼容、时间格式、精度丢失、循环引用和反序列化安全等问题。
在 Spring Boot 2.x/3.x 项目中,Jackson 通常是首选方案。使用时要注意:ObjectMapper 应尽量单例复用,并在首次使用前完成配置;泛型反序列化要使用 TypeReference 保留类型信息;LocalDateTime 需要确认已注册 JavaTimeModule;雪花 ID 等超长 Long 字段建议按需转为字符串,避免前端精度丢失。
对于复杂对象,应优先通过 DTO 控制返回结构,避免直接序列化数据库实体导致字段暴露或循环引用。涉及多态反序列化时,应使用明确的白名单类型映射,避免使用基于 Java 类名的类型标识。
性能优化方面,优先关注 ObjectMapper 复用、字段裁剪和大 JSON 的流式处理,再根据真实压测结果决定是否引入 Fastjson2、Blackbird 等方案。