面试官:MyBatis 如何实现 Java 类型与 JDBC 类型的转换?自定义 TypeHandler 该如何实现?它的底层调用时机是什么?
一、开篇
TypeHandler(类型处理器)是 MyBatis 中负责Java 类型与 JDBC 类型相互转换的核心组件。当你为 PreparedStatement 设置参数或从 ResultSet 中取值时,背后都是 TypeHandler 在工作。
核心职责:
setParameter:将 Java 对象的值转换成 JDBC 类型,设置到 PreparedStatementgetResult:从 ResultSet / CallableStatement 中获取列值,转换成 Java 对象
本篇目标:
- 掌握 TypeHandler 的继承体系和核心方法
- 了解 MyBatis 内置的 TypeHandler 全集
- 学习自定义 TypeHandler 的开发步骤和注册方式
- 追踪 TypeHandler 在参数赋值和结果集封装时的调用时机
二、TypeHandler 核心类
TypeHandler<T>:接口,定义四个核心方法。BaseTypeHandler<T>:抽象类,实现了参数 null 值处理等公共逻辑,留下setNonNullParameter和getNullableResult让子类实现。TypeHandlerRegistry:注册中心,维护 Java 类型 ↔ TypeHandler、JDBC 类型 ↔ TypeHandler 的映射。
三、TypeHandler 核心接口与抽象类
3.1 TypeHandler 接口
// org.apache.ibatis.type.TypeHandlerpublic interface TypeHandler<T> {// 设置参数void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;// 根据列名获取结果T getResult(ResultSet rs, String columnName) throws SQLException;// 根据列索引获取结果T getResult(ResultSet rs, int columnIndex) throws SQLException;// 从存储过程获取结果T getResult(CallableStatement cs, int columnIndex) throws SQLException;}
3.2 BaseTypeHandler 抽象类
// org.apache.ibatis.type.BaseTypeHandlerpublic abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {@Overridepublic void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {if (parameter == null) {if (jdbcType == null) {// ✅ 关键:如果参数为 null 且未指定 jdbcType,直接抛出异常throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");}try {ps.setNull(i, jdbcType.TYPE_CODE);} catch (SQLException e) {throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "+ "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "+ "Cause: " + e, e);}} else {try {setNonNullParameter(ps, i, parameter, jdbcType);} catch (Exception e) {throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "+ "Try setting a different JdbcType for this parameter or a different configuration property. " + "Cause: "+ e, e);}}}@Overridepublic T getResult(ResultSet rs, String columnName) throws SQLException {try {return getNullableResult(rs, columnName);} catch (Exception e) {throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set. Cause: " + e, e);}}// 子类需要实现的方法protected abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;protected abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;protected abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException;protected abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException;}
四、内置 TypeHandler 全集(常用)
TypeHandler | Java 类型 | JDBC 类型 |
|
|
, |
|
|
|
|
|
|
|
|
|
|
|
, |
|
|
, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
, |
|
|
(可配置) |
|
|
(存储序号) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
, |
|
|
|
... | ... | ... |
完整清单可以在org.apache.ibatis.type.TypeHandlerRegistry的构造方法中看到,MyBatis 启动时就会注册这些默认处理器。
五、TypeHandler 的注册与获取
5.1 TypeHandlerRegistry 核心结构
public final class TypeHandlerRegistry {// ✅ JdbcType → TypeHandler 映射(用于根据 jdbcType 快速查找)private final Map<JdbcType, TypeHandler<?>> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);// ✅ Type(可以是 Class 或 ParameterizedType)→ JdbcType → TypeHandler 映射private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();// ✅ 未知类型处理器private final TypeHandler<Object> unknownTypeHandler;// ✅ 所有 TypeHandler 实例的汇总(用于调试或遍历)private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();// ✅ 空 Map 常量,用于未找到时的默认返回值private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();}
在TypeHandlerRegistry构造方法中,会注册所有内置的 TypeHandler。
5.2 注册方式
方式一:全局配置 XML
<typeHandlers><typeHandlerhandler="com.example.MyTypeHandler"javaType="com.example.User"jdbcType="VARCHAR"/><!-- 或扫描包 --><packagename="com.example.typehandlers"/></typeHandlers>
方式二:注解@MappedTypes/@MappedJdbcTypes
@MappedTypes(User.class)@MappedJdbcTypes(JdbcType.VARCHAR)public class MyTypeHandler extends BaseTypeHandler<User> {// ...}
方式三:在 Configuration 中手动注册
configuration.getTypeHandlerRegistry().register(MyTypeHandler.class);5.3 获取 TypeHandler
MyBatis 在需要时会通过 TypeHandlerRegistry.getTypeHandler(Class<T> type, JdbcType jdbcType) 查找对应的 TypeHandler。查找顺序:(基于getTypeHandler(Type type, JdbcType jdbcType)方法):
- 根据传入的
type(Java 类型)从TYPE_HANDLER_MAP中获取对应的Map<JdbcType, TypeHandler<?>>。 - 若上一步获取的 Map 不为空,则尝试用传入的
jdbcType作为 key 精确匹配对应的TypeHandler。 - 若未匹配到,则尝试获取该 Map 中
jdbcType为null的默认TypeHandler。 - 若仍未找到,会递归查找其父类或实现的接口所对应的
TypeHandler。 - 若以上都未找到,且传入的
jdbcType不为null,才从JDBC_TYPE_HANDLER_MAP中根据jdbcType获取一个默认处理器作为兜底。 - 若最终仍未找到,返回
null(调用方可能会包装为UnknownTypeHandler)。
六、自定义 TypeHandler 开发
6.1 示例:将 java.util.Date 存储为 BIGINT(时间戳)
@MappedTypes(Date.class)@MappedJdbcTypes(JdbcType.BIGINT)public class DateToLongTypeHandler extends BaseTypeHandler<Date> {@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType) throws SQLException {ps.setLong(i, parameter.getTime());}@Overridepublic Date getNullableResult(ResultSet rs, String columnName) throws SQLException {long value = rs.getLong(columnName);return rs.wasNull() ? null : new Date(value);}@Overridepublic Date getNullableResult(ResultSet rs, int columnIndex) throws SQLException {long value = rs.getLong(columnIndex);return rs.wasNull() ? null : new Date(value);}@Overridepublic Date getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {long value = cs.getLong(columnIndex);return cs.wasNull() ? null : new Date(value);}}
6.2 注册自定义 TypeHandler
XML 方式:
<typeHandlers><typeHandlerhandler="com.example.DateToLongTypeHandler"/></typeHandlers>
注解方式(配合包扫描):
@MappedTypes(Date.class)@MappedJdbcTypes(JdbcType.BIGINT)public classDateToLongTypeHandlerextendsBaseTypeHandler<Date> { ... }
然后在 XML 中扫描包:
<typeHandlers><packagename="com.example.typehandlers"/></typeHandlers>
Mapper 中使用:如果 Java 类型和 JDBC 类型都能被自动匹配,则无需显式指定;否则可以在结果映射或参数中指定。
<resultMap id="userMap" type="User"><result column="create_time" property="createTime"javaType="java.util.Date" jdbcType="BIGINT"typeHandler="com.example.DateToLongTypeHandler"/></resultMap>
在参数中指定(例如update语句):
<update id="updateTime">UPDATE user SET create_time = #{createTime, typeHandler=com.example.DateToLongTypeHandler}WHERE id = #{id}</update>
七、TypeHandler 的调用时机源码分析
7.1 参数设置:DefaultParameterHandler.setParameters
在 Executor 执行 doQuery 或 doUpdate 之前,会调用 ParameterHandler.setParameters 为 PreparedStatement 设置参数。
// DefaultParameterHandler@Overridepublic void setParameters(PreparedStatement ps) {List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();for (int i = 0; i < parameterMappings.size(); i++) {ParameterMapping mapping = parameterMappings.get(i);// 非存储过程的 OUT 参数if (mapping.getMode() != ParameterMode.OUT) {Object value = getParameterValue(mapping, parameterObject);JdbcType jdbcType = mapping.getJdbcType();// 获取 TypeHandlerTypeHandler typeHandler = mapping.getTypeHandler();if (typeHandler == null) {// 从 registry 中根据 javaType 和 jdbcType 获取typeHandler = typeHandlerRegistry.getTypeHandler(mapping.getJavaType(), jdbcType);}// 调用 TypeHandler.setParametertypeHandler.setParameter(ps, i + 1, value, jdbcType);}}}
7.2 结果集映射:DefaultResultSetHandler.getPropertyMappingValue
在处理结果集时,会调用 ResultSetHandler.handleResultSets,内部对每个列使用对应的 TypeHandler 取值。
// DefaultResultSetHandlerprivate Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject,ResultMapping rm, ResultLoaderMap lazyLoader, String columnPrefix) {// 获取 TypeHandlerTypeHandler<?> typeHandler = rm.getTypeHandler();if (typeHandler == null) {// 从 registry 中获取typeHandler = typeHandlerRegistry.getTypeHandler(rm.getJavaType(), rm.getJdbcType());}// 根据列名或索引取值Object value = typeHandler.getResult(rs, rm.getColumn());// 延迟加载处理等return value;}
八、面试高频题
Q1:TypeHandler 的作用是什么?哪些场景需要自定义?
A:TypeHandler 负责 Java 类型与 JDBC 类型的双向转换。需要自定义的场景包括:
- Java 枚举与数据库自定义编码(如存储
ACTIVE字符串但 Java 中是枚举Status.ACTIVE)。 - Java 8 时间类型与数据库特定类型的转换(虽然 MyBatis 3.5+ 已内置
LocalDateTime支持,但早期版本需要自定义)。 - 复杂类型如 JSON 字段与 Java 对象的转换(配合 JSON 库)。
- 数据库不支持的类型(如 PostgreSQL 的
JSONB、MySQL 的GEOMETRY)。
Q2:BaseTypeHandler的setParameter为什么不能直接定义成抽象方法?
A:BaseTypeHandler需要统一处理null值的情况:如果参数为null,则调用ps.setNull(i, jdbcType)。如果让子类直接实现setParameter,每个子类都要重复写 null 判断,增加了代码冗余和出错风险。所以父类实现公共逻辑,子类只关注非 null 时的setNonNullParameter。
Q3:MyBatis 如何根据 Java 类型和 JDBC 类型找到合适的 TypeHandler?
A:MyBatis 使用 TypeHandlerRegistry 维护映射关系。查找顺序:TypeHandlerRegistry.getTypeHandler(Type type, JdbcType jdbcType) 的实际逻辑):
● 根据 javaType 从 TYPE_HANDLER_MAP 获取对应的 JdbcType → TypeHandler 映射;● 先用传入的 jdbcType 进行精确匹配;● 若未匹配到,则取该映射中 jdbcType 为 null 的默认处理器;● 若仍未找到,递归查找父类或接口对应的处理器;● 若以上均未找到且 jdbcType 非 null,从 JDBC_TYPE_HANDLER_MAP 中按 jdbcType兜底;● 最终仍未找到则返回 null(由调用方决定是否转为 UnknownTypeHandler)。
Q4:如何让 MyBatis 自动扫描自定义的 TypeHandler?
A:在 mybatis-config.xml 中配置:
<typeHandlers><packagename="com.example.typehandlers"/></typeHandlers>
MyBatis 会扫描该包下所有实现了TypeHandler接口的类,并根据@MappedTypes和@MappedJdbcTypes注解自动注册。在包扫描注册TypeHandler时:
- 如果类上有
@MappedTypes注解,则使用注解指定的 Java 类型进行注册。 - 如果类上没有
@MappedTypes注解,MyBatis 会尝试通过TypeReference机制解析出其泛型参数,并以此作为默认的 Java 类型进行注册-
Q5:TypeHandlerRegistry中为什么要区分TYPE_HANDLER_MAP和JDBC_TYPE_HANDLER_MAP?
A:
TYPE_HANDLER_MAP:Map<Type, Map<JdbcType, TypeHandler<?>>>。这是核心索引,通过 Java 类型找到对应的、按JdbcType区分的TypeHandler映射,支持“Java 类型 + JDBC 类型”的精确匹配。JDBC_TYPE_HANDLER_MAP:Map<JdbcType, TypeHandler<?>>。这是一个二级索引/兜底索引,用于通过JdbcType快速查找默认的TypeHandler。这种分离设计既支持快速映射,又支持精细化控制。
九、下篇预告
第 11 篇我们将深入MyBatis 事务管理与 Spring 整合原理,包括:
- MyBatis 原生事务管理器(
JdbcTransaction/ManagedTransaction) - Spring 如何接管 MyBatis 事务
SqlSession与 Spring 事务同步机制- 为什么整合后不需要手动
commit/rollback
如果觉得有帮助,欢迎点赞、在看、转发支持!
系列持续更新,关注不走丢 👇
夜雨聆风