引言:为什么 字段能自动映射为
MyBatis 从 ResultSet 中读取 create_time 列的值时,调用的是 ResultSet.getTimestamp() 还是 ResultSet.getObject()?答案是:取决于 TypeHandler。TypeHandler 是 MyBatis 中连接"JDBC 类型"和"Java 类型"的桥梁——它负责从 ResultSet 中读取数据并转换为 Java 对象(getResult),以及将 Java 对象设置到 PreparedStatement 中(setParameter)。这篇文章从 TypeHandler 的接口设计到 MyBatis 内置的 30+ 个类型处理器,再到自定义类型处理器和枚举映射的四种方式,完整拆解 MyBatis 的类型处理机制。
一、TypeHandler 接口与工作原理


核心接口
public interface TypeHandler<T> {
// 设置 PreparedStatement 参数
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
// 从 ResultSet 按列名获取结果
T getResult(ResultSet rs, String columnName) throws SQLException;
// 从 ResultSet 按列索引获取结果
T getResult(ResultSet rs, int columnIndex) throws SQLException;
// 从存储过程 CallableStatement 获取结果
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
BaseTypeHandler 抽象类
MyBatis 提供了 BaseTypeHandler<T> 抽象类,它实现了 TypeHandler<T> 接口,并处理了 null 值的判断逻辑。自定义 TypeHandler 通常继承 BaseTypeHandler 并重写四个抽象方法:
public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {
@Override
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null) {
if (jdbcType == null) {
ps.setNull(i, JdbcType.NULL.TYPE_CODE); // 没有指定 jdbcType 时用 NULL
} else {
ps.setNull(i, jdbcType.TYPE_CODE); // 指定了 jdbcType 时用对应的类型
}
} else {
setNonNullParameter(ps, i, parameter, jdbcType); // 非 null 时委托给子类
}
}
// getResult 类似,null 检查后委托给子类
}
类型处理的完整链路
- 参数设置:
ParameterHandler.setParameters()→ 遍历参数列表 → 查找对应的TypeHandler→ 调用setParameter()→PreparedStatement.setXxx() - 结果读取:
ResultSetHandler.handleResultSets()→ 遍历ResultSet列 → 查找对应的TypeHandler→ 调用getResult()→ResultSet.getXxx()→ 转换为 Java 类型
TypeHandler 的注册与查找
MyBatis 内置了 30+ 个 TypeHandler,注册在 Configuration.typeHandlerRegistry 中。查找逻辑:先按 javaType + jdbcType 精确匹配,找不到则按 javaType 匹配,再找不到则尝试用"父类型"匹配(如 LocalDateTime 找不到时尝试 Object)。
二、MyBatis 内置的 TypeHandler 分类

| 类别 | Java 类型 | JDBC 类型 | 内置 TypeHandler |
|---|---|---|---|
| 基本类型 | String |
VARCHAR/CHAR |
StringTypeHandler |
| 基本类型 | Integer/int |
INTEGER |
IntegerTypeHandler |
| 基本类型 | Long/long |
BIGINT |
LongTypeHandler |
| 基本类型 | Boolean/boolean |
BOOLEAN |
BooleanTypeHandler |
| 基本类型 | BigDecimal |
DECIMAL |
BigDecimalTypeHandler |
| 日期时间 | java.util.Date |
TIMESTAMP |
DateTypeHandler |
| 日期时间 | java.sql.Date |
DATE |
DateOnlyTypeHandler |
| 日期时间 | java.sql.Time |
TIME |
TimeOnlyTypeHandler |
| 日期时间 | LocalDateTime |
TIMESTAMP |
LocalDateTimeTypeHandler |
| 日期时间 | LocalDate |
DATE |
LocalDateTypeHandler |
| 二进制 | byte[ ] | BLOB/BINARY | BlobTypeHandler |
| 枚举 | Enum | VARCHAR/INTEGER | EnumTypeHandler/EnumOrdinalTypeHandler |
枚举类型的两种内置处理方式
EnumTypeHandler:将枚举的 name()(如 ACTIVE)存入数据库。优点:可读性好。缺点:枚举重命名后数据库值不匹配。
EnumOrdinalTypeHandler:将枚举的 ordinal()(如 0)存入数据库。优点:存储紧凑。缺点:枚举顺序改变后映射错乱,不推荐。
三、自定义 TypeHandler


场景:将 JSON 字段映射为 Java 对象
public class JsonTypeHandler<T> extends BaseTypeHandler<T> {
private final Class<T> type;
public JsonTypeHandler(Class<T> type) {
this.type = type;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, JsonUtil.toJson(parameter));
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
String json = rs.getString(columnName);
return json == null ? null : JsonUtil.fromJson(json, type);
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String json = rs.getString(columnIndex);
return json == null ? null : JsonUtil.fromJson(json, type);
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String json = cs.getString(columnIndex);
return json == null ? null : JsonUtil.fromJson(json, type);
}
}
注册自定义 TypeHandler
方式一:在 mybatis-config.xml 中注册:
<typeHandlers>
<typeHandler handler="com.example.JsonTypeHandler" javaType="com.example.Address"/>
</typeHandlers>
方式二:在 resultMap 中指定:
<result column="address" property="address"
javaType="com.example.Address"
typeHandler="com.example.JsonTypeHandler"/>
方式三:Spring Boot 中通过 mybatis.type-handlers-package 自动扫描:
mybatis:
type-handlers-package: com.example.typehandler
四、枚举映射的四种方式


方式一:EnumTypeHandler(默认)
public enum Status { ACTIVE, INACTIVE }
// 存入: "ACTIVE", 读取: Status.ACTIVE
方式二:EnumOrdinalTypeHandler
public enum Status { ACTIVE, INACTIVE } // ordinal: 0, 1
// 存入: 0, 读取: Status.ACTIVE
// 危险: 如果在 ACTIVE 前面插入 PENDING, PENDING 变成 0, ACTIVE 变成 1, 数据错乱
方式三:自定义枚举属性映射
public enum Status {
ACTIVE(1, "激活"),
INACTIVE(0, "未激活");
private final int code;
private final String desc;
Status(int code, String desc) { this.code = code; this.desc = desc; }
public int getCode() { return code; }
public static Status fromCode(int code) {
for (Status s : values()) {
if (s.code == code) return s;
}
throw new IllegalArgumentException("Unknown code: " + code);
}
}
// 自定义 TypeHandler
public class StatusTypeHandler extends BaseTypeHandler<Status> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Status p, JdbcType jt)
throws SQLException {
ps.setInt(i, p.getCode());
}
@Override
public Status getNullableResult(ResultSet rs, String columnName) throws SQLException {
int code = rs.getInt(columnName);
return Status.fromCode(code);
}
// ... 其他 getNullableResult 方法类似
}
方式四:MyBatis-Plus 的 @EnumValue 注解
public enum Status {
ACTIVE(1), INACTIVE(0);
@EnumValue
private final int code;
Status(int code) { this.code = code; }
public int getCode() { return code; }
}
MyBatis-Plus 自动扫描 @EnumValue 注解,注册对应的 TypeHandler,无需手动配置。这是最简洁的方式,但需要引入 MyBatis-Plus 依赖。
五、常见陷阱

陷阱一:未指定 jdbcType 导致 null 值报错
<insert id="insert">
INSERT INTO user (name, create_time) VALUES (#{name}, #{createTime})
</insert>
如果 createTime 为 null,MyBatis 不知道应该调用 ps.setNull(i, Types.TIMESTAMP) 还是 ps.setNull(i, Types.NULL)。如果未指定 jdbcType,MyBatis 默认用 Types.NULL,某些数据库(如 Oracle)不支持 Types.NULL,会报错。解决方案:#{createTime, jdbcType=TIMESTAMP}。
陷阱二:自定义 TypeHandler 未注册导致不生效
自定义 TypeHandler 必须显式注册(XML 配置、resultMap 中指定、或 Spring Boot 自动扫描),否则 MyBatis 不会使用它。最常见的排查盲区:在 resultMap 中指定了 javaType 但未指定 typeHandler,MyBatis 会用内置的 TypeHandler 而不是你自定义的。
陷阱三:LocalDateTime 和 Date 的 TypeHandler 混淆
MyBatis 3.4.5+ 内置了 LocalDateTimeTypeHandler,但需要 mybatis-typehandlers-jsr310 依赖(或 MyBatis 3.5+ 内置)。如果 classpath 上没有这个依赖,LocalDateTime 会回退到 ObjectTypeHandler,导致类型转换错误。排查方法:检查 Configuration.typeHandlerRegistry 中是否注册了 LocalDateTimeTypeHandler。
陷阱四:枚举反序列化失败时静默返回 null
EnumTypeHandler 在数据库中遇到不匹配的枚举名时(如 UNKNOWN),Enum.valueOf() 会抛出 IllegalArgumentException。MyBatis 的默认行为是记录警告日志并返回 null——而不是抛异常。这意味着数据库中插入了一个非法枚举值后,应用不会报错,但查询结果中该字段为 null。如果业务逻辑依赖枚举非 null,这会导致隐蔽的 NPE。
小结:TypeHandler 是 JDBC 和 Java 之间的翻译官
TypeHandler 本质上封装了"JDBC 类型的 get/set"和"Java 类型的转换"两个操作。理解 TypeHandler 后,你就能解释:为什么 LocalDateTime 能自动映射(LocalDateTimeTypeHandler)、为什么 JSON 字段可以直接映射为 Java 对象(自定义 JsonTypeHandler)、为什么枚举映射有四种方式(各有取舍)。TypeHandler 是 MyBatis 中"最容易被忽视但最实用的扩展点"——自定义 TypeHandler 可以解决字段加密、JSON 序列化、敏感信息脱敏等一系列实际问题,而不需要在上层业务代码中做转换。
(以上正文阅读时长约 40-50 分钟,以下问答部分不计入正文阅读时长)
文末答疑:面试官会怎么问
问 1:MyBatis 的**TypeHandler**和 JDBC 的**setObject()/getObject()**有什么区别?为什么 MyBatis 不直接用 JDBC 的自动类型映射?
答:JDBC 的 setObject()/getObject() 依赖 JDBC 驱动的默认类型映射,这个映射是"驱动层面"的,不同数据库驱动行为可能不一致。MyBatis 的 TypeHandler 是"框架层面"的,提供了统一的类型映射规则,不受驱动差异影响。更重要的是,TypeHandler 支持自定义——你可以把 VARCHAR 列映射为 Address 对象(通过 JsonTypeHandler),这是 JDBC 的 setObject() 做不到的。TypeHandler 在"JDBC 类型"和"Java 类型"之间插入了自定义转换逻辑,而 JDBC 的自动映射是"直连"的,没有转换空间。
问 2:你如何设计一个"加密 TypeHandler"——让数据库中的敏感字段加密存储,但应用层看到的始终是明文?
答:继承 BaseTypeHandler<String>,在 setNonNullParameter 中调用加密函数将明文转为密文存入数据库,在 getNullableResult 中读取密文并解密为明文返回。关键点:加密密钥不应该硬编码在 TypeHandler 中,应该通过配置注入(如 @Value 或环境变量)。但 TypeHandler 不是 Spring Bean,不能用 @Autowired 注入——需要通过 mybatis-config.xml 的 <typeHandler> 配置或在 Configuration 初始化时手动设置。另外,加密字段不能用于数据库的 WHERE 条件(因为数据库中的值是密文,WHERE name = '张三' 查不到),这是设计上的重要限制。
问 3:**EnumTypeHandler**和**EnumOrdinalTypeHandler**有什么本质区别?为什么说**EnumOrdinalTypeHandler**是危险的?
答:EnumTypeHandler 存储枚举的 name()(字符串),EnumOrdinalTypeHandler 存储枚举的 ordinal()(整数,从 0 开始)。EnumOrdinalTypeHandler 危险的原因是:ordinal() 的值取决于枚举的声明顺序。如果你在 ACTIVE 前面插入一个新枚举值 PENDING,ACTIVE 的 ordinal 从 0 变成 1,数据库中所有之前存储的 0 现在映射为 PENDING 而不是 ACTIVE——数据错乱且没有报错。EnumTypeHandler 用 name() 存储,枚举重命名也会导致问题,但至少"插入新枚举"不会改变已有枚举的 name。工程上建议用自定义的 code 属性(如 int code)来映射,而不是依赖 ordinal() 或 name()。
问 4:如果你的项目中同时使用了 MyBatis 和 MyBatis-Plus,MyBatis-Plus 的**@EnumValue**注解和 MyBatis 原生的**EnumTypeHandler**会冲突吗?
答:不会冲突,但 MyBatis-Plus 会覆盖 MyBatis 的默认行为。MyBatis-Plus 在启动时通过 MybatisSqlSessionFactoryBean 扫描所有带 @EnumValue 注解的枚举类,自动注册对应的 TypeHandler。这个注册的优先级高于 MyBatis 原生的 EnumTypeHandler。如果某个枚举类有 @EnumValue,MyBatis-Plus 的 TypeHandler 生效;如果没有,回退到 MyBatis 原生的 EnumTypeHandler。两者共存的典型场景:项目中大部分枚举用 @EnumValue 映射 code,少数简单枚举(不需要 code 映射)用默认的 EnumTypeHandler。
问 5:**TypeHandler**在**setParameter**时,如果**jdbcType**为 null 且参数值也为 null,MyBatis 怎么处理?这会有什么问题?
答:BaseTypeHandler.setParameter() 中,当 jdbcType == null 且 parameter == null 时,调用 ps.setNull(i, JdbcType.NULL.TYPE_CODE)。JdbcType.NULL.TYPE_CODE 是 java.sql.Types.NULL,值为 0。MySQL 的 JDBC 驱动可以接受 Types.NULL,但 Oracle 的 JDBC 驱动会报错 java.sql.SQLException: Invalid column type。解决方案:在 SQL 中显式指定 jdbcType,如 #{createTime, jdbcType=TIMESTAMP}。MyBatis 3.4+ 支持在全局配置 jdbcTypeForNull 来指定默认的 null 类型,如 <setting name="jdbcTypeForNull" value="NULL"/>(MySQL 适用)或 value="VARCHAR"(Oracle 适用)。