MyBatis 类型处理器(TypeHandler)注册与映射机制:JsonListTypeHandler和JsonListTypeHandler注册时机
下面几种机制会让你的 List<String>
/Map<String,?>
能正确读写成 JSON 数组/对象文本:
-
MyBatis-Plus 自动注册
最新版本的 MyBatis-Plus starter 会把类路径下所有带@MappedTypes({List.class})
、@MappedJdbcTypes(JdbcType.VARCHAR)
这类注解的TypeHandler
自动注册进TypeHandlerRegistry
,所以即使你不在ConfigurationCustomizer
里再手动registry.register(...)
,MyBatis-Plus 启动时也会把它们扫描进来。 -
字段注解强制指定
如果你的实体里写了@TableField(typeHandler = JsonListTypeHandler.class) private List<String> skills;
那 MyBatis 在构建映射的时候会直接 new 这个
JsonListTypeHandler
来处理该字段,不会再走默认的StringTypeHandler
。 -
JDBC 驱动的容错
就算真没有任何自定义 Handler,JDBC 驱动也会把你传进去的字符串(比如你在 Mapper XML 里写#{skills}
,skills.toString() 恰好是["a","b"]
)原封不动地当VARCHAR
存到 DB,当你再查回String
字段时,就能看到 JSON 文本。
所以你看到“即使把那两行全局注册删掉,也能正常上传/查询”,并不奇怪:
-
MyBatis-Plus Starter 在后台已经给你注册好了 List/Map 的 JSON 处理器;
-
你要么在实体里用注解显式选了 Handler,要么 DB 和驱动自己把字符串存下来了。
如果你真想验证“到底哪条路径在生效”,可以:
-
断点调试
JsonListTypeHandler#setNonNullParameter(...)
直接看看在执行插入/更新时,MyBatis 到底走的是哪个 Handler 的方法。 -
临时写个
ConfigurationCustomizer
打日志@Bean public ConfigurationCustomizer customizer() { return config -> { config.getTypeHandlerRegistry() .getTypeHandler(List.class) .ifPresent(h -> System.out.println("List 类型处理器是:" + h.getClass())); }; }
启动后控制台能看到到底用的哪个 Handler。
核心原理回顾
-
@MappedTypes
注解
在类上打了@MappedTypes(List.class)
或@MappedTypes(Map.class)
,MyBatis-Plus Starter 在启动时会 扫描 并 自动注册 这些类型处理器。-
如果你的版本里这个自动扫描没开,就算有
@MappedTypes
,MyBatis 也不会加载它,你就必须在ConfigurationCustomizer
里手动registry.register(...)
。
-
-
字段级别指定
如果你在实体类字段上写了@TableField(typeHandler = JsonListTypeHandler.class) private List<String> skills;
那就算全局没注册,MyBatis 也会给该字段硬绑定到这个 handler 上。
-
MyBatis-Plus 内置 JSON 处理器
新版本里,MP 自带了基于 Jackson 的JacksonTypeHandler
,它也会给带@MappedTypes
的类自动装上。
所以你如果单纯删掉全局注册,MP 可能已经自己把JsonListTypeHandler
/JsonMapTypeHandler
扫进去了,看起来就“好像不用注册也行”了。
为什么你“以前删掉会报错”?
-
旧版本 的 MP Starter 并不自动扫描你自己写的
JsonListTypeHandler
,也没给List.class
或Map.class
任何默认 handler。 -
这时,插入或查询带
List<String>
字段,就会抛错:Type handler was null for parameter List
-
你就逼不得已在
ConfigurationCustomizer
里手动注册,或者在字段上加@TableField(typeHandler=…)
,才跑通。
现在为啥又“删了也能正常”?
-
升级后,MP 已经把所有带
@MappedTypes
注解的 handler 自动扫描并注册 了; -
或者你给字段加了
@TableField(typeHandler=…)
,绕过了全局注册需求; -
又或者你的表里该列其实是
VARCHAR
/TEXT
,你直接在 service 里手动JSON.toString(list)
存,插入成功——但是反查时就只能拿到 String。
建议
-
保留你的 Handler 类,毕竟它能让你自动在实体里写
List<String>
/Map<String,?>
。 -
如果想最简化,全局注册可以删,但要确保你的 MP Starter 版本 ≥ 3.5 且内置扫描打开:
mybatis-plus: type-handlers-package: com.example.chat2.handler
-
或者在每个 JSON 字段上写:
@TableField(typeHandler = JsonListTypeHandler.class) private List<String> skills;
这样就算全局没扫到,也能按字段绑定。
小结
-
JsonListTypeHandler/JsonMapTypeHandler 本身是你项目必需的,只是注册方式可以有三条路:
-
全局
ConfigurationCustomizer
手动注册 -
@MappedTypes
+ MP 自动扫描 -
字段上
@TableField(typeHandler=…)
-
-
你“以前没写注册就报错”是因为那会儿自动扫描没开;现在自动扫描打开了,注册看似“多余”了,但在老版本或没有字段注解的情况下,它依然是必不可少的。
希望理清了为什么「看着好像多余」又「删掉又会报错」的来龙去脉!
而我是在xml写了这个
<result property="skills" column="skills" typeHandler="com.example.chat2.handler.JsonListTypeHandler"/>
<result property="tools" column="tools" typeHandler="com.example.chat2.handler.JsonListTypeHandler"/>
MyBatis 会直接按照你指定的 typeHandler 来做序列化/反序列化,这种方式和在 Java 里用 @MappedTypes 或者在全局注册处理器是等价的——它们都是最终把 JSON 文本和 List<String> 对象挂钩。
是XML 显式覆盖
在 <resultMap> 或 <select> 的 <result> 里写 typeHandler="...",MyBatis 在映射这个列的时候,跳过默认的类型推断,直接 new 指定的 JsonListTypeHandler。
不再依赖自动扫描
无需再借助 @MappedTypes、type-handlers-package 或者 ConfigurationCustomizer 全局注册,都能保证该字段走你想要的 Handler。
清晰直观
只要看 XML 就知道哪些列要走 JSON 处理,不会被其他配置“隐式”影响。
何时用哪种方式?
方式 | 优点 | 缺点 |
---|---|---|
XML 中 typeHandler | 最直观,按字段精确控制;不依赖额外扫描 | 每个字段都得在 XML 定义一次,比较啰嗦 |
字段注解 @TableField(typeHandler=…) | 配置集中在实体类;配合 MP 自动生成也能生效 | 如果你写 XML,而是用 MP 的 Wrapper/注解方式,则需要这样,XML 与注解混用时可能有重复 |
全局自动扫描(@MappedTypes + Starter 或者 ConfigurationCustomizer ) | 一次注册,全表所有 List /Map 列自动生效 | 控制粒度粗,所有同类型字段都会走同一个 Handler |
小贴士
如果你只在少数几个字段用 JSON,XML 显式 是最简单可靠的方式;
如果全项目大量用到,建议用 全局扫描 或者 字段注解,免得 XML 太长;
切勿同时对同一个字段在 XML、注解和全局注册里都写不同的 Handler,否则会有优先级混乱的问题。