利用 Lombok 与 MyBatis 实现 List 类型字段的自动序列化与反序列化
在 Java 后端开发中,将 List 类型字段存入数据库时的序列化与反序列化一直是个繁琐的问题。本文介绍如何通过 Lombok 和 MyBatis-Plus 的配合,用两个注解优雅地解决这个痛点。
一个常见的烦恼
写 Java 后端的同学大概都遇到过这样的场景:业务上需要在某个实体里存一组标签、一组编码,或者一组配置项,用 List 来表示再自然不过了。但到了落库的时候,关系型数据库压根不认识 List 这种类型,你不得不手动把它序列化成 JSON 字符串存进去,读的时候再反序列化回来。
这段转换代码写一次还好,写多了就会发现到处都是重复的 JSON.toJSONString() 和 JSON.parseArray(),既啰嗦又容易出错——少写一个泛型参数,运行时就给你一个 ClassCastException。这里的 JSON 字符串只是数据库层面的存储格式,业务代码不应该关心它。
有没有办法让框架帮我们把这件事自动做了?答案是有的,而且只需要两个注解。
方案对比:你有哪些选择
在动手之前,先看看处理 List 字段入库常见的几种方式:
| 方案 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 手动转换 | 业务代码中调用 JSON.toJSONString() / JSON.parseArray() | 简单直接,无额外依赖 | 重复代码多,泛型易出错 |
| 自定义 TypeHandler | 继承 BaseTypeHandler 手写序列化逻辑 | 灵活度高,可定制 | 每种类型都要写一个,维护成本高 |
| 注解方案(推荐) | @TableField(typeHandler = JacksonTypeHandler.class) | 零代码,声明式,通用性强 | 仅适用于 MyBatis-Plus |
对于 MyBatis-Plus 项目,注解方案是性价比最高的选择——零额外代码,声明即生效。
解决方案:两个注解搞定一切
核心思路很简单:让 MyBatis-Plus 的 JacksonTypeHandler 来接管序列化和反序列化的工作,再配合 Lombok 消除样板代码。来看一个完整的实体类:
@Data
@Accessors(chain = true)
@TableName(value = "data", autoResultMap = true)
public class Data {
@TableId(type = IdType.AUTO)
private Integer id;
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> code;
}
就是这样,没有多余的转换代码,没有手写的 TypeHandler,干净利落。
背后发生了什么
看起来只加了几个注解,但背后的协作机制值得了解一下,这样遇到问题时才知道往哪里排查。
[!WARNING]
autoResultMap = true是最常见的踩坑点。漏掉这个配置,写入没问题,但读出来的字段会是null或者一个原始的 JSON 字符串。很多人在这里卡半天,因为写入正常会让你以为配置没问题。
@TableName(autoResultMap = true) 是容易被忽略的一步。默认情况下 MyBatis-Plus 生成的 ResultMap 不会应用自定义的 TypeHandler,加上 autoResultMap = true 之后,框架才会在查询结果映射时使用你指定的处理器1。
@TableField(typeHandler = JacksonTypeHandler.class) 告诉框架这个字段需要特殊处理。在插入或更新时,JacksonTypeHandler 会调用 Jackson 把 List 序列化成 JSON 字符串写入数据库;在查询时,再把 JSON 字符串反序列化回 List 对象。整个过程对业务代码完全透明。
@Data 则是 Lombok 的经典注解,自动生成 getter、setter、toString()、equals() 和 hashCode(),让实体类保持极简。配合 @Accessors(chain = true),还能写出 new Data().setId(1).setCode(List.of("A", "B")) 这样的链式调用,代码读起来很流畅。
[!TIP] 如果你的 List 元素不是简单类型(比如
List<MyDTO>),JacksonTypeHandler 同样能处理,前提是 DTO 类需要有无参构造函数和标准的 getter/setter。
为什么选择这种方式
和手动转换相比,这种方案的优势不仅仅是少写几行代码:
- 不容易出错:手动转换时,序列化和反序列化的泛型类型必须严格匹配,稍有不慎就是运行时异常。交给 JacksonTypeHandler,类型推断由框架处理,出错概率大大降低。
- 改动集中:如果将来要换序列化方式(比如从 Jackson 换成 Fastjson),只需要替换 TypeHandler 的类名,不用满项目找手动转换的代码。
- 性能有保障:Jackson 是 Java 生态中最成熟的 JSON 库之一,内部做了大量优化,比自己手写的转换逻辑更可靠。
什么时候适合用
这种方案并不是万能的,它最适合以下场景:
- 字段存储的是简单列表数据(如标签、编码、配置项),不需要对列表元素做单独查询或索引。
- 数据结构相对稳定,不会频繁变更 List 内部元素的类型。
- 不想为了存几个字符串就多建一张关联表,一个 JSON 字段就能解决的事情,没必要过度设计。
但如果你需要对列表中的元素做复杂查询(比如”找出所有包含某个标签的记录”),那还是老老实实建关联表更合适,JSON 字段在这种场景下的查询性能和灵活性都不够理想。
[!NOTE] MySQL 5.7+ 支持
JSON_CONTAINS()函数对 JSON 字段做简单查询,但性能远不如关联表 + 索引。如果查询频率低、数据量小,JSON 字段凑合用;否则还是建表吧。
小结
回过头来看,这个方案的本质就是把重复性的类型转换工作交给框架去做。两个注解的配合——autoResultMap = true 负责读,JacksonTypeHandler 负责读写转换——覆盖了完整的序列化生命周期。代码量少了,出错的机会也少了,这才是工具和框架应该带给我们的价值。
Footnotes
-
autoResultMap = true的工作原理:MyBatis-Plus 在启动时会为每个实体类生成对应的 ResultMap。默认情况下,生成的 ResultMap 只包含简单的列名到字段名的映射。开启autoResultMap后,框架会扫描@TableField上声明的typeHandler,并将其注册到 ResultMap 的对应字段上,使得查询结果在映射回 Java 对象时自动经过 TypeHandler 的反序列化处理。详见 MyBatis-Plus 注解文档。 ↩