背景
第一步:mybatis 默认是怎么把数据库字段映射到 java 属性的?
当你执行一个查询,mybatis 拿到 resultset(数据库返回的结果集)后,需要把每一行数据转换成你的 java 实体对象。
这个过程中,mybatis 内置了一套类型处理器(typehandler),负责把数据库的列类型转换成 java 类型:
| 数据库类型 | java 类型 | 谁负责转换 |
|---|---|---|
| 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"]
);你在 java 实体类里想直接映射成 list<string>:
public class user {
private long id;
private string name;
private list<string> roles; // 想直接拿到 list
}问题出现了:
mybatis 内置的类型处理器里没有 json → list<string> 这个转换逻辑。它看到数据库列是 varchar/json 类型,java 属性是 list,这两个类型之间没有内置的桥梁。
没有 typehandler 的弊端:
- 查询时,roles 字段的值会是 null,因为 mybatis 不知道该怎么把 json 字符串变成 list
- 或者你只能把 roles 定义成 string,然后在业务代码里自己写 json.parse(),每次查询完都手动转换
- 插入时也一样,list 对象不能直接当成 sql 参数塞进去,需要先转成 json 字符串
这违背了 orm 框架的初衷——orm 的目标是让开发者尽量只操作 java 对象,而不是手动处理数据库和 java 之间的格式转换。
第三步:typehandler 被设计出来解决什么问题?
因为上面的问题,mybatis 设计了一个接口 typehandler<t>,它的职责只有一个:
在 java 类型和 jdbc 类型之间做双向转换。
它规定了两个核心行为:
- 写入数据库时(java → sql):把 java 对象转换成数据库能接受的格式
- 从数据库读取时(sql → java):把数据库返回的原始值转换成 java 对象
public interface typehandler<t> {
// 写入: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 中负责 java 类型 ↔ 数据库类型 之间转换的处理器。
当内置的处理器满足不了需求时,就需要自定义。
二、使用场景
最典型的就是数据库存 json 字符串,java 里想直接用对象接收:
数据库:{"name":"张三","age":22} ←→ java:address 对象
三、实现步骤
第一步:定义 java 对象
@data
public class address {
private string province;
private string city;
}
第二步:编写自定义 typehandler
继承 basetypehandler<t>,实现4个方法:
@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);
}
}
}
第三步:实体类中使用
@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 里想用 list<string> 来接收。
第一步:先看数据库表结构
create table users (
id int primary key auto_increment,
username varchar(50),
hobbies varchar(500) -- 存 json 字符串,比如 ["篮球","足球"]
);第二步:理解 typehandler 要做什么
你可以把 typehandler 理解成一个翻译官:
- 存数据时:list<string> ["篮球","足球"] → 翻译成 → 字符串 '["篮球","足球"]' → 存入数据库
- 取数据时:字符串 '["篮球","足球"]' → 翻译成 → list<string> ["篮球","足球"] → 返回给java
第三步:编写 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 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);
}
}
}
第四步:实体类中使用
@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 去哪里找我们的处理器:
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); 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 完全搞不定,必须自定义。
第一步:准备一个简单的加密工具类
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<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);
}
}
第三步:实体类中使用
@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 底层会构建一条 sql:
insert 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 对应 i=1,idcard 对应 i=2。
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 内置了基本类型的转换,但无法覆盖所有业务场景(如 json)
- 没有 typehandler 的弊端:复杂类型无法自动映射,查询为 null,需要手动转换
- 所以 mybatis 设计了 typehandler 接口,让你自定义转换逻辑
- mybatis-plus 的通用方法不走 xml 的 resultmap,导致查询时 typehandler 配置无法被识别
- 所以 mybatis-plus 设计了 autoresultmap,自动构建包含 typehandler 信息的 resultmap,让通用查询方法也能正确使用自定义类型处理器
核心记忆点:
只要你在实体类字段上写了 @tablefield(typehandler = ...),这个实体类的 @tablename 就必须加上 autoresultmap = true,否则查询结果里这个字段永远是 null。
到此这篇关于mybatisplus实现自定义 typehandler示例的文章就介绍到这了,更多相关mybatisplus自定义typehandler内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论