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