乐于分享
好东西不私藏

MyBatis 源码深度拆解(十):TypeHandler 类型转换体系

MyBatis 源码深度拆解(十):TypeHandler 类型转换体系

面试官:MyBatis 如何实现 Java 类型与 JDBC 类型的转换?自定义 TypeHandler 该如何实现?它的底层调用时机是什么?

一、开篇

TypeHandler(类型处理器)是 MyBatis 中负责Java 类型与 JDBC 类型相互转换的核心组件。当你为 PreparedStatement 设置参数或从 ResultSet 中取值时,背后都是 TypeHandler 在工作。

核心职责

  • setParameter:将 Java 对象的值转换成 JDBC 类型,设置到 PreparedStatement
  • getResult:从 ResultSet / CallableStatement 中获取列值,转换成 Java 对象

本篇目标

  • 掌握 TypeHandler 的继承体系和核心方法
  • 了解 MyBatis 内置的 TypeHandler 全集
  • 学习自定义 TypeHandler 的开发步骤和注册方式
  • 追踪 TypeHandler 在参数赋值和结果集封装时的调用时机

二、TypeHandler 核心类

  • TypeHandler<T>:接口,定义四个核心方法。
  • BaseTypeHandler<T>:抽象类,实现了参数 null 值处理等公共逻辑,留下setNonNullParametergetNullableResult让子类实现。
  • 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> {    @Override    public 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);            }        }    }    @Override    public 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 类型

StringTypeHandler

String

VARCHAR

,CHAR

IntegerTypeHandler

Integer

INTEGER

LongTypeHandler

Long

BIGINT

DoubleTypeHandler

Double

DOUBLE

BigDecimalTypeHandler

BigDecimal

DECIMAL

,NUMERIC

BooleanTypeHandler

Boolean

BOOLEAN

,BIT

DateTypeHandler

java.util.Date

TIMESTAMP

DateOnlyTypeHandler

java.util.Date

DATE

LocalDateTimeTypeHandler

java.time.LocalDateTime

TIMESTAMP

LocalDateTypeHandler

java.time.LocalDate

DATE

LocalTimeTypeHandler

java.time.LocalTime

TIME

ByteArrayTypeHandler

byte[]

LONGVARBINARY

,BLOB

EnumTypeHandler

Enum

VARCHAR

(可配置)

EnumOrdinalTypeHandler

Enum

INTEGER

(存储序号)

SqlTimestampTypeHandler

java.sql.Timestamp

TIMESTAMP

SqlDateTypeHandler

java.sql.Date

DATE

TimeTypeHandler

java.sql.Time

TIME

BlobTypeHandler

Blob

BLOB

ClobTypeHandler

Clob

CLOB

NStringTypeHandler

String

NVARCHAR

,NCHAR

NClobTypeHandler

String

NCLOB

...

...

...

完整清单可以在org.apache.ibatis.type.TypeHandlerRegistry的构造方法中看到,MyBatis 启动时就会注册这些默认处理器。

五、TypeHandler 的注册与获取

5.1 TypeHandlerRegistry 核心结构

public final class TypeHandlerRegistry {    // ✅ JdbcType → TypeHandler 映射(用于根据 jdbcType 快速查找)    private final Map<JdbcTypeTypeHandler<?>> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);    // ✅ Type(可以是 Class 或 ParameterizedType)→ JdbcType → TypeHandler 映射    private final Map<TypeMap<JdbcTypeTypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();    // ✅ 未知类型处理器    private final TypeHandler<Object> unknownTypeHandler;    // ✅ 所有 TypeHandler 实例的汇总(用于调试或遍历)    private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();    // ✅ 空 Map 常量,用于未找到时的默认返回值    private static final Map<JdbcTypeTypeHandler<?>> 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)方法):

  1. 根据传入的type(Java 类型)从TYPE_HANDLER_MAP中获取对应的Map<JdbcType, TypeHandler<?>>
  2. 若上一步获取的 Map 不为空,则尝试用传入的jdbcType作为 key 精确匹配对应的TypeHandler
  3. 若未匹配到,则尝试获取该 Map 中jdbcTypenull的默认TypeHandler
  4. 若仍未找到,会递归查找其父类或实现的接口所对应的TypeHandler
  5. 若以上都未找到,且传入的jdbcType不为null,才从JDBC_TYPE_HANDLER_MAP中根据jdbcType获取一个默认处理器作为兜底
  6. 若最终仍未找到,返回 null(调用方可能会包装为 UnknownTypeHandler)。

六、自定义 TypeHandler 开发

6.1 示例:将 java.util.Date 存储为 BIGINT(时间戳)

@MappedTypes(Date.class)@MappedJdbcTypes(JdbcType.BIGINT)public class DateToLongTypeHandler extends BaseTypeHandler<Date> {    @Override    public void setNonNullParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType) throws SQLException {        ps.setLong(i, parameter.getTime());    }    @Override    public Date getNullableResult(ResultSet rs, String columnName) throws SQLException {        long value = rs.getLong(columnName);        return rs.wasNull() ? null : new Date(value);    }    @Override    public Date getNullableResult(ResultSet rs, int columnIndex) throws SQLException {        long value = rs.getLong(columnIndex);        return rs.wasNull() ? null : new Date(value);    }    @Override    public 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();            // 获取 TypeHandler            TypeHandler typeHandler = mapping.getTypeHandler();            if (typeHandler == null) {                // 从 registry 中根据 javaType 和 jdbcType 获取                typeHandler = typeHandlerRegistry.getTypeHandler(mapping.getJavaType(), jdbcType);            }            // 调用 TypeHandler.setParameter            typeHandler.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) {    // 获取 TypeHandler    TypeHandler<?> 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:BaseTypeHandlersetParameter为什么不能直接定义成抽象方法?

ABaseTypeHandler需要统一处理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时:

  1. 如果类上有@MappedTypes注解,则使用注解指定的 Java 类型进行注册。
  2. 如果类上没有 @MappedTypes 注解,MyBatis 会尝试通过 TypeReference 机制解析出其泛型参数,并以此作为默认的 Java 类型进行注册-

Q5:TypeHandlerRegistry中为什么要区分TYPE_HANDLER_MAPJDBC_TYPE_HANDLER_MAP

A

  • TYPE_HANDLER_MAPMap<Type, Map<JdbcType, TypeHandler<?>>>。这是核心索引,通过 Java 类型找到对应的、按 JdbcType 区分的 TypeHandler 映射,支持“Java 类型 + JDBC 类型”的精确匹配。
  • JDBC_TYPE_HANDLER_MAPMap<JdbcType, TypeHandler<?>>。这是一个二级索引/兜底索引,用于通过 JdbcType 快速查找默认的 TypeHandler这种分离设计既支持快速映射,又支持精细化控制。

九、下篇预告

第 11 篇我们将深入MyBatis 事务管理与 Spring 整合原理,包括:

  • MyBatis 原生事务管理器(JdbcTransaction/ManagedTransaction
  • Spring 如何接管 MyBatis 事务
  • SqlSession与 Spring 事务同步机制
  • 为什么整合后不需要手动commit/rollback

如果觉得有帮助,欢迎点赞、在看、转发支持!

系列持续更新,关注不走丢 👇