Jackson 完全指南:从入门到精通
Jackson 完全指南:从入门到精通
做 Java 后端的,没人没碰过 JSON。而只要碰 JSON,基本绕不开 Jackson。
这篇文章是我踩了无数坑之后写的,把 Jackson 的核心原理、常用注解、日期时间处理、性能优化、常见坑点全部梳理了一遍,配合图表尽量说清楚。
一、Jackson 是什么?
Jackson 是 Java 生态里用得最多的 JSON 处理库,Spring Boot 默认就用的它。干的事情很简单:Java 对象和 JSON 之间互相转换。
市场地位
| 库名 | 市占率 | Spring Boot 默认 | 维护状态 |
|---|---|---|---|
| Jackson | ~80% | 是 | 活跃 |
| Gson | ~10% | 否 | 维护中 |
| Fastjson | ~5% | 否 | 漏洞多,不推荐 |
| JsonB | ~3% | 否 | 低活跃 |
在 Java 世界里用 JSON,默认选 Jackson 就对了。
二、核心概念:序列化与反序列化
Jackson 只做两件事——把 Java 对象变成 JSON,和把 JSON 变回 Java 对象。
最简代码
ObjectMapper mapper = new ObjectMapper();
// 序列化:Java → JSON
User user = new User("小明", 25);
String json = mapper.writeValueAsString(user);
// {"name":"小明","age":25}
// 反序列化:JSON → Java
String json = "{\"name\":\"小明\",\"age\":25}";
User user = mapper.readValue(json, User.class);
// User{name='小明', age=25}
序列化的完整调用链
写一行 writeValueAsString(),背后其实做了不少事:
三、ObjectMapper:Jackson 的核心引擎
ObjectMapper 是 Jackson 的心脏,所有操作都通过它完成。
核心类体系
ObjectMapper 是重量级对象,别每次都 new
这是个很多人忽略的点。new ObjectMapper() 并不轻量——它要初始化序列化/反序列化工厂、扫描注解、缓存反射信息、分配内部缓冲区。首次构建大概 50ms,内存占用 200KB 起步。
// 反面教材:每次请求都 new
public String toJson(Object obj) {
return new ObjectMapper().writeValueAsString(obj);
}
// 正确做法:全局复用
private static final ObjectMapper MAPPER = new ObjectMapper();
// 更好:Spring Bean
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(WRITE_DATES_AS_TIMESTAMPS);
}
ObjectMapper 本身是线程安全的,放心复用。
常用配置一览
ObjectMapper mapper = new ObjectMapper()
// 序列化
.enable(SerializationFeature.INDENT_OUTPUT)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
// 反序列化
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
这里面最常改的就两个:FAIL_ON_UNKNOWN_PROPERTIES(建议关掉,不然前后端字段不同步就炸)和 WRITE_DATES_AS_TIMESTAMPS(建议关掉,后面会说)。
四、注解体系:精确控制每一个字段
Jackson 的注解非常多,但日常用到的就那么几个。我按用途分了下类:
高频注解实战
@JsonProperty — 字段重命名
前后端命名规范不同时经常用。Java 这边叫 name,JSON 那边叫 user_name:
public class User {
@JsonProperty("user_name")
private String name;
@JsonProperty("user_age")
private int age;
}
// 输出:{"user_name":"小明","user_age":25}
@JsonIgnore / @JsonIgnoreProperties — 忽略字段
密码这种敏感字段,序列化时肯定不能让它出去:
@JsonIgnoreProperties({"password", "secret"}) // 类级忽略
public class User {
private String name;
@JsonIgnore
private String password;
private String secret;
}
// 输出:{"name":"小明"}
@JsonInclude — 控制空值输出
默认情况下 null 字段也会输出,很多时候不需要:
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
private String name;
private String hobby;
}
// hobby=null → {"name":"小明"}
// hobby="篮球" → {"name":"小明","hobby":"篮球"}
实际项目中用 NON_NULL 最多,省得前端一堆 xxx: null。
@JsonFormat — 日期格式化
public class Event {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
private LocalDateTime startTime;
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate birthday;
}
// {"startTime":"2024-05-08 10:30:00","birthday":"2000-01-15"}
注意 timezone 参数,不加的话可能跟你想的不一样。
@JsonCreator — 自定义构造
不可变对象没有 setter,反序列化时 Jackson 不知道怎么赋值,就得用 @JsonCreator 告诉它:
public class Money {
private final BigDecimal amount;
private final String currency;
@JsonCreator
public Money(
@JsonProperty("amount") BigDecimal amount,
@JsonProperty("currency") String currency
) {
this.amount = amount;
this.currency = currency;
}
}
@JsonTypeInfo + @JsonSubTypes — 多态序列化
接口或抽象类在序列化时会丢掉实际类型信息,反序列化时 Jackson 不知道该实例化哪个子类。这两个注解就是来解决这个问题的:
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = Circle.class, name = "circle"),
@JsonSubTypes.Type(value = Rectangle.class, name = "rectangle")
})
public abstract class Shape { }
Shape shape = mapper.readValue(json, Shape.class);
// 自动根据 "type" 字段选择 Circle 或 Rectangle
五、日期时间处理:最大的坑
这块我踩的坑最多。Java 8 引入了 java.time,但 Jackson 默认不认识 LocalDateTime 这些类型,直接序列化会报错。
痛点与解决
三种解决方式
方式 1:全局配置(推荐)
在 Spring Boot 项目里加一个配置类就够了:
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
return builder -> builder
.modulesToInstall(new JavaTimeModule())
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
}
这样整个项目的 ObjectMapper 都会带上这两个配置,不用每个地方手动加。
方式 2:字段级注解
只需要改个别字段的格式时用:
public class Event {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createdAt;
}
方式 3:手动注册模块
不用 Spring 管理 ObjectMapper 时:
ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(WRITE_DATES_AS_TIMESTAMPS);
Spring Boot 自动配置链路
加了这个 JacksonConfig 之后,整个配置是怎么生效的?
关键点:Spring 自动注入的 ObjectMapper 和 Controller 里 @RequestBody/@ResponseBody 用的是同一个实例,所以 JacksonConfig 的配置对整个项目生效。
六、高级特性
1. 泛型反序列化 — TypeReference
这个坑很多人踩过。因为 Java 泛型擦除,readValue(json, List.class) 拿到的其实是 List<LinkedHashMap>,不是 List<User>:
// 类型擦除:运行时 List.class 不携带元素类型
List<User> users = mapper.readValue(json, List.class); // 返回 List<LinkedHashMap>
// 用 TypeReference 保留泛型
List<User> users = mapper.readValue(json, new TypeReference<List<User>>() {});
// 嵌套泛型也没问题
Map<String, List<User>> map = mapper.readValue(json,
new TypeReference<Map<String, List<User>>>() {});
2. 树模型 — JsonNode
有时候 JSON 结构不确定,或者只想取里面几个字段,定义 Java 类太麻烦。这时候可以直接用 JsonNode 操作,类似前端操作 JSON 对象:
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(json);
// 读取
String name = root.get("name").asText();
int score = root.get("scores").get(0).asInt();
boolean hasAge = root.has("age");
// 遍历
root.fields().forEachRemaining(entry -> {
System.out.println(entry.getKey() + " = " + entry.getValue());
});
// 修改
ObjectNode obj = (ObjectNode) root;
obj.put("age", 25);
obj.remove("name");
obj.putArray("tags").add("VIP").add("活跃");
String newJson = mapper.writeValueAsString(obj);
3. 自定义序列化器 — JsonSerializer
有些业务场景需要特殊的序列化逻辑,比如金额想输出 "CNY 99.9" 而不是 {"amount":99.9,"currency":"CNY"}:
public class MoneySerializer extends JsonSerializer<Money> {
@Override
public void serialize(Money value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
gen.writeString(value.getCurrency() + " " + value.getAmount());
}
}
// 注册方式一:注解(只对这一个字段生效)
public class Order {
@JsonSerialize(using = MoneySerializer.class)
private Money price;
}
// 注册方式二:模块(全局生效)
SimpleModule module = new SimpleModule();
module.addSerializer(Money.class, new MoneySerializer());
mapper.registerModule(module);
4. 自定义反序列化器 — JsonDeserializer
反过来,从 "CNY 99.9" 还原成 Money 对象:
public class MoneyDeserializer extends JsonDeserializer<Money> {
@Override
public Money deserialize(JsonParser p, DeserializationContext ctx)
throws IOException {
String text = p.getValueAsString(); // "CNY 99.9"
String[] parts = text.split(" ");
return new Money(new BigDecimal(parts[1]), parts[0]);
}
}
public class Order {
@JsonDeserialize(using = MoneyDeserializer.class)
private Money price;
}
5. 三种数据处理模式对比
Jackson 其实有三种操作 JSON 的方式,日常用数据绑定就够了,但另外两种各有适用场景:
| 模式 | 适用场景 | 内存占用 | 代码量 |
|---|---|---|---|
| 数据绑定 | 已知 JSON 结构 | 中 | 少 |
| 树模型 | 不确定 JSON 结构、需要动态探索 | 高 | 中 |
| 流式 | 大文件处理、极致性能要求 | 极低 | 多 |
流式模式平时很少用到,除非你在处理几百 MB 的 JSON 文件,需要逐 token 读取而不是一口气全加载到内存。
七、性能优化
性能对比
| 操作 | Jackson | Gson | Fastjson2 |
|---|---|---|---|
| 序列化 (ops/ms) | ~850 | ~400 | ~750 |
| 反序列化 (ops/ms) | ~600 | ~300 | ~650 |
| 内存占用 | 中 | 高 | 低 |
| 大对象(>1MB) | 稳定 | 较慢 | 稳定 |
数据来自社区基准测试,具体数值因场景而异,但相对排名基本稳定:Jackson 和 Fastjson2 第一梯队,Gson 明显慢一截。
优化技巧
这里最容易忽视的是第 4 点。很多人习惯先 writeValueAsString() 再 getBytes() 写到 response,其实多了一次不必要的内存拷贝:
// 慢:多余的字符串拷贝
String json = mapper.writeValueAsString(obj);
response.getOutputStream().write(json.getBytes(StandardCharsets.UTF_8));
// 快:直接写到输出流
mapper.writeValue(response.getOutputStream(), obj);
八、常见坑与排雷指南
我把平时遇到最多的几个问题画了个决策树,碰到报错可以顺着往下找:
逐个展开
坑 1:未知字段报错
前后端字段不同步是家常便饭。后端加了新字段,老版本前端没有,反序列化直接报错。建议全局关掉这个校验:
mapper.disable(FAIL_ON_UNKNOWN_PROPERTIES);
// 或者只对某个类生效
@JsonIgnoreProperties(ignoreUnknown = true)
public class User { ... }
坑 2:日期时间序列化成数字
LocalDateTime 不注册 JavaTimeModule 就报错,注册了不关 TIMESTAMPS 就输出 1715145600000 这种时间戳,前端拿到一脸懵:
mapper.registerModule(new JavaTimeModule());
mapper.disable(WRITE_DATES_AS_TIMESTAMPS);
坑 3:泛型擦除变成 LinkedHashMap
Java 泛型在运行时会被擦除,List<User> 传进去运行时只剩 List,Jackson 只好给你塞 LinkedHashMap:
List<User> users = mapper.readValue(json, List.class); // 拿到 List<LinkedHashMap>
List<User> users = mapper.readValue(json,
new TypeReference<List<User>>() {}); // 拿到真正的 List<User>
坑 4:循环引用导致 StackOverflow
双向关联的对象,序列化 A 会去序列化 B,序列化 B 又回到 A,无限递归:
public class User {
@JsonIgnore // 打断循环
private List<Order> orders;
}
更优雅的方式是用 @JsonManagedReference + @JsonBackReference,在正向序列化时正常输出,反向时自动跳过。
坑 5:每次 new ObjectMapper
前面说过,不再赘述。一句话:全局复用。
坑 6:Spring Bean 和手动 new 的 ObjectMapper 不一致
这是我亲身踩过的坑。Spring 容器里的 ObjectMapper 注册了 JavaTimeModule、禁用了 TIMESTAMPS,但某个 Service 里手动 new ObjectMapper() 啥都没配,日期序列化出来的格式完全不一样,排查了半天。
// 项目中常见的错误
private static final ObjectMapper mapper = new ObjectMapper();
// 正确做法:注入 Spring 管理的
@Autowired
private ObjectMapper objectMapper;
九、实战:在本项目中的应用
拿我最近在做的项目举例,Jackson 在整个数据流中无处不在:
具体用法
// 解析 LLM Planner 返回的执行计划
List<Map<String, Object>> items = mapper.readValue(raw,
mapper.getTypeFactory().constructCollectionType(List.class, Map.class));
// 解析偏好提取结果
Map<String, String> kvs = mapper.readValue(raw, Map.class);
// 解析实体关系抽取 — 不确定 LLM 返回什么,用 readTree 安全些
JsonNode root = mapper.readTree(raw);
JsonNode entities = root.get("entities");
// Embedding 向量存入 PG — 向量是 List<Double>,存成 JSONB
String embJson = mapper.writeValueAsString(embedding);
// 任务快照 — 用序列化/反序列化做深拷贝
String json = mapper.writeValueAsString(currentTask);
TaskState copy = mapper.readValue(json, TaskState.class);
// Kafka 事件
String eventData = mapper.writeValueAsString(Map.of("query", query, "mode", mode));
项目中存在的问题
之前 review 代码时发现,这个项目手动创建了 5 个 ObjectMapper 实例,配置还不一致:
// UnifiedAgentService.java — 注册了 JavaTimeModule
private static final ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule());
// InfrastructureService.java — 注册了 JavaTimeModule
private static final ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule());
// LlmService.java — 啥都没注册
private static final ObjectMapper mapper = new ObjectMapper();
// ToolService.java — 啥都没注册
private static final ObjectMapper mapper = new ObjectMapper();
// HybridStore.java — 啥都没注册
private static final ObjectMapper mapper = new ObjectMapper();
后面三个碰到 LocalDateTime 就会炸。改成统一注入 Spring Bean 就好了:
@Service
public class LlmService {
private final ObjectMapper mapper;
public LlmService(ObjectMapper mapper) {
this.mapper = mapper;
}
}
十、速查表
常用 API
| 操作 | 方法 |
|---|---|
| 对象 → JSON 字符串 | mapper.writeValueAsString(obj) |
| 对象 → OutputStream | mapper.writeValue(outStream, obj) |
| JSON 字符串 → 对象 | mapper.readValue(json, MyClass.class) |
| JSON 字符串 → 泛型对象 | mapper.readValue(json, new TypeReference<List<MyClass>>(){}) |
| JSON 字符串 → 树 | mapper.readTree(json) |
| 对象深拷贝 | mapper.readValue(mapper.writeValueAsString(obj), MyClass.class) |
| 美化输出 | mapper.enable(INDENT_OUTPUT) |
| 忽略未知字段 | mapper.disable(FAIL_ON_UNKNOWN_PROPERTIES) |
| 注册模块 | mapper.registerModule(new JavaTimeModule()) |
注解速查
| 注解 | 作用 | 示例 |
|---|---|---|
@JsonProperty |
指定 JSON 字段名 | @JsonProperty("user_name") |
@JsonIgnore |
忽略字段 | @JsonIgnore private String password; |
@JsonIgnoreProperties |
类级忽略 | @JsonIgnoreProperties({"password"}) |
@JsonInclude |
控制空值 | @JsonInclude(NON_NULL) |
@JsonFormat |
日期格式 | @JsonFormat(pattern="yyyy-MM-dd") |
@JsonCreator |
自定义构造 | @JsonCreator public Obj(@JsonProperty(...) ...) |
@JsonSerialize |
自定义序列化器 | @JsonSerialize(using = XSerializer.class) |
@JsonDeserialize |
自定义反序列化器 | @JsonDeserialize(using = XDeserializer.class) |
@JsonTypeInfo |
多态类型信息 | @JsonTypeInfo(use=Id.NAME, property="type") |
@JsonSubTypes |
子类型映射 | @JsonSubTypes.Type(value=Circle.class, name="circle") |
@JsonAlias |
反序列化别名 | @JsonAlias({"userName", "user_name"}) |
@JsonRawValue |
原始 JSON | @JsonRawValue String config; |
@JsonValue |
整个对象当单值 | @JsonValue String getCode() |
@JsonView |
视图过滤 | @JsonView(Views.Public.class) |
模块速查
| 模块 | 依赖 | 作用 |
|---|---|---|
JavaTimeModule |
jackson-datatype-jsr310 | 支持 java.time |
Jdk8Module |
jackson-datatype-jdk8 | 支持 Optional、Stream |
ParameterNamesModule |
jackson-module-parameter-names | 构造器参数名推断 |
HibernateModule |
jackson-datatype-hibernate | 处理 Hibernate 懒加载 |
KotlinModule |
jackson-module-kotlin | 支持 Kotlin 数据类 |
BlackbirdModule |
jackson-module-blackbird | 编译期优化 |
AfterburnerModule |
jackson-module-afterburner | 字节码增强 |
总结
用好 Jackson 说到底就三件事:复用 ObjectMapper、注册需要的 Module、善用注解控制细节。把这三点做到,基本就不会踩坑了。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)