下面几种机制会让你的 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,否则会有优先级混乱的问题。