typeHandlers
导学
本节学习目标:
- 理解
typeHandlers在 Java 类型与 JDBC 类型之间的"翻译官"角色 - 掌握 MyBatis 内置类型处理器的覆盖范围与使用场景
- 能够根据业务需求自定义
TypeHandler,实现特殊类型的存取转换 - 了解类型处理器的注册方式与优先级规则
定义
MyBatis 执行 SQL 时,需要将 Java 方法的参数转换为 JDBC 预编译语句(PreparedStatement)能接受的类型,同时将查询结果集(ResultSet)中的列值转换为 Java 对象属性类型。例如:
- Java 的
String↔ JDBC 的VARCHAR/CHAR - Java 的
Integer↔ JDBC 的INTEGER - Java 的
Date↔ JDBC 的TIMESTAMP
当遇到 MyBatis 未内置支持的类型转换(如自定义枚举 ↔ 数据库 TINYINT,或 JSON 字符串 ↔ Java 对象),就需要通过 typeHandlers 注册自定义的类型处理器。
适用位置与核心属性
typeHandlers 位于 mybatis-config.xml 中 typeAliases 之后、objectFactory 之前。
单个处理器注册
<typeHandlers>
<typeHandler
javaType="com.feixiang.enums.EmployeeStatus"
jdbcType="TINYINT"
handler="com.feixiang.handler.EmployeeStatusTypeHandler"/>
</typeHandlers>
| 属性 | 是否必填 | 说明 |
|---|---|---|
javaType | 可选 | 该处理器处理的 Java 类型全限定名 |
jdbcType | 可选 | 该处理器处理的 JDBC 类型(来自 JdbcType 枚举) |
handler | 必填 | 处理器类的全限定名,必须实现 TypeHandler 接口 |
包扫描注册
<typeHandlers>
<package name="com.feixiang.handler"/>
</typeHandlers>
包扫描时,MyBatis 会自动识别类路径下所有实现了
TypeHandler接口的类并注册。
核心原理
类型转换流程图(Java 类型 → JDBC 类型)
查询时的反向流程:
核心接口:TypeHandler<T> 定义了三个核心方法:
setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType):Java → JDBCgetResult(ResultSet rs, String columnName):JDBC → Java(按列名)getResult(ResultSet rs, int columnIndex):JDBC → Java(按列索引)
完整示例
场景说明
乐途公司员工管理系统中,员工状态 EmployeeStatus 是一个枚举:
public enum EmployeeStatus {
ACTIVE(1, "在职"),
INACTIVE(0, "离职"),
SUSPENDED(2, "停薪留职");
private final int code;
private final String desc;
// 构造方法、getter 省略
}
数据库表 employee 中,status 字段类型为 TINYINT,存储枚举的 code 值。需要自定义 TypeHandler 实现枚举与 TINYINT 的互转。
操作前的状态
若不配置类型处理器,MyBatis 默认将枚举按名称(name())存入数据库,即 ACTIVE 字符串,而非期望的 1。
完整配置代码
步骤一:实现自定义 TypeHandler
package com.feixiang.handler;
import com.feixiang.enums.EmployeeStatus;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
// 注解方式声明映射关系(可选,也可在 XML 中配置)
@MappedTypes(EmployeeStatus.class)
@MappedJdbcTypes(JdbcType.TINYINT)
public class EmployeeStatusTypeHandler extends BaseTypeHandler<EmployeeStatus> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
EmployeeStatus parameter, JdbcType jdbcType) throws SQLException {
// Java 枚举 -> JDBC TINYINT
ps.setInt(i, parameter.getCode());
System.out.println("【TypeHandler】写入数据库: " + parameter.name() + " -> " + parameter.getCode());
}
@Override
public EmployeeStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
// JDBC TINYINT -> Java 枚举
int code = rs.getInt(columnName);
return convert(code);
}
@Override
public EmployeeStatus getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
int code = rs.getInt(columnIndex);
return convert(code);
}
@Override
public EmployeeStatus getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
int code = cs.getInt(columnIndex);
return convert(code);
}
private EmployeeStatus convert(int code) {
for (EmployeeStatus status : EmployeeStatus.values()) {
if (status.getCode() == code) {
System.out.println("【TypeHandler】读取数据库: " + code + " -> " + status.name());
return status;
}
}
return null;
}
}
步骤二:在 mybatis-config.xml 中注册
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties"/>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<package name="com.feixiang.entity"/>
</typeAliases>
<!-- 类型处理器配置 -->
<typeHandlers>
<!-- 方式一:显式注册 -->
<typeHandler
javaType="com.feixiang.enums.EmployeeStatus"
jdbcType="TINYINT"
handler="com.feixiang.handler.EmployeeStatusTypeHandler"/>
<!-- 方式二:包扫描(若处理器类上有 @MappedTypes/@MappedJdbcTypes 注解) -->
<!-- <package name="com.feixiang.handler"/> -->
</typeHandlers>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/EmployeeMapper.xml"/>
</mappers>
</configuration>
步骤三:Mapper XML 中使用
<mapper namespace="com.feixiang.mapper.EmployeeMapper">
<resultMap id="BaseResultMap" type="employee">
<id column="employee_id" property="employeeId"/>
<result column="employee_name" property="employeeName"/>
<!-- 显式指定 typeHandler,也可依赖全局注册自动匹配 -->
<result column="status" property="status"
typeHandler="com.feixiang.handler.EmployeeStatusTypeHandler"/>
</resultMap>
<insert id="insert">
INSERT INTO employee (employee_name, department_code, status)
VALUES (#{employeeName}, #{departmentCode},
#{status, typeHandler=com.feixiang.handler.EmployeeStatusTypeHandler})
</insert>
<select id="selectById" resultMap="BaseResultMap">
SELECT employee_id, employee_name, department_code, status
FROM employee
WHERE employee_id = #{id}
</select>
</mapper>
步骤四:Java 测试代码
public class TypeHandlerDemo {
public static void main(String[] args) throws Exception {
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
try (SqlSession session = factory.openSession()) {
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
Employee emp = new Employee();
emp.setEmployeeName("李四");
emp.setDepartmentCode("HR001");
emp.setStatus(EmployeeStatus.ACTIVE);
mapper.insert(emp);
session.commit();
Employee result = mapper.selectById(emp.getEmployeeId());
System.out.println("查询到的状态: " + result.getStatus().getDesc());
}
}
}
实际效果/结果
控制台输出:
【TypeHandler】写入数据库: ACTIVE -> 1
【TypeHandler】读取数据库: 1 -> ACTIVE
查询到的状态: 在职
数据库中 status 列存储值为 1,而非字符串 ACTIVE。
分析
BaseTypeHandler是 MyBatis 提供的抽象类,处理了null值的边界情况,自定义处理器只需关注非空逻辑- 在 Mapper XML 的
#{status}中显式指定typeHandler可以覆盖全局注册,适合同一 Java 类型在不同场景需要不同转换策略的情况 @MappedTypes和@MappedJdbcTypes注解配合包扫描,可以省去 XML 中的繁琐配置
易错场景/常见误区
| 误区 | 错误表现 | 正解 |
|---|---|---|
继承 TypeHandler 但不处理 null | 数据库 NULL 值导致 NullPointerException | 继承 BaseTypeHandler,它已封装 null 判断逻辑 |
| 注册了全局处理器但 Mapper 中又显式指定不同处理器 | 实际使用的处理器与预期不符 | 记住优先级:Mapper 中显式指定 > 全局注册。确保两者一致 |
认为 javaType 和 jdbcType 必须同时指定 | 对简单场景过度配置 | 若处理器类上有 @MappedTypes 注解,可省略 javaType;MyBatis 也常能自动推断 jdbcType |
| 枚举默认行为与预期不符 | 枚举存入的是 ACTIVE 字符串而非 1 | 枚举默认使用 EnumTypeHandler(存名称),如需存数值必须自定义 TypeHandler 或使用 EnumOrdinalTypeHandler(存 ordinal,但不稳定) |
| 包扫描后处理器未生效 | 处理器类缺少无参构造方法 | TypeHandler 实现类必须提供公共无参构造方法,否则 MyBatis 无法实例化 |
面试考点
Q1:MyBatis 有哪些常用的内置类型处理器?
A:MyBatis 内置了覆盖绝大多数基础类型的处理器,例如:
StringTypeHandler:String↔VARCHAR/CHARIntegerTypeHandler:Integer↔INTEGERLongTypeHandler:Long↔BIGINTDateTypeHandler:Date↔TIMESTAMPBooleanTypeHandler:Boolean↔BOOLEAN(部分数据库映射为BIT)BlobTypeHandler:byte[]↔BLOB完整列表可参考org.apache.ibatis.type包下的类。
Q2:自定义 TypeHandler 时,为什么要继承 BaseTypeHandler 而不是直接实现 TypeHandler?
A:
BaseTypeHandler是抽象类,它实现了TypeHandler接口,并在三个getResult方法中统一处理了null值判断(通过wasNull())。子类只需实现setNonNullParameter和getNullableResult,代码更简洁且不易遗漏null边界情况。
Q3:如果同一个 Java 类型注册了多个 TypeHandler,MyBatis 如何选择?
A:选择优先级为:① Mapper 语句中显式指定的
typeHandler;② 全局注册时同时指定了javaType和jdbcType的精确匹配;③ 仅指定javaType的匹配;④ 包扫描自动注册的匹配。若存在多个同优先级处理器,后注册的会覆盖先注册的。
小结
typeHandlers 是 MyBatis 类型系统的核心桥梁,它让 Java 世界与 JDBC 世界能够无缝对话。内置处理器已覆盖绝大多数基础场景,但在枚举映射、JSON 存储、加密字段等特殊需求下,自定义 TypeHandler 是优雅且可复用的解决方案。记住继承 BaseTypeHandler、处理好 null、合理选择注册方式,是编写健壮处理器的关键。
下一章引子
类型转换问题解决后,MyBatis 还需要将查询结果转换为 Java 对象。这个"对象创建"的工作由 ObjectFactory 负责。虽然大多数情况下使用默认实现即可,但了解它的扩展机制有助于理解 MyBatis 的对象实例化原理,也为某些特殊场景(如对象池、代理创建)提供可能。