MyBatis 的 TypeHandler 是什么?如何自定义类型转换?
简化版
TypeHandler(类型处理器)是 MyBatis 负责 Java 类型和 JDBC 类型互相转换的组件。数据库里存的是 JDBC 类型(VARCHAR、INT、TIMESTAMP),Java 里是 String、Integer、LocalDateTime——两者之间的转换全靠 TypeHandler:设置参数时(#{})把 Java 值转成 JDBC 类型(setString/setInt),读取结果时把 JDBC 值转成 Java 类型(getString/getInt)。MyBatis 内置了一堆常用类型的 TypeHandler。当你有特殊转换需求(如枚举存成 code、把 JSON 字符串转成对象、加解密字段)时,就自定义 TypeHandler——实现 TypeHandler 接口(或继承 BaseTypeHandler)注册进去。
详细版
TypeHandler 的职责(双向转换):
Java 参数 → 数据库(写):#{name} 时,TypeHandler 把 Java 值 set 进 PreparedStatement
数据库 → Java 对象(读):查询结果,TypeHandler 从 ResultSet 把列值 get 成 Java 类型
内置 TypeHandler 举例:
StringTypeHandler:String ↔ VARCHAR
IntegerTypeHandler:Integer ↔ INT
LocalDateTimeTypeHandler:LocalDateTime ↔ TIMESTAMP
EnumTypeHandler:枚举 ↔ 枚举名字符串(默认按 name())
自定义 TypeHandler(以「枚举存 code」为例):
// 需求:Status 枚举在数据库存 int code(1=启用, 0=禁用),而非枚举名
public enum Status {
ENABLED(1), DISABLED(0);
private final int code;
Status(int code) { this.code = code; }
public int getCode() { return code; }
}
// 自定义 TypeHandler,继承 BaseTypeHandler
@MappedTypes(Status.class) // 处理的 Java 类型
@MappedJdbcTypes(JdbcType.INTEGER) // 对应的 JDBC 类型
public class StatusTypeHandler extends BaseTypeHandler<Status> {
// 写:Java 枚举 → 数据库 int
public void setNonNullParameter(PreparedStatement ps, int i, Status s, JdbcType jt) throws SQLException {
ps.setInt(i, s.getCode()); // 存 code
}
// 读:数据库 int → Java 枚举
public Status getNullableResult(ResultSet rs, String col) throws SQLException {
int code = rs.getInt(col);
return codeToStatus(code); // 按 code 反查枚举
}
// 还有两个 getNullableResult 重载(按列索引、CallableStatement)
}
注册方式:
<!-- mybatis-config.xml -->
<typeHandlers>
<typeHandler handler="com.example.StatusTypeHandler"/>
</typeHandlers>
# Spring Boot:application.yml 指定扫描包
mybatis:
type-handlers-package: com.example.handler
⚠️ MyBatis 内置的
EnumTypeHandler默认把枚举存成枚举名字符串(ENABLED/DISABLED),而EnumOrdinalTypeHandler存序数 ordinal(0/1)。别用EnumOrdinalTypeHandler——它按枚举声明顺序存序数,一旦调整枚举顺序,数据库里的老数据全错位(和「别用 ordinal 做持久化」是同一个坑)。要存自定义 code 就自定义 TypeHandler,别依赖 ordinal。
完整版教学
一、TypeHandler 解决什么:类型的「翻译官」
数据库世界和 Java 世界的类型是两套体系:数据库有 VARCHAR、INT、DATE、BLOB(JDBC 类型),Java 有 String、Integer、LocalDate、byte[]。它们不能直接互通——从数据库读出来的是 JDBC 类型,要转成 Java 类型才能用;往数据库写时反过来。TypeHandler 就是这个「翻译官」:
写数据(Java → 数据库):
userMapper.insert(user) → #{user.createTime}(LocalDateTime)
→ TypeHandler 调 ps.setTimestamp() 转成 JDBC TIMESTAMP 存进去
读数据(数据库 → Java):
查出来的 create_time 列是 JDBC TIMESTAMP
→ TypeHandler 调 rs.getTimestamp() 转成 Java LocalDateTime 填进对象
关键认知:每次 #{} 参数设置、每次结果列映射,背后都有一个 TypeHandler 在做类型转换。你平时感觉不到它,是因为 MyBatis 内置了几十个常用类型的 TypeHandler,自动处理了 String、数字、日期、布尔等常见转换。只有当默认转换不满足需求时,你才需要自定义。理解「TypeHandler 是类型转换的底层机制」,就理解了 MyBatis 怎么把「一行数据」变成「一个 Java 对象」。
二、TypeHandler 的四个方法:读写双向
TypeHandler 接口(通常继承 BaseTypeHandler)有四个核心方法,覆盖「写一次、读三种情况」:
public abstract class BaseTypeHandler<T> implements TypeHandler<T> {
// 写:设置参数(Java → JDBC)
void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType);
// 读:从 ResultSet 按列名取(JDBC → Java)
T getNullableResult(ResultSet rs, String columnName);
// 读:从 ResultSet 按列索引取
T getNullableResult(ResultSet rs, int columnIndex);
// 读:从 CallableStatement 取(存储过程)
T getNullableResult(CallableStatement cs, int columnIndex);
}
设计逻辑:写只有一种(往 PreparedStatement 设参数),读有三种来源(ResultSet 按列名、ResultSet 按列索引、CallableStatement 存储过程)。BaseTypeHandler 已经处理了 null 判断(setParameter 里判 null、getResult 返回 null),你只需实现「非 null 情况下怎么转」。所以自定义时重点是:写的时候 Java 值怎么 set 进 JDBC、读的时候 JDBC 值怎么 get 成 Java——这就是转换逻辑的核心。
三、内置 TypeHandler 与自动匹配
MyBatis 内置了大量 TypeHandler,覆盖常见类型,自动按「Java 类型 + JDBC 类型」匹配:
你的实体字段 数据库列类型 自动选用的 TypeHandler
String name VARCHAR StringTypeHandler
Integer age INT IntegerTypeHandler
LocalDateTime time TIMESTAMP LocalDateTimeTypeHandler
Boolean active BIT/TINYINT BooleanTypeHandler
BigDecimal amount DECIMAL BigDecimalTypeHandler
MyBatis 维护一个 TypeHandlerRegistry(注册表),根据「Java 类型和 JDBC 类型」查找对应的 TypeHandler。所以大多数标准类型你什么都不用做——#{} 和结果映射会自动选对 TypeHandler。只有以下情况需要自定义:① 数据库存的格式和 Java 类型不直接对应(枚举存 code、状态存中文);② 复杂类型转换(JSON 字符串 ↔ 对象、逗号分隔字符串 ↔ List);③ 字段级加解密(存密文、读明文)。这些「非标准转换」就是 TypeHandler 的用武之地。
四、经典场景一:枚举的正确存储
枚举存储是 TypeHandler 最经典的应用,也最容易踩坑。MyBatis 有两个内置枚举 TypeHandler:
EnumTypeHandler(默认):存枚举的 name()
Status.ENABLED → 数据库存 "ENABLED"(字符串)
优点:直观、改枚举顺序不影响
缺点:占空间、和数据库常见的 int code 约定不符
EnumOrdinalTypeHandler:存枚举的 ordinal()(序数 0,1,2...)
Status.ENABLED → 数据库存 0(int)
★ 危险:ordinal 绑定声明顺序,中间插入一个枚举值,老数据全错位!
生产实践几乎都是「枚举存自定义 code」(如 1=启用, 0=禁用),既省空间又语义稳定。做法就是自定义 TypeHandler(如上面的 StatusTypeHandler)——写时存 getCode()、读时按 code 反查枚举。千万别用 EnumOrdinalTypeHandler——它按声明顺序存序数,和「别用 ordinal 做持久化」是同一个坑:调整枚举顺序,数据库里的历史数据就全部对应错枚举了。这是枚举存储的核心考点。
五、经典场景二、三:JSON 字段与加解密
另外两个高频自定义场景:
// 场景二:把一个 List/对象字段,以 JSON 字符串存进一个 VARCHAR 列
@MappedTypes(List.class)
public class JsonListTypeHandler extends BaseTypeHandler<List<String>> {
public void setNonNullParameter(PreparedStatement ps, int i, List<String> list, JdbcType jt) {
ps.setString(i, JSON.toJSONString(list)); // List → JSON 字符串存
}
public List<String> getNullableResult(ResultSet rs, String col) {
return JSON.parseArray(rs.getString(col), String.class); // JSON → List
}
}
// 场景三:字段级加解密(数据库存密文,Java 里是明文)
public class EncryptTypeHandler extends BaseTypeHandler<String> {
public void setNonNullParameter(PreparedStatement ps, int i, String plain, JdbcType jt) {
ps.setString(i, encrypt(plain)); // 明文加密后存
}
public String getNullableResult(ResultSet rs, String col) {
return decrypt(rs.getString(col)); // 密文解密后返回明文
}
}
这两个场景体现 TypeHandler 的价值——把「转换逻辑」收敛到一处,业务代码无感。JSON 字段:实体里就是 List<String>,存进去自动变 JSON、读出来自动变回 List,业务不用手动序列化。字段加解密:实体里是明文,存进数据库自动加密、查出来自动解密,业务代码完全不知道底层加密的存在(如手机号、身份证号的隐私字段加密)。这种「透明转换」是 TypeHandler 最优雅的用法。
六、注册方式与作用范围
自定义 TypeHandler 后要注册,有全局和局部两种方式:
全局注册(对该类型的所有映射生效):
XML:<typeHandlers><typeHandler handler="..."/></typeHandlers>
Spring Boot:mybatis.type-handlers-package: com.example.handler(扫包)
→ 之后所有 Status 类型的 #{} 和结果映射都自动用你的 TypeHandler
局部指定(只对某个字段/参数生效):
#{status, typeHandler=com.example.StatusTypeHandler}
<result column="status" property="status" typeHandler="..."/>
→ 只有这一处用指定的 TypeHandler
一般用全局注册(一劳永逸,该类型统一转换)。局部指定用于「同一个 Java 类型在不同地方要不同转换」的特殊情况。注册时用 @MappedTypes(指定 Java 类型)和 @MappedJdbcTypes(指定 JDBC 类型)帮助 MyBatis 精确匹配。理解注册的作用范围,就能控制「哪些字段用自定义转换、哪些用默认」。
记忆钩子:「TypeHandler 是 Java 类型↔JDBC 类型的翻译官:写时 set 参数、读时 get 结果,四个方法(一写三读);内置覆盖标准类型自动匹配;自定义用于枚举存 code(别用 EnumOrdinalTypeHandler,ordinal 会错位)、JSON 字段、字段加解密;全局注册或 #{} 局部指定」。
七、常见误区与追问
- 误区:TypeHandler 只在读结果时工作。 双向——写参数时(#{})把 Java 转 JDBC、读结果时把 JDBC 转 Java,四个方法覆盖「一写三读」。
- 误区:枚举存储用 EnumOrdinalTypeHandler 最省空间。 危险——它存 ordinal(声明序数),调整枚举顺序会让老数据全错位;要存 int 应自定义 TypeHandler 存稳定的 code。
- 误区:所有类型转换都要自定义 TypeHandler。 标准类型(String/数字/日期/布尔)MyBatis 内置自动处理;只有枚举存 code、JSON 字段、加解密等非标准转换才需自定义。
- 误区:TypeHandler 处理不了复杂对象。 能——把复杂对象序列化成 JSON 字符串存进一个列(读时反序列化),就是常见的 JSON TypeHandler 用法。
- 追问:MyBatis 默认怎么存枚举? 默认 EnumTypeHandler 存枚举的 name()(字符串);EnumOrdinalTypeHandler 存 ordinal(不推荐);要存自定义 code 需自定义 TypeHandler。
- 追问:字段加解密怎么做到业务无感? 自定义 TypeHandler——写时在 setNonNullParameter 里加密、读时在 getNullableResult 里解密,实体字段始终是明文,业务代码不知道底层加密。
- 追问:TypeHandler 是在 MyBatis 执行流程的哪一步工作? 写:StatementHandler/ParameterHandler 设置参数时;读:ResultSetHandler 处理结果集、把列值映射到对象属性时——都通过 TypeHandler 做类型转换。
八、加强记忆
TypeHandler 是 MyBatis 里 Java 类型 ↔ JDBC 类型的「翻译官」——数据库存 JDBC 类型(VARCHAR/INT/TIMESTAMP)、Java 用 String/Integer/LocalDateTime,两者的转换全靠它:写数据时(#{})把 Java 值 set 进 PreparedStatement(Java→JDBC),读数据时把 ResultSet 列值 get 成 Java 类型(JDBC→Java),接口有四个方法(一写三读,BaseTypeHandler 已处理 null)。MyBatis 内置了几十个 TypeHandler 覆盖标准类型、按「Java 类型+JDBC 类型」自动匹配,所以平时无感。需要自定义的三大场景:① 枚举存 code(最经典——默认 EnumTypeHandler 存 name 字符串、EnumOrdinalTypeHandler 存 ordinal 但绝不能用因调整顺序会错位,要自定义存稳定 code);② JSON 字段(List/对象 ↔ JSON 字符串,业务无感序列化);③ 字段加解密(存密文读明文,隐私字段透明加密)。注册用全局(type-handlers-package 扫包)或 #{status, typeHandler=...} 局部指定,配 @MappedTypes/@MappedJdbcTypes 精确匹配。一句话「TypeHandler 翻译 Java↔JDBC,写 set 读 get,自定义处理枚举 code/JSON/加解密,别用 EnumOrdinalTypeHandler」。