序列化本质与 Jackson

一、⚡ 序列化是什么:Java 对象和 JSON 之间的翻译官

日常写的代码里全是 Java 对象——OrderUserProduct。但网络传输只能传二进制/文本,数据库只能存文本,前端浏览器只认得 JSON。怎么把 Java 对象变成 JSON、再把 JSON 变回 Java 对象?这就是序列化和反序列化

序列化(Serialization):    Java 对象  →  JSON / XML / 二进制
反序列化(Deserialization): JSON / XML / 二进制  →  Java 对象

如果不做序列化,你就得手动拼 JSON:

// 没有序列化工具——手动拼 JSON(又臭又长)
public String orderToJson(Order order) {
    return "{"
        + "\"orderId\":" + order.getOrderId() + ","
        + "\"userId\":" + order.getUserId() + ","
        + "\"productName\":\"" + escape(order.getProductName()) + "\","
        + "\"amount\":" + order.getAmount()
        + "}";
}
// 手写这段代码的时候,你就知道自己需要一个序列化工具了

序列化框架做的事就是自动完成这个转换——你要做的只是加几个注解、调一行方法。

1.1 序列化的两个方向

// 你写的是左边,JSON 是右边
// 序列化:左边 → 右边(ObjectMapper.writeValueAsString)
// 反序列化:右边 → 左边(ObjectMapper.readValue)

Order order = new Order(10001L, 2001L, "iPhone 15", new BigDecimal("6999.00"), "created", LocalDateTime.now());

// 序列化后(JSON):
{
  "orderId": 10001,
  "userId": 2001,
  "productName": "iPhone 15",
  "amount": 6999.00,
  "action": "created",
  "createTime": "2024-01-15T10:30:00"
}

1.2 JSON、XML、二进制——三种序列化格式

格式可读性体积解析速度适用场景
JSON高(人眼可读)HTTP API、配置文件、前端通信
XML旧系统、SOAP 协议
二进制(Protobuf / Hessian2)低(不可读)最小最快RPC 微服务内部通信

Jackson、Fastjson、Gson 都是JSON 序列化工具——只处理 JSON。Protobuf、Hessian2 是二进制序列化。

二、🧬 Jackson 是什么:SpringBoot 的"隐形"默认序列化器

如果你用过 SpringBoot,你已经在用 Jackson 了——只是你不知道:

@RestController
public class OrderController {

    @GetMapping("/api/order/{id}")
    public Order getOrder(@PathVariable Long id) {
        Order order = orderService.getOrderById(id);
        return order;  // ← 这里!SpringBoot 自动调用 Jackson 把 Order 对象序列化为 JSON
    }
}

// curl http://localhost:8080/api/order/10001
// 返回:{"orderId":10001,"userId":2001,"productName":"iPhone 15","amount":6999.00}
//       ↑ 这就是 Jackson 干的活

SpringBoot 的 spring-boot-starter-web 默认依赖了 jackson-databind。当你写 return order 时,Spring 的 MappingJackson2HttpMessageConverter 拦截返回值,调 ObjectMapper.writeValueAsString(order) 转成 JSON,再写到 HTTP Response Body。整个过程对开发者透明——你只看到 return 了一个对象,浏览器收到了 JSON。

flowchart LR
classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
classDef data fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold;
classDef highlight fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;

    CTRL([Controller\nreturn order]) -->|"返回 Java 对象"| MC["MappingJackson2HttpMessageConverter\nSpring 的 HTTP 消息转换器"]
    MC -->|"调用"| MAPPER["ObjectMapper\nJackson 的核心类"]
    MAPPER -->|"writeValueAsString"| JSON["{\n  #quot;orderId#quot;: 10001,\n  #quot;productName#quot;: #quot;iPhone 15#quot;\n}"]
    JSON -->|"写入 HTTP Response Body"| BROWSER([浏览器收到 JSON])

    class CTRL,BROWSER startEnd;
    class MC highlight;
    class MAPPER process;
    class JSON data;

三、🔧 ObjectMapper —— Jackson 的万能工具箱

3.1 基本用法

// ObjectMapper 是 Jackson 最核心的类——一切转换都通过它
ObjectMapper mapper = new ObjectMapper();

// ===== 序列化:Java 对象 → JSON 字符串 =====
Order order = new Order(10001L, 2001L, "iPhone 15",
        new BigDecimal("6999.00"), "created", LocalDateTime.now());
String json = mapper.writeValueAsString(order);
System.out.println(json);
// 输出:{"orderId":10001,"userId":2001,"productName":"iPhone 15","amount":6999.00,"action":"created","createTime":"2024-01-15T10:30:00"}

// ===== 反序列化:JSON 字符串 → Java 对象 =====
String jsonStr = "{\"orderId\":10001,\"userId\":2001,\"productName\":\"iPhone 15\",\"amount\":6999.00}";
Order parsedOrder = mapper.readValue(jsonStr, Order.class);
System.out.println(parsedOrder.getProductName()); // 输出:iPhone 15

// ===== 反序列化:JSON 字节数组 → Java 对象 =====
byte[] jsonBytes = jsonStr.getBytes(StandardCharsets.UTF_8);
Order fromBytes = mapper.readValue(jsonBytes, Order.class);

3.2 用 Java 8 的 Optional 优雅拿值

// Jackson 的 JsonNode(Tree Model)——不想定义 Java 类时用
String response = "{\"code\":200,\"message\":\"success\",\"data\":{\"orderId\":10001,\"amount\":6999.00}}";

JsonNode root = mapper.readTree(response);

// 链式取值——干净利落
int code = root.get("code").asInt();
String message = root.get("message").asText();
BigDecimal amount = new BigDecimal(root.get("data").get("amount").asText());

// 安全取值
Optional.ofNullable(root.get("data"))
        .map(data -> data.get("amount"))
        .map(JsonNode::asText)
        .map(BigDecimal::new)
        .ifPresent(amt -> System.out.println("金额: " + amt));

3.3 格式化输出(调试专用)

// 紧凑模式——网络传输
String compact = mapper.writeValueAsString(order);
// {"orderId":10001,"userId":2001,"productName":"iPhone 15","amount":6999.00}

// 美化模式——日志/调试
String pretty = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(order);
/*
{
  "orderId" : 10001,
  "userId" : 2001,
  "productName" : "iPhone 15",
  "amount" : 6999.00
}
*/

四、🏷️ Jackson 注解大全 —— 控制序列化的一切

4.1 @JsonProperty —— 改字段名、控制顺序

public class Order {

    @JsonProperty("order_id")      // JSON 中的字段名变成 order_id
    private Long orderId;

    @JsonProperty(value = "user_id", index = 1)  // index 控制 JSON 输出顺序(越小越前)
    private Long userId;

    @JsonProperty(value = "product_name", index = 2)
    private String productName;

    // JSON 输出:
    // {
    //   "user_id": 2001,       ← index=1,排第一
    //   "product_name": "iPhone 15", ← index=2,排第二
    //   "order_id": 10001       ← 没指定 index,按声明顺序
    // }
}

4.2 @JsonIgnore / @JsonIgnoreProperties —— 隐藏字段

// 方式一:标注在单个字段上
public class User {
    private Long userId;
    private String username;

    @JsonIgnore  // 序列化时完全忽略这个字段——不会出现在 JSON 中
    private String password;

    // 输出:{"userId": 2001, "username": "yaomingye"}
    // password 字段不存在——前端永远收不到密码
}

// 方式二:标注在类上——忽略多个字段
@JsonIgnoreProperties({"password", "salt", "internalId"})
public class User {
    private Long userId;
    private String username;
    private String password;
    private String salt;
    private Long internalId;
    // 序列化输出只包含 userId 和 username
}

// 方式三:忽略未知字段——防止反序列化时炸
@JsonIgnoreProperties(ignoreUnknown = true)
public class Order {
    // 已有的字段...
    // 如果前端多传了一个 "note" 字段,反序列化时不会报错——直接忽略
}

4.3 @JsonFormat —— 控制日期/数字格式

public class Order {

    // 日期格式化
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    // 输出:"createTime": "2024-01-15 10:30:00"

    // 时区控制
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;

    // 数字格式化
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private BigDecimal amount;
    // 输出:"amount": "6999.00"
    // 不加这个注解 → 输出:"amount": 6999.00 —— JS 可能精度丢失
}

⚠️ 新手提示:BigDecimal 一定要加 @JsonFormat(shape = JsonFormat.Shape.STRING)。JavaScript 的 Number 类型最大安全整数是 2^53-1(约 9007199254740991),超过这个值精度就丢了。BigDecimal 做金额计算精度极高——但 JSON 传输时 JavaScript 会把它当成 Number 处理,容易丢精度。金额字段必须用字符串传

4.4 @JsonInclude —— 过滤 null 值和空值

// 标注在类上——控制序列化时哪些字段被包含
@JsonInclude(JsonInclude.Include.NON_NULL)  // null 字段不输出
public class Order {
    private Long orderId;
    private String productName;      // 如果是 null → 不出现在 JSON 中
    private BigDecimal amount;
    private String remark;           // 如果是 null → 不出现在 JSON 中
}

// Order 对象:orderId=10001, productName=null, amount=6999.00, remark=null
// 序列化输出:{"orderId": 10001, "amount": 6999.00}
// 而不是:     {"orderId": 10001, "productName": null, "amount": 6999.00, "remark": null}
Include 策略行为
NON_NULLnull 字段不输出
NON_EMPTYnull + 空字符串 + 空集合 + 空数组不输出
NON_DEFAULT等于默认值的字段不输出(int=0, boolean=false, String=null 等)
ALWAYS(默认)全部输出——包括 null

4.5 @JsonPropertyOrder —— 按指定顺序输出

@JsonPropertyOrder({"userId", "orderId", "amount", "productName"})
public class Order {
    private Long orderId;
    private Long userId;
    private String productName;
    private BigDecimal amount;
    // JSON 输出严格按照注解中的顺序——不管字段声明顺序
}

4.6 @JsonAlias —— 别名(兼容多个字段名)

public class Order {

    @JsonAlias({"order_id", "orderId", "id"})
    private Long orderId;
    // 反序列化时:order_id / orderId / id 三个名字都能映射到 orderId 字段
    // 序列化时:输出用 @JsonProperty 的名,没有 @JsonProperty 就用字段名
}

4.7 @JsonSerialize / @JsonDeserialize —— 自定义序列化器

// 场景:敏感信息脱敏——手机号中间四位变 ****
public class PhoneDesensitizeSerializer extends JsonSerializer<String> {
    @Override
    public void serialize(String value, JsonGenerator gen,
                          SerializerProvider serializers) throws IOException {
        if (value != null && value.length() == 11) {
            gen.writeString(value.substring(0, 3) + "****" + value.substring(7));
        } else {
            gen.writeString(value);
        }
    }
}

public class User {
    @JsonSerialize(using = PhoneDesensitizeSerializer.class)
    private String phone;
    // 序列化:"phone": "138****5678"
}

五、ObjectMapper 全局配置

5.1 SpringBoot 中定制 ObjectMapper

@Configuration
public class JacksonConfig {

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
        return builder -> {
            // 日期格式
            builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
            // 时区
            builder.timeZone("GMT+8");
            // null 字段不输出
            builder.serializationInclusion(JsonInclude.Include.NON_NULL);
            // 空 Bean 不报错(默认 true——空 Bean 序列化抛异常)
            builder.failOnEmptyBeans(false);
            // 未知字段不报错
            builder.failOnUnknownProperties(false);
            // 缩进输出(调试用——生产关闭)
            builder.indentOutput(false);
            // BigDecimal 用字符串输出——防止 JS 精度丢失
            builder.featuresToEnable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);
        };
    }
}
# 或者直接 yml——更简洁
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
    default-property-inclusion: non_null
    serialization:
      write-dates-as-timestamps: false  # 日期不转时间戳
    deserialization:
      fail-on-unknown-properties: false  # 未知字段不报错
    generator:
      write-bigdecimal-as-plain: true   # BigDecimal 不用科学计数法

5.2 关键配置项速查

配置yml 路径默认值建议
日期格式spring.jackson.date-formatyyyy-MM-dd HH:mm:ss
时区spring.jackson.time-zoneUTCGMT+8
null 不输出spring.jackson.default-property-inclusion: non_nullalwaysnon_null
未知字段报错spring.jackson.deserialization.fail-on-unknown-propertiestruefalse——生产必关
BigDecimal 科学计数法spring.jackson.generator.write-bigdecimal-as-plainfalsetrue——必须开
日期转时间戳spring.jackson.serialization.write-dates-as-timestampstruefalse——人更愿意看字符串

六、泛型反序列化 —— TypeReference 解决擦除

ObjectMapper mapper = new ObjectMapper();

// 场景:反序列化包含泛型的 API 响应
String response = """
    {
      "code": 200,
      "data": [
        {"orderId": 10001, "amount": 6999.00},
        {"orderId": 10002, "amount": 1999.00}
      ]
    }
    """;

// ❌ 错误写法——泛型擦除导致 ClassCastException
List<Order> orders = mapper.readValue(
    mapper.readTree(response).get("data").toString(),
    List.class  // 这里拿到的实际是 List<LinkedHashMap>,不是 List<Order>!
);

// ✅ 正确写法——用 TypeReference 保留泛型信息
List<Order> orders = mapper.readValue(
    mapper.readTree(response).get("data").toString(),
    new TypeReference<List<Order>>() {}  // ← 匿名子类保留泛型
);

for (Order o : orders) {
    System.out.println(o.getAmount());  // 6999.00, 1999.00
}

TypeReference 的原理:Java 泛型在编译后会被擦除——List<Order>.class 不存在,只有 List.classTypeReference 通过创建匿名子类——子类的字节码中保留了父类的泛型参数信息——Jackson 通过反射读取这个信息来实现正确反序列化。

七、多态反序列化 —— @JsonTypeInfo

场景:一个字段的声明类型是父类/接口,但 JSON 中可能传任意子类:

// 动物——可能是猫也可能是狗
@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,       // 通过类型名称区分
    property = "type"                  // JSON 中的 type 字段标识具体类型
)
@JsonSubTypes({
    @JsonSubTypes.Type(value = Cat.class, name = "cat"),
    @JsonSubTypes.Type(value = Dog.class, name = "dog")
})
public abstract class Animal {
    private String name;
}

public class Cat extends Animal {
    private boolean indoor;
}

public class Dog extends Animal {
    private String breed;
}

// JSON:
// {"type": "cat", "name": "小白", "indoor": true}
//   → 反序列化为 Cat 对象

// {"type": "dog", "name": "大黄", "breed": "金毛"}
//   → 反序列化为 Dog 对象

// 使用——Animal 类型的字段可以接收任意子类
public class PetOwner {
    private Animal pet;  // 这里只写 Animal——Jackson 根据 type 字段自动选择子类
}

八、Jackson 的注册模块 —— 处理 JDK 8+ 特殊类型

<!-- Jackson 本身不理解 Java 8 的 LocalDateTime、Optional 等类型——需要额外模块 -->
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
// 注册模块
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());  // 支持 LocalDateTime、LocalDate
mapper.registerModule(new Jdk8Module());      // 支持 Optional、Stream
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // 日期不转时间戳

// SpringBoot 自动引入了 jackson-datatype-jsr310——不用手动注册

九、常见坑与 FAQ

问题原因解决
InvalidDefinitionException: No serializer found for class类中没有 getter 方法——Jackson 默认通过 getter 方法获取字段值给类加 public getter,或配 mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY)
UnrecognizedPropertyException: Unrecognized field "xxx"JSON 中有字段但 Java 类没有——Jackson 默认报错@JsonIgnoreProperties(ignoreUnknown = true) 或 yml 配 fail-on-unknown-properties: false
前端收到的时间是一串数字(1705284000000Jackson 默认把日期序列化为时间戳spring.jackson.serialization.write-dates-as-timestamps: false
BigDecimal 变成 6.999E+3Jackson 默认用科学计数法写 BigDecimalspring.jackson.generator.write-bigdecimal-as-plain: true
order_id 字段映射不到 orderIdJackson 默认按驼峰匹配——字段名 order_id → 对应的 Java 属性是 setOrder_id()@JsonProperty("order_id")
List<Order> 反序列化后变成 List<LinkedHashMap>泛型擦除new TypeReference<List<Order>>() {}

🎯 总结

  1. 序列化是 Java 对象和 JSON 之间的翻译官:序列化 = Java 对象 → JSON(输出),反序列化 = JSON → Java 对象(输入)。SpringBoot 默认用 Jackson 做这个翻译——return order 那一行背后就是 ObjectMapper。

  2. Jackson 的注解就是控制开关@JsonProperty(改字段名)、@JsonIgnore(隐藏字段)、@JsonFormat(格式化日期/数字)、@JsonInclude(过滤 null/空值)、@JsonAlias(兼容多个别名)。不需要改业务代码——加注解就行。

  3. BigDecimal 必须用字符串传@JsonFormat(shape = JsonFormat.Shape.STRING) 或全局配 write-bigdecimal-as-plain: true。这不是可选项——JavaScript 的 Number 精度不够。

  4. 泛型反序列化必须用 TypeReferencenew TypeReference<List<Order>>() {}——泛型擦除后 Jackson 无法知道 List 里装什么类型。

  5. SpringBoot yml 配置优先于代码 Bean:绝大多数 Jackson 全局设置都可以用 yml 一行搞定——不需要写 @Bean Jackson2ObjectMapperBuilderCustomizer

📖 下一步阅读:Jackson 掌握了。但国内很多老项目用的是 Fastjson——它曾经是最快的 JSON 库,也爆出过一串高危漏洞。Fastjson 2 号称 “API 不变但安全了”。继续阅读 Fastjson 进化史:从 1.x 漏洞到 Fastjson2