MyBatis TypeHandler 深度解析:Java 与 JDBC 类型转换的桥梁(从原理到自定义实践) 在 MyBatis 与数据库交互过程中,Java 类型与 JDBC 类型无法直接兼容 (如 Java 的 Date
与 JDBC 的 Timestamp
、Java 枚举与 JDBC 的 VARCHAR
)。TypeHandler(类型处理器)正是解决这一问题的核心组件,负责参数绑定时的 Java→JDBC 类型转换 和结果映射时的 JDBC→Java 类型转换 ,是 MyBatis 数据交互的 “翻译官”。从 “接口定义→核心实现→注册管理→自定义实践” 四个维度,彻底拆解 TypeHandler 的工作机制。
TypeHandler 核心定位与接口规范 TypeHandler 是 MyBatis 中类型转换的标准接口 ,定义了 “参数绑定” 和 “结果获取” 两类核心操作,覆盖所有数据交互场景。
TypeHandler 接口定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public 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 ; }
接口设计思路:
泛型 <T>
:指定该 TypeHandler 处理的 Java 类型 (如 DateTypeHandler
的泛型是 Date
);
两类操作:
setParameter
:参数绑定时的 “正向转换”(Java→JDBC),由 ParameterHandler
调用;
getResult
:结果映射时的 “反向转换”(JDBC→Java),由 ResultSetHandler
调用。
BaseTypeHandler:简化自定义实现的抽象基类 MyBatis 提供 BaseTypeHandler<T>
抽象类,它实现了 TypeHandler<T>
接口,并抽离了 null 值的统一处理逻辑 ,让自定义 TypeHandler 只需关注 “非 null 类型转换”,大幅降低开发成本。
核心源码解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 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 ) { throw new TypeException("JDBC Type is required for null value." ); } try { ps.setNull(i, jdbcType.TYPE_CODE); } catch (SQLException e) { throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + ". 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 + ". 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); } } @Override public T getResult (ResultSet rs, int columnIndex) throws SQLException { } @Override public T getResult (CallableStatement cs, int columnIndex) throws SQLException { } public abstract void setNonNullParameter (PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException ; public abstract T getNullableResult (ResultSet rs, String columnName) throws SQLException ; public abstract T getNullableResult (ResultSet rs, int columnIndex) throws SQLException ; public abstract T getNullableResult (CallableStatement cs, int columnIndex) throws SQLException ; }
核心价值:
统一 null 处理 :避免子类重复编写 null 值判断逻辑,减少冗余代码;
简化接口 :子类只需实现 4 个抽象方法,专注于 “非 null 类型转换”;
异常封装 :将 JDBC 异常封装为 MyBatis 统一的 TypeException
/ResultMapException
,便于问题定位。
内置 TypeHandler 示例:DateTypeHandler MyBatis 为常用类型(如 String
、Integer
、Date
)提供了内置 TypeHandler,无需自定义即可直接使用。以 DateTypeHandler
为例,它负责 Java Date
与 JDBC Timestamp
的转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class DateTypeHandler extends BaseTypeHandler <Date > { @Override public void setNonNullParameter (PreparedStatement ps, int i, Date parameter, JdbcType jdbcType) throws SQLException { ps.setTimestamp(i, new Timestamp(parameter.getTime())); } @Override public Date getNullableResult (ResultSet rs, String columnName) throws SQLException { Timestamp sqlTimestamp = rs.getTimestamp(columnName); return sqlTimestamp != null ? new Date(sqlTimestamp.getTime()) : null ; } @Override public Date getNullableResult (ResultSet rs, int columnIndex) throws SQLException { Timestamp sqlTimestamp = rs.getTimestamp(columnIndex); return sqlTimestamp != null ? new Date(sqlTimestamp.getTime()) : null ; } @Override public Date getNullableResult (CallableStatement cs, int columnIndex) throws SQLException { Timestamp sqlTimestamp = cs.getTimestamp(columnIndex); return sqlTimestamp != null ? new Date(sqlTimestamp.getTime()) : null ; } }
转换逻辑:
正向转换(Java→JDBC) :Date.getTime()
获取毫秒数,构建 Timestamp
对象,通过 ps.setTimestamp()
绑定;
反向转换(JDBC→Java) :从 ResultSet
获取 Timestamp
,转为 Date
(利用 Timestamp
继承 Date
的特性,或通过毫秒数构建)。
TypeHandlerRegistry:类型处理器的注册与管理 MyBatis 通过 TypeHandlerRegistry
管理所有 TypeHandler(内置 + 自定义),负责:
初始化时注册内置 TypeHandler;
加载并注册用户自定义 TypeHandler;
根据 “Java 类型 + JDBC 类型” 匹配对应的 TypeHandler。
核心属性:存储 TypeHandler 映射关系 1 2 3 4 5 6 7 8 9 10 public class TypeHandlerRegistry { private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>(); private final Map<String, TypeHandler<?>> typeHandlerAliasMap = new ConcurrentHashMap<>(); private final TypeHandler<Object> unknownTypeHandler; private final Map<JdbcType, TypeHandler<?>> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class); }
初始化:注册内置 TypeHandler MyBatis 在 TypeHandlerRegistry
构造函数中注册内置 TypeHandler,覆盖所有常用类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public TypeHandlerRegistry (Configuration configuration) { this .unknownTypeHandler = new UnknownTypeHandler(configuration); register(String.class, new StringTypeHandler()); register(Integer.class, new IntegerTypeHandler()); register(int .class, new IntegerTypeHandler()); register(Date.class, new DateTypeHandler()); register(BigDecimal.class, new BigDecimalTypeHandler()); }
自定义 TypeHandler 的注册方式 用户自定义的 TypeHandler 需注册到 TypeHandlerRegistry
才能生效,MyBatis 提供 XML 配置 和 注解配置 两种方式。
方式 1:XML 配置(mybatis-config.xml) 通过 <typeHandlers>
标签注册,支持 “单个注册” 和 “包扫描批量注册”:
1 2 3 4 5 6 7 8 9 10 11 12 <configuration > <typeHandlers > <typeHandler javaType="com.example.enums.UserStatusEnum" <!-- 目标 Java 类型(如枚举) --> jdbcType="VARCHAR" handler="com.example.handler.UserStatusEnumTypeHandler"/> <package name ="com.example.handler" /> </typeHandlers > </configuration >
方式 2:注解配置(@MappedTypes + @MappedJdbcTypes) 通过注解直接在自定义 TypeHandler 类上指定关联的 Java 类型和 JDBC 类型,配合包扫描使用更高效:
1 2 3 4 5 6 7 8 9 10 11 import org.apache.ibatis.type.MappedJdbcTypes;import org.apache.ibatis.type.MappedTypes;import org.apache.ibatis.type.JdbcType;@MappedTypes({UserStatusEnum.class}) @MappedJdbcTypes({JdbcType.VARCHAR}) public class UserStatusEnumTypeHandler extends BaseTypeHandler <UserStatusEnum > { }
注册逻辑:
包扫描时,MyBatis 会扫描指定包下所有 TypeHandler
实现类;
若类上有 @MappedTypes
和 @MappedJdbcTypes
,则自动关联对应的 Java 类型和 JDBC 类型;
若无注解,MyBatis 会通过 TypeReference
(BaseTypeHandler
的父类)获取泛型指定的 Java 类型(如 BaseTypeHandler<UserStatusEnum>
则 Java 类型为 UserStatusEnum
)。
TypeHandler 匹配逻辑 当 MyBatis 需要转换类型时(如参数绑定、结果映射),会通过 TypeHandlerRegistry
按以下优先级匹配对应的 TypeHandler:
精确匹配 :根据 “Java 类型 + JDBC 类型” 查找(如 UserStatusEnum
+ VARCHAR
);
Java 类型匹配 :仅根据 Java 类型查找(适用于结果映射,无需指定 JDBC 类型);
默认匹配 :若未找到,使用 UnknownTypeHandler
(尝试自动推断类型)。
示例:参数绑定时的匹配 在 ParameterHandler.setParameters()
中,MyBatis 会调用 TypeHandlerRegistry.getTypeHandler()
匹配 TypeHandler:
1 2 3 4 5 6 7 TypeHandler<?> typeHandler = parameterMapping.getTypeHandler(); if (typeHandler == null ) { typeHandler = configuration.getTypeHandlerRegistry() .getTypeHandler(parameterValue.getClass(), parameterMapping.getJdbcType()); }
自定义 TypeHandler 实战:处理枚举类型 枚举是业务开发中常用的类型(如 UserStatusEnum{ACTIVE(1, "活跃"), INACTIVE(0, "禁用")}
),但 JDBC 不直接支持枚举类型,需自定义 TypeHandler 实现 枚举与数据库值(如 int/varchar) 的转换。
需求场景 假设 User
类中有 UserStatusEnum
类型的 status
字段,数据库中存储为 INT
(1 = 活跃,0 = 禁用),需实现:
参数绑定:UserStatusEnum.ACTIVE
→ JDBC 1
;
结果映射:JDBC 0
→ UserStatusEnum.INACTIVE
。
步骤 1:定义枚举类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public enum UserStatusEnum { ACTIVE(1 , "活跃" ), INACTIVE(0 , "禁用" ); private final int code; private final String desc; UserStatusEnum(int code, String desc) { this .code = code; this .desc = desc; } public static UserStatusEnum getByCode (int code) { for (UserStatusEnum status : values()) { if (status.code == code) { return status; } } throw new IllegalArgumentException("无效的用户状态码:" + code); } public int getCode () { return code; } public String getDesc () { return desc; } }
步骤 2:自定义 TypeHandler 继承 BaseTypeHandler<UserStatusEnum>
,实现非 null 转换逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import org.apache.ibatis.type.BaseTypeHandler;import org.apache.ibatis.type.JdbcType;import org.apache.ibatis.type.MappedJdbcTypes;import org.apache.ibatis.type.MappedTypes;@MappedTypes({UserStatusEnum.class}) @MappedJdbcTypes({JdbcType.INTEGER}) public class UserStatusEnumTypeHandler extends BaseTypeHandler <UserStatusEnum > { @Override public void setNonNullParameter (PreparedStatement ps, int i, UserStatusEnum parameter, JdbcType jdbcType) throws SQLException { ps.setInt(i, parameter.getCode()); } @Override public UserStatusEnum getNullableResult (ResultSet rs, String columnName) throws SQLException { int code = rs.getInt(columnName); return rs.wasNull() ? null : UserStatusEnum.getByCode(code); } @Override public UserStatusEnum getNullableResult (ResultSet rs, int columnIndex) throws SQLException { int code = rs.getInt(columnIndex); return rs.wasNull() ? null : UserStatusEnum.getByCode(code); } @Override public UserStatusEnum getNullableResult (CallableStatement cs, int columnIndex) throws SQLException { int code = cs.getInt(columnIndex); return cs.wasNull() ? null : UserStatusEnum.getByCode(code); } }
步骤 3:注册 TypeHandler 通过 XML 包扫描注册(推荐,批量处理):
1 2 3 4 5 6 7 <configuration > <typeHandlers > <package name ="com.example.handler" /> </typeHandlers > </configuration >
步骤 4:在 Mapper 中使用 无需额外配置,MyBatis 会自动匹配 TypeHandler:
1 2 3 4 5 6 7 8 9 10 11 12 <mapper namespace ="com.example.mapper.UserMapper" > <insert id ="insertUser" > INSERT INTO user (username, status) VALUES (#{username}, #{status}) </insert > <select id ="selectUserById" resultType ="com.example.model.User" > SELECT id, username, status FROM user WHERE id = #{id} </select > </mapper >
测试代码: 1 2 3 4 5 6 7 8 9 User user = new User(); user.setUsername("张三" ); user.setStatus(UserStatusEnum.ACTIVE); userMapper.insertUser(user); User queryUser = userMapper.selectUserById(user.getId()); System.out.println(queryUser.getStatus());
常见问题与解决方案 1. 自定义 TypeHandler 不生效
原因 1 :未注册 TypeHandler(未配置 <typeHandlers>
或包路径错误);
原因 2 :@MappedTypes
/@MappedJdbcTypes
注解配置错误(Java 类型或 JDBC 类型不匹配);
原因 3 :Mapper 中显式指定了其他 TypeHandler(如 #{status, typeHandler=OtherHandler}
);
解决方案:
检查 mybatis-config.xml
的 <typeHandlers>
配置,确保包路径正确;
验证注解的 Java 类型和 JDBC 类型与实际匹配;
移除 Mapper 中显式指定的 TypeHandler(或改为自定义 Handler)。
2. 枚举类型转换报 “无效代码” 异常
原因 :结果映射时,数据库值无对应的枚举(如数据库值为 2,但枚举中无该 code);
解决方案:
在枚举 getByCode()
方法中处理无效 code(返回默认值或抛出明确异常);
数据库添加约束(如 CHECK (status IN (0,1))
),避免无效值存入。
3. null 值处理异常
原因 :参数为 null 但未指定 jdbcType
(MyBatis 无法确定 JDBC 类型);
解决方案:
Mapper 中显式指定 jdbcType
:#{status, jdbcType=INTEGER}
;
全局配置默认 null 类型:mybatis-config.xml
中添加 <setting name="jdbcTypeForNull" value="NULL"/>
。
总结 TypeHandler 是 MyBatis 解决 Java 与 JDBC 类型不兼容的核心组件,其设计遵循 “接口定义规范 + 抽象基类简化实现 + 注册中心统一管理” 的思路:
接口 :TypeHandler
定义类型转换的标准方法;
基类 :BaseTypeHandler
抽离 null 处理,降低自定义成本;
注册 :TypeHandlerRegistry
管理所有 TypeHandler,支持内置和自定义;
使用 :无需手动调用,MyBatis 自动在参数绑定和结果映射时触发转换
v1.3.10