返回

利用 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

  1. autoResultMap = true 的工作原理:MyBatis-Plus 在启动时会为每个实体类生成对应的 ResultMap。默认情况下,生成的 ResultMap 只包含简单的列名到字段名的映射。开启 autoResultMap 后,框架会扫描 @TableField 上声明的 typeHandler,并将其注册到 ResultMap 的对应字段上,使得查询结果在映射回 Java 对象时自动经过 TypeHandler 的反序列化处理。详见 MyBatis-Plus 注解文档