MyBatisPlus自定义TypeHandler实现方法与示例
时间:2026-08-18 | 作者:火苗实验室 | 阅读:0背景
第一步:MyBatis 默认是怎么把数据库字段映射到 Ja va 属性的?
当你执行一个查询,MyBatis 拿到 ResultSet(数据库返回的结果集),接着需要把每一行数据转换成你的 Ja va 实体对象。这个过程里,MyBatis 内置了一套类型处理器(TypeHandler),专门负责把数据库列类型转换成 Ja va 类型。
| 数据库类型 | Ja va 类型 | 谁负责转换 |
|---|---|---|
| VARCHAR | String | 内置 StringTypeHandler |
| INT | Integer | 内置 IntegerTypeHandler |
| DATETIME | LocalDateTime | 内置 LocalDateTimeTypeHandler |
这些转换全是自动的——MyBatis 的作者早就替你写好了对应处理器。你的实体类里写了 private String name,MyBatis 一看就知道:哦,VARCHAR 列,直接塞给这个字段。这是第一层设计:基本类型的映射,你完全不用操心。
第二步:什么时候默认映射会失效?
假设数据库里有一个 JSON 类型的字段:
CREATE TABLE tb_user (
id BIGINT PRIMARY KEY,
name VARCHAR(50),
roles JSON -- 存储如:["admin", "editor"]
);
你在 Ja va 实体类里想直接映射成 List:
public class User {
private Long id;
private String name;
private List roles; // 想直接拿到 List
}
问题来了:
MyBatis 内置的类型处理器里,根本没有 JSON → List
第三步:TypeHandler 被设计出来解决什么问题?
因为上面的痛点,MyBatis 设计了一个接口 TypeHandler,它的职责只有一个:
在 Ja va 类型和 JDBC 类型之间做双向转换。
它规定了两个核心行为:
- 写入数据库时(Ja va → SQL):把 Ja va 对象转换成数据库能接受的格式
- 从数据库读取时(SQL → Ja va):把数据库返回的原始值转换成 Ja va 对象
public interface TypeHandler{ // 写入:PreparedStatement.setXxx() void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType); // 读取:ResultSet.getXxx(),三种重载对应不同场景 T getResult(ResultSet rs, String columnName); T getResult(ResultSet rs, int columnIndex); T getResult(CallableStatement cs, int columnIndex); }
为什么这样设计?因为 MyBatis 的作者心里清楚,他不可能预先知道所有项目里会出现的自定义类型转换需求(JSON、加密、压缩、特殊枚举等)。所以他把转换逻辑抽象成一个接口,让你自己实现具体的转换规则,然后 MyBatis 在需要转换的地方直接调用你写的逻辑。这就是扩展性。
一、自定义 TypeHandler
TypeHandler 是 MyBatis 中负责 Ja va 类型 数据库类型 之间转换的处理器。当内置的处理器满足不了需求时,就需要自己动手写一个。
二、使用场景
最典型的就是数据库存 JSON 字符串,Ja va 里想直接用对象接收:
数据库:{"name":"张三","age":22} Ja va:Address 对象
三、实现步骤
第一步:定义 Ja va 对象
@Data
public class Address {
private String province;
private String city;
}
第二步:编写自定义 TypeHandler
继承 BaseTypeHandler,实现 4 个方法:
@MappedTypes(Address.class) // 声明处理的Ja va类型
@MappedJdbcTypes(JdbcType.VARCHAR) // 声明处理的数据库类型
public class AddressTypeHandler extends BaseTypeHandler {
private static final ObjectMapper mapper = new ObjectMapper();
// 写入数据库:Ja va对象 → 字符串
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
Address address, JdbcType jdbcType) throws SQLException {
ps.setString(i, mapper.writeValueAsString(address));
}
// 查询时映射:字符串 → Ja va对象(按列名)
@Override
public Address getNullableResult(ResultSet rs, String columnName) throws SQLException {
return parse(rs.getString(columnName));
}
// 查询时映射:字符串 → Ja va对象(按列下标)
@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 字符串 ["篮球","足球","游泳"],Ja va 里想用 List 接收。
第一步:先看数据库表结构
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50),
hobbies VARCHAR(500) -- 存 JSON 字符串,比如 ["篮球","足球"]
);
第二步:理解 TypeHandler 要做什么
你可以把 TypeHandler 理解成一个翻译官:
- 存数据时:List
["篮球","足球"] → 翻译成 → 字符串 '["篮球","足球"]' → 存入数据库 - 取数据时:字符串 '["篮球","足球"]' → 翻译成 → List
["篮球","足球"] → 返回给 Ja va
第三步:编写 TypeHandler
package 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 ja va.sql.CallableStatement; import ja va.sql.PreparedStatement; import ja va.sql.ResultSet; import ja va.sql.SQLException; import ja va.util.List; @MappedTypes(List.class) // 告诉MyBatis,这个处理器是处理 List 类型的 public class ListTypeHandler extends BaseTypeHandler> { // ObjectMapper 是 Jackson 库的核心类,用来做 JSON 转换 private static final ObjectMapper mapper = new ObjectMapper(); /** * 存数据时调用:把 Ja va 的 List
转成 JSON 字符串存入数据库 * ps:可以理解成数据库操作对象 * i:第几个参数 * parameter:就是你传进来的 List */ @Override public void setNonNullParameter(PreparedStatement ps, int i, List 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 字符串转成 List * columnName:数据库列名,比如 "hobbies" */ @Override public List getNullableResult(ResultSet rs, String columnName) throws SQLException { return parse(rs.getString(columnName)); } /** * 取数据时调用(按列的下标查询):把数据库的 JSON 字符串转成 List * columnIndex:第几列,从1开始 */ @Override public List getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return parse(rs.getString(columnIndex)); } /** * 存储过程时调用,一般用不到,但必须实现 */ @Override public List getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return parse(cs.getString(columnIndex)); } /** * 抽取一个公共方法:把 JSON 字符串转成 List */ private List parse(String json) { try { if (json == null || json.isEmpty()) { return null; } // 把字符串 '["篮球","足球"]' 转回 List return mapper.readValue(json, new TypeReference >() {}); } 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 List 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.sa ve(user);
此时数据库 hobbies 字段存的是:
["篮球","足球","游泳"]
取数据:
Users user = usersService.getById(1); Listhobbies = user.getHobbies(); System.out.println(hobbies); // [篮球, 足球, 游泳]
自动就转回 List
整体流程图
存数据: Ja va代码 → List["篮球","足球"] → ListTypeHandler.setNonNullParameter() → '["篮球","足球"]' → 数据库 取数据: 数据库 → '["篮球","足球"]' → ListTypeHandler.getNullableResult() → parse() 方法解析 → List ["篮球","足球"] → Ja va代码
【备注】:对于 List@TableField(typeHandler = JacksonTypeHandler.class) 即可。
常见错误
查询时字段一直是 null?检查 @TableName 里有没有加 autoResultMap = true,这是最常见的遗漏。
JSON 解析报错?检查数据库里存的值格式是否正确,手动查一下是不是合法的 JSON 格式。
找不到 TypeHandler?检查 application.yml 里的包路径是否和你的 Handler 实际所在包一致。
七、完整示例2
场景描述
用户的手机号、身份证号属于敏感信息,监管要求必须加密存储在数据库中,但 Ja va 代码里操作的时候要用明文。存入数据库: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);
}
}
}
第二步:自定义 TypeHandler
@MappedTypes(String.class) public class EncryptTypeHandler extends BaseTypeHandler{ // 存数据库时:明文 → 加密 → 存密文 @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.sa ve(user);
数据库实际存的是:
phone: a3f8c2d1e9b74f2a... (密文,看不出原始手机号)
idCard: 9c2e1d8f3a7b6e4c... (密文)
取数据:
Users user = usersService.getById(1); System.out.println(user.getPhone()); // 13812345678 自动解密成明文 System.out.println(user.getIdCard()); // 110101199001011234 自动解密成明文
为什么这个场景必须自定义?
因为这个需求是在 Ja va 和数据库之间做了额外的业务处理(加解密),不是简单的类型转换,任何内置的 TypeHandler 都做不到,只能自己写。类似的场景还有:数据压缩存储、手机号脱敏显示、特殊格式转换等,都是自定义 TypeHandler 的典型使用场景。
八、setNonNullParameter()方法详解
先理解这个方法是干什么的
当你执行 usersService.sa ve(user) 时,MyBatis-Plus 底层会构建一条 SQL:
INSERT INTO users (phone) VALUES ()
这个 是占位符,MyBatis 需要把你 Ja va 里的 "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 对应 i=1,idCard 对应 i=2。String plainText 就是你 Ja va 代码里传进来的原始值,比如 "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.sa ve(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 内置了基本类型的转换,但无法覆盖所有业务场景(如 JSON)
- 没有 TypeHandler 的弊端:复杂类型无法自动映射,查询为 null,需要手动转换
- 所以 MyBatis 设计了 TypeHandler 接口,让你自定义转换逻辑
- MyBatis-Plus 的通用方法不走 XML 的 ResultMap,导致查询时 TypeHandler 配置无法被识别
- 所以 MyBatis-Plus 设计了 autoResultMap,自动构建包含 TypeHandler 信息的 ResultMap,让通用查询方法也能正确使用自定义类型处理器
核心记忆点:
只要你在实体类字段上写了
@TableField(typeHandler = ...),这个实体类的@TableName就必须加上autoResultMap = true,否则查询结果里这个字段永远是 null。
免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。
相关文章
更多-
- Qwen3.7-Plus发布:11小时自主闭环开发真实APP与GUI编程
- 时间:2026-08-21
-
- Google AI Plus月费降至4.99美元 存储空间翻倍至400GB
- 时间:2026-08-21
-
- MyBatis-Plus实现悲观锁与乐观锁项目实践指南
- 时间:2026-08-18
-
- Google AI Plus服务降价升级:存储空间同步扩容
- 时间:2026-08-18
-
- ChatGPT Plus 8美元重置功能上线,额度用完无需再等
- 时间:2026-08-17
-
- 阿里开源Qwen3.8-27B模型:270亿参数编程能力超越Qwen3.7-Plus
- 时间:2026-08-16
-
- ChatGPT Plus如何实现Codex登录权限与使用教程
- 时间:2026-08-12
-
- BTF 3.0无线主机发售 270K Plus+5070 Ti售价17499元
- 时间:2026-08-10
精选合集
更多大家都在玩
大家都在看
更多-
- 糖尿病完全不能吃糖吗
- 时间:2026-09-15
-
- 蚂蚁庄园小课堂2026年9月16日最新题目答案
- 时间:2026-09-15
-
- 小鸡答题今天的答案是什么2026年9月16日
- 时间:2026-09-15
-
- 蚂蚁庄园每日答题答案2026年9月16日
- 时间:2026-09-15
-
- 以下哪种粮食是酿造绍兴黄酒的主要原料 蚂蚁庄园今日答案9月16日
- 时间:2026-09-15
-
- 劝学名句“及时当勉励,岁月不待人”出自哪位诗人 蚂蚁庄园今日答案9.16
- 时间:2026-09-15
-
- 蚂蚁庄园今天答题答案2026年9月16日
- 时间:2026-09-15
-
- 蚂蚁庄园答题今日答案2026年9月16日
- 时间:2026-09-15
