MyBatis-Plus13:自定义 TypeHandler
一、自定义 TypeHandlerTypeHandler 是 MyBatis 中负责Java 类型 ↔ 数据库类型之间转换的处理器。当内置的处理器满足不了需求时就需要自定义。二、使用场景最典型的就是数据库存 JSON 字符串Java 里想直接用对象接收数据库{name:张三,age:22} ←→ JavaAddress 对象三、实现步骤第一步定义 Java 对象Data public class Address { private String province; private String city; }第二步编写自定义 TypeHandler继承BaseTypeHandlerT实现4个方法MappedTypes(Address.class) // 声明处理的Java类型 MappedJdbcTypes(JdbcType.VARCHAR) // 声明处理的数据库类型 public class AddressTypeHandler extends BaseTypeHandlerAddress { private static final ObjectMapper mapper new ObjectMapper(); // 写入数据库Java对象 → 字符串 Override public void setNonNullParameter(PreparedStatement ps, int i, Address address, JdbcType jdbcType) throws SQLException { ps.setString(i, mapper.writeValueAsString(address)); } // 查询时映射字符串 → Java对象按列名 Override public Address getNullableResult(ResultSet rs, String columnName) throws SQLException { return parse(rs.getString(columnName)); } // 查询时映射字符串 → Java对象按列下标 Override public Address getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return parse(rs.getString(columnIndex)); } // 存储过程用 Override public Address getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return parse(cs.getString(columnIndex)); } private Address parse(String json) { try { if (json null) return null; return mapper.readValue(json, Address.class); } catch (Exception e) { throw new RuntimeException(JSON解析失败, e); } } }第三步实体类中使用TableName(value users, autoResultMap true) // 必须开启 Data public class Users { private Integer id; private String username; TableField(typeHandler AddressTypeHandler.class) // 指定处理器 private Address address; }第四步注册 TypeHandler二选一方式一配置文件注册mybatis-plus: type-handlers-package: com.example.handler # 扫描你的handler包方式二直接在TableField上指定就不需要全局注册用哪个指哪个即可。四、整个链路插入时 Address对象 → AddressTypeHandler → JSON字符串 → 数据库 查询时 数据库 → JSON字符串 → AddressTypeHandler → Address对象五、和 JacksonTypeHandler 的关系MyBatis-Plus 内置了JacksonTypeHandler和FastjsonTypeHandler如果只是简单的 JSON 对象映射直接用内置的就行不需要自定义TableField(typeHandler JacksonTypeHandler.class) private Address address;自定义 TypeHandler 适合处理内置处理器搞不定的场景比如加密存储、特殊格式转换、压缩存储等。六、完整的例子1我用一个完整的例子来讲解用户表中有一个hobbies字段数据库存的是 JSON 字符串[篮球,足球,游泳]Java 里想用ListString来接收。第一步先看数据库表结构CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50), hobbies VARCHAR(500) -- 存 JSON 字符串比如 [篮球,足球] );第二步理解 TypeHandler 要做什么你可以把 TypeHandler 理解成一个翻译官存数据时ListString [篮球,足球] → 翻译成 → 字符串 [篮球,足球] → 存入数据库取数据时字符串 [篮球,足球] → 翻译成 → ListString [篮球,足球] → 返回给Java第三步编写 TypeHandlerpackage com.example.handler; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedTypes; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; MappedTypes(List.class) // 告诉MyBatis这个处理器是处理 List 类型的 public class ListTypeHandler extends BaseTypeHandlerListString { // ObjectMapper 是 Jackson 库的核心类用来做 JSON 转换 private static final ObjectMapper mapper new ObjectMapper(); /** * 存数据时调用把 Java 的 ListString 转成 JSON 字符串存入数据库 * ps可以理解成数据库操作对象 * i第几个参数 * parameter就是你传进来的 ListString */ Override public void setNonNullParameter(PreparedStatement ps, int i, ListString parameter, JdbcType jdbcType) throws SQLException { try { // 把 [篮球,足球] 这个List转成字符串 [篮球,足球] String json mapper.writeValueAsString(parameter); ps.setString(i, json); } catch (Exception e) { throw new SQLException(List转JSON失败, e); } } /** * 取数据时调用按列名查询把数据库的 JSON 字符串转成 ListString * columnName数据库列名比如 hobbies */ Override public ListString getNullableResult(ResultSet rs, String columnName) throws SQLException { return parse(rs.getString(columnName)); } /** * 取数据时调用按列的下标查询把数据库的 JSON 字符串转成 ListString * columnIndex第几列从1开始 */ Override public ListString getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return parse(rs.getString(columnIndex)); } /** * 存储过程时调用一般用不到但必须实现 */ Override public ListString getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return parse(cs.getString(columnIndex)); } /** * 抽取一个公共方法把 JSON 字符串转成 ListString */ private ListString parse(String json) { try { if (json null || json.isEmpty()) { return null; } // 把字符串 [篮球,足球] 转回 ListString return mapper.readValue(json, new TypeReferenceListString() {}); } catch (Exception e) { throw new RuntimeException(JSON转List失败原始值 json, e); } } }第四步实体类中使用TableName(value users, autoResultMap true) // autoResultMap必须为true否则查询时不生效 Data public class Users { TableId(type IdType.AUTO) private Integer id; private String username; // 指定用我们自定义的 ListTypeHandler 来处理这个字段 TableField(typeHandler ListTypeHandler.class) private ListString hobbies; }第五步注册 TypeHandler在application.yml中告诉 MyBatis-Plus 去哪里找我们的处理器mybatis-plus: type-handlers-package: com.example.handler # 改成你自己的包路径第六步测试效果存数据Users user new Users(); user.setUsername(张三); user.setHobbies(List.of(篮球, 足球, 游泳)); usersService.save(user);此时数据库hobbies字段存的是[篮球,足球,游泳]取数据Users user usersService.getById(1); ListString hobbies user.getHobbies(); System.out.println(hobbies); // [篮球, 足球, 游泳]自动就转回ListString了完全不需要手动处理。整体流程图存数据 Java代码 → ListString[篮球,足球] → ListTypeHandler.setNonNullParameter() → [篮球,足球] → 数据库 取数据 数据库 → [篮球,足球] → ListTypeHandler.getNullableResult() → parse() 方法解析 → ListString[篮球,足球] → Java代码【备注】对于ListString的java ---数据库的映射不需要自己手写typehandler以上只是示例实际只需要加TableField(typeHandler JacksonTypeHandler.class)即可。常见错误查询时字段一直是 null检查TableName里有没有加autoResultMap true这是最常见的遗漏。JSON解析报错检查数据库里存的值格式是否正确手动查一下看看是不是合法的 JSON 格式。找不到 TypeHandler检查application.yml里的包路径是否和你的 Handler 实际所在包一致。七、完整示例2场景描述用户的手机号、身份证号属于敏感信息监管要求必须加密存储在数据库中但 Java 代码里操作的时候要用明文。存入数据库13812345678 → 加密 → a3f8c2d1e9b7...密文 从数据库取a3f8c2d1e9b7...密文 → 解密 → 13812345678这种场景用JacksonTypeHandler完全搞不定必须自定义。第一步准备一个简单的加密工具类public class AesUtil { private static final String KEY 1234567890abcdef; // 16位密钥实际项目放配置文件 // 加密明文 → 密文 public static String encrypt(String content) { try { Cipher cipher Cipher.getInstance(AES/ECB/PKCS5Padding); SecretKeySpec keySpec new SecretKeySpec(KEY.getBytes(), AES); cipher.init(Cipher.ENCRYPT_MODE, keySpec); byte[] encrypted cipher.doFinal(content.getBytes()); return Base64.getEncoder().encodeToString(encrypted); } catch (Exception e) { throw new RuntimeException(加密失败, e); } } // 解密密文 → 明文 public static String decrypt(String content) { try { Cipher cipher Cipher.getInstance(AES/ECB/PKCS5Padding); SecretKeySpec keySpec new SecretKeySpec(KEY.getBytes(), AES); cipher.init(Cipher.DECRYPT_MODE, keySpec); byte[] decoded Base64.getDecoder().decode(content); return new String(cipher.doFinal(decoded)); } catch (Exception e) { throw new RuntimeException(解密失败, e); } } }第二步自定义 TypeHandlerMappedTypes(String.class) public class EncryptTypeHandler extends BaseTypeHandlerString { // 存数据库时明文 → 加密 → 存密文 Override public void setNonNullParameter(PreparedStatement ps, int i, String plainText, JdbcType jdbcType) throws SQLException { ps.setString(i, AesUtil.encrypt(plainText)); } // 取数据库时密文 → 解密 → 返回明文按列名 Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { return decrypt(rs.getString(columnName)); } // 取数据库时密文 → 解密 → 返回明文按列下标 Override public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return decrypt(rs.getString(columnIndex)); } // 存储过程 Override public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return decrypt(cs.getString(columnIndex)); } private String decrypt(String cipherText) { if (cipherText null || cipherText.isEmpty()) return null; return AesUtil.decrypt(cipherText); } }第三步实体类中使用TableName(value users, autoResultMap true) Data public class Users { TableId(type IdType.AUTO) private Integer id; private String username; // 手机号加密存储 TableField(typeHandler EncryptTypeHandler.class) private String phone; // 身份证号加密存储 TableField(typeHandler EncryptTypeHandler.class) private String idCard; // 普通字段不需要加密 private String email; }测试效果存数据Users user new Users(); user.setUsername(张三); user.setPhone(13812345678); // 传明文 user.setIdCard(110101199001011234); // 传明文 usersService.save(user);数据库实际存的是phone: a3f8c2d1e9b74f2a... 密文看不出原始手机号 idCard: 9c2e1d8f3a7b6e4c... 密文取数据Users user usersService.getById(1); System.out.println(user.getPhone()); // 13812345678 自动解密成明文 System.out.println(user.getIdCard()); // 110101199001011234 自动解密成明文为什么这个场景必须自定义因为这个需求是在 Java 和数据库之间做了额外的业务处理加解密不是简单的类型转换任何内置的 TypeHandler 都做不到只能自己写。类似的场景还有数据压缩存储、手机号脱敏显示、特殊格式转换等都是自定义 TypeHandler 的典型使用场景。八、setNonNullParameter()方法详解先理解这个方法是干什么的当你执行usersService.save(user)时MyBatis-Plus 底层会构建一条 SQLINSERT INTO users (phone) VALUES (?)这个?是占位符MyBatis 需要把你 Java 里的13812345678填进去。填之前就会调用这个方法你可以在这里对值做任何处理然后再填入。逐个参数解释public void setNonNullParameter( PreparedStatement ps, // 参数1 int i, // 参数2 String plainText, // 参数3 JdbcType jdbcType // 参数4 )PreparedStatement ps就是那条带?的 SQL 语句对象可以理解成一个容器等着你把值填进去。int i是第几个?从1开始。比如 SQL 是INSERT INTO users (phone, idCard) VALUES (?, ?)phone对应i1idCard对应i2。String plainText就是你 Java 代码里传进来的原始值比如13812345678。JdbcType jdbcType是数据库的字段类型比如 VARCHAR、INT 等这里一般用不到。方法体解释ps.setString(i, AesUtil.encrypt(plainText));拆开来看就是String cipherText AesUtil.encrypt(plainText); // 第一步把明文加密成密文 ps.setString(i, cipherText); // 第二步把密文填入第i个?占位符ps.setString(i, 值)的意思就是把值填入 SQL 的第 i 个问号。整个流程串起来你写的代码 user.setPhone(13812345678) usersService.save(user) ↓ MyBatis构建SQL INSERT INTO users (phone) VALUES (?) ↓ 调用 setNonNullParameter(ps, 1, 13812345678, VARCHAR) ↓ 方法内部执行 AesUtil.encrypt(13812345678) → a3f8c2d1... ps.setString(1, a3f8c2d1...) ↓ 最终执行的SQL INSERT INTO users (phone) VALUES (a3f8c2d1...) ↓ 数据库存的是密文所以这个方法就是一个拦截器的作用在值真正写入数据库之前偷偷把它加密了。

相关新闻

MyBatis-Plus12:JSON处理器

MyBatis-Plus12:JSON处理器

一、JSON 类型 在 Mysql中的使用MySQL 5.7.8 版本开始就原生支持 JSON 类型了。1-1、基本用法建表时直接用 JSON 作为字段类型:CREATE TABLE orders (id INT PRIMARY KEY AUTO_INCREMENT,user_id INT,extra JSON -- JSON 类型字段 );插入数据:INSERT IN…

2026/5/17 5:47:33 阅读更多 →
经专业测评筛选的9款AI降重工具,采用NLP深度改写技术,确保输出内容符合学术规范要求

经专业测评筛选的9款AI降重工具,采用NLP深度改写技术,确保输出内容符合学术规范要求

每到毕业季,最让人头疼的就是论文查重了。辛辛苦苦写的论文,一查重竟然高达40%,那种绝望感只有经历过的人才懂。传统的降重方法又费时又费力,改来改去还可能把专业术语都改没了。 好在现在有了AI降重工具,真正实现了“…

2026/7/4 3:03:59 阅读更多 →
传统降重太耗时?这9个AI网站10秒完成高质量改写,语义保留度超95%,效率提升20倍

传统降重太耗时?这9个AI网站10秒完成高质量改写,语义保留度超95%,效率提升20倍

每到毕业季,最让人头疼的就是论文查重了。辛辛苦苦写的论文,一查重竟然高达40%,那种绝望感只有经历过的人才懂。传统的降重方法又费时又费力,改来改去还可能把专业术语都改没了。 好在现在有了AI降重工具,真正实现了“…

2026/5/17 5:47:32 阅读更多 →

最新新闻

AMD Ryzen调试工具SMUDebugTool:5步解锁处理器隐藏性能

AMD Ryzen调试工具SMUDebugTool:5步解锁处理器隐藏性能

AMD Ryzen调试工具SMUDebugTool:5步解锁处理器隐藏性能 【免费下载链接】SMUDebugTool A dedicated tool to help write/read various parameters of Ryzen-based systems, such as manual overclock, SMU, PCI, CPUID, MSR and Power Table. 项目地址: https://g…

2026/7/6 6:20:52 阅读更多 →
如何在FGO中实现自动化战斗:Fate/Grand Automata完整技术指南

如何在FGO中实现自动化战斗:Fate/Grand Automata完整技术指南

如何在FGO中实现自动化战斗:Fate/Grand Automata完整技术指南 【免费下载链接】FGA Auto-battle app for F/GO Android 项目地址: https://gitcode.com/gh_mirrors/fg/FGA Fate/Grand Automata(FGA)是一款专为《Fate/Grand Order》玩家…

2026/7/6 6:18:51 阅读更多 →
绝对真理的不可动摇性与当代学术泡沫——基于哥德尔定理、皮亚诺公理及科学哲学的综合批判

绝对真理的不可动摇性与当代学术泡沫——基于哥德尔定理、皮亚诺公理及科学哲学的综合批判

绝对真理的不可动摇性与当代学术泡沫——基于哥德尔定理、皮亚诺公理及科学哲学的综合批判摘要:本文以“绝对真理”(如算术基本事实“112”)为锚点,系统检视当代西方学术主流中两类“软科学”现象:一类是形式主义公理化…

2026/7/6 6:16:50 阅读更多 →
AI商业洞察动态简报(2026.07.05)

AI商业洞察动态简报(2026.07.05)

第1条:快手可灵AI完成30亿美元融资,估值达150亿美元商业价值:可灵AI成立于2023年,是快手旗下的AI视频生成模型业务。本轮融资创下全球视频大模型公司最大额融资纪录,投资者涵盖产业资本(腾讯、阿里云、百度…

2026/7/6 6:16:50 阅读更多 →
吾爱大佬出品,可离线的识别工具!一键提取图片文字、图片表格、PDF文字!

吾爱大佬出品,可离线的识别工具!一键提取图片文字、图片表格、PDF文字!

软件获取 OCR识别软件 使用提示: 1、 打开软件后,点击"截图识别"即可框选识别区域。 2、点击"导入文件"可以选择图片或 PDF。 3、"设置"里可以设置保存目录、自动复制、自动保存、HTTP服务、自定义API 等。 4、"…

2026/7/6 6:14:50 阅读更多 →
鸿蒙新特性:CalendarPicker 日历组件详解——构建一个日程管理应用

鸿蒙新特性:CalendarPicker 日历组件详解——构建一个日程管理应用

日历是时间管理类应用中最高频的交互界面之一。HarmonyOS NEXT ArkUI 提供了 CalendarPicker 组件,以月视图网格的方式展示完整日历,支持年份月份滑动切换、日期选中高亮和自定义日期范围。配合事件数据,可以轻松构建日程管理、酒店预订、排班…

2026/7/6 6:14:50 阅读更多 →

日新闻

H2 与 MySQL 单元测试兼容性:5 个关键 SQL 语句差异与规避方案

H2 与 MySQL 单元测试兼容性:5 个关键 SQL 语句差异与规避方案

H2与MySQL单元测试兼容性:5个关键SQL语句差异与规避方案1. 单元测试中的数据库兼容性挑战在Java开发领域,单元测试是保证代码质量的重要环节。当应用涉及数据库操作时,测试环境的搭建往往成为开发者的痛点。H2数据库因其轻量级、内存模式和快…

2026/7/6 0:01:17 阅读更多 →
Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘

Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘

Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘 【免费下载链接】rbtray A fork of RBTray from http://sourceforge.net/p/rbtray/code/. 项目地址: https://gitcode.com/gh_mirrors/rb/rbtray 你是否厌倦了Windows任务栏上密密麻麻的图标&…

2026/7/6 0:01:17 阅读更多 →
Visual C++ 运行时库一键安装终极指南:告别DLL缺失烦恼

Visual C++ 运行时库一键安装终极指南:告别DLL缺失烦恼

Visual C 运行时库一键安装终极指南:告别DLL缺失烦恼 【免费下载链接】vcredist AIO Repack for latest Microsoft Visual C Redistributable Runtimes 项目地址: https://gitcode.com/gh_mirrors/vc/vcredist 你是否曾经遇到过这样的情况:下载了…

2026/7/6 0:05:19 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/5 0:03:34 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/5 0:03:34 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/5 0:07:38 阅读更多 →

月新闻