Java JSON 序列化原理与实战问题总结

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 Google 一般 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. 序列化流程

flowchart LR %% 定义节点与基础结构 (同时保留了形状优化) A([Java Object]) --> B[ObjectMapper.writeValueAsString] B --> C{查找 Serializer 缓存} C -->|命中| E[执行序列化] C -->|未命中| D[反射分析类结构
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);

注意: 盲目全局把 LongString 属于粗暴治标。这会导致分页参数(如 totalpageSize)、时间戳(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+25E-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 等方案。

相关推荐
hexu_blog1 小时前
前端vue后端java+springboot如何实现pdf,word,excel之间的相互转换
java·前端·vue.js·spring boot·文档转换
贺国亚1 小时前
synchronized- 并发
java·面试
martian6651 小时前
在 IntelliJ IDEA 中安装、配置 Claude Code 及解决连接错误完全指南
java·ide·intellij-idea
lalala_Zou1 小时前
某大厂后端一面
java·开发语言
爱笑的源码基地1 小时前
拿来即用:基于Spring Cloud+UniApp的智慧工地源码,架构清晰易扩展
java·云计算·源码·智慧工地·程序·开箱即用·数字工地
WL_Aurora1 小时前
Java技术体系:JDK、JRE、JVM的关系与演进(2026最新版)
java·开发语言·jvm
砚底藏山河1 小时前
股票数据API接口:(沪深A股)如何获取股票当天逐笔交易数据
java·windows·python·maven
小江的记录本2 小时前
【MySQL】MySQL日志体系:redo log/undo log/binlog 三者区别、两阶段提交、如何保证数据一致性
java·数据库·后端·python·sql·mysql·面试
摇滚侠2 小时前
Java 饿汉式 单例模式
java·开发语言·单例模式