0%

mybatis之类型转换

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> {
// ------------------------------ 1. 参数绑定:Java类型 → JDBC类型 ------------------------------
// 为 PreparedStatement 绑定参数时调用,将 Java 对象转为 JDBC 支持的类型
// 参数:ps=PreparedStatement 对象;i=参数索引(从1开始);parameter=Java 参数值;jdbcType=目标 JDBC 类型
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

// ------------------------------ 2. 结果获取:JDBC类型 → Java类型 ------------------------------
// 从 ResultSet 按列名获取值,转为 Java 类型
T getResult(ResultSet rs, String columnName) throws SQLException;

// 从 ResultSet 按列索引获取值,转为 Java 类型
T getResult(ResultSet rs, int columnIndex) throws SQLException;

// 从 CallableStatement(存储过程)按列索引获取值,转为 Java 类型
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> {
// ------------------------------ 实现 TypeHandler 的 setParameter 方法 ------------------------------
@Override
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
// 1. 处理参数为 null 的情况
if (parameter == null) {
if (jdbcType == null) {
throw new TypeException("JDBC Type is required for null value.");
}
try {
// 调用 JDBC API 设置 null 值(需指定 JDBC 类型)
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 {
// 2. 参数非 null,调用抽象方法由子类实现具体转换
setNonNullParameter(ps, i, parameter, jdbcType);
} catch (Exception e) {
throw new TypeException("Error setting non null for parameter #" + i
+ " with JdbcType " + jdbcType
+ ". Cause: " + e, e);
}
}
}

// ------------------------------ 实现 TypeHandler 的 getResult 方法(按列名) ------------------------------
@Override
public T getResult(ResultSet rs, String columnName) throws SQLException {
try {
// 调用子类实现的非 null 结果获取逻辑
return getNullableResult(rs, columnName);
} catch (Exception e) {
throw new ResultMapException("Error attempting to get column '" + columnName
+ "' from result set. Cause: " + e, e);
}
}

// ------------------------------ 其他 getResult 方法(按索引、存储过程)逻辑类似 ------------------------------
@Override
public T getResult(ResultSet rs, int columnIndex) throws SQLException { /* 省略,逻辑同上 */ }

@Override
public T getResult(CallableStatement cs, int columnIndex) throws SQLException { /* 省略,逻辑同上 */ }

// ------------------------------ 抽象方法:子类需实现的非 null 转换逻辑 ------------------------------
// 非 null 参数绑定(Java→JDBC)
public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

// 非 null 结果获取(JDBC→Java,按列名)
public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;

// 非 null 结果获取(JDBC→Java,按列索引)
public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException;

// 非 null 结果获取(JDBC→Java,存储过程)
public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException;
}
核心价值:
  • 统一 null 处理:避免子类重复编写 null 值判断逻辑,减少冗余代码;
  • 简化接口:子类只需实现 4 个抽象方法,专注于 “非 null 类型转换”;
  • 异常封装:将 JDBC 异常封装为 MyBatis 统一的 TypeException/ResultMapException,便于问题定位。

内置 TypeHandler 示例:DateTypeHandler

MyBatis 为常用类型(如 StringIntegerDate)提供了内置 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> {
// 1. 参数绑定:Java Date → JDBC Timestamp
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType) throws SQLException {
// 将 Date 转为 Timestamp(JDBC 中推荐用 Timestamp 存储日期时间)
ps.setTimestamp(i, new Timestamp(parameter.getTime()));
}

// 2. 结果获取:JDBC Timestamp → Java Date(按列名)
@Override
public Date getNullableResult(ResultSet rs, String columnName) throws SQLException {
Timestamp sqlTimestamp = rs.getTimestamp(columnName);
return sqlTimestamp != null ? new Date(sqlTimestamp.getTime()) : null;
}

// 3. 结果获取:JDBC Timestamp → Java Date(按列索引)
@Override
public Date getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Timestamp sqlTimestamp = rs.getTimestamp(columnIndex);
return sqlTimestamp != null ? new Date(sqlTimestamp.getTime()) : null;
}

// 4. 结果获取:JDBC Timestamp → Java Date(存储过程)
@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(内置 + 自定义),负责:

  1. 初始化时注册内置 TypeHandler;
  2. 加载并注册用户自定义 TypeHandler;
  3. 根据 “Java 类型 + JDBC 类型” 匹配对应的 TypeHandler。

核心属性:存储 TypeHandler 映射关系

1
2
3
4
5
6
7
8
9
10
public class TypeHandlerRegistry {
// 1. Java 类型 → TypeHandler 映射(用于结果映射:JDBC→Java,无需指定 JDBC 类型时)
private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
// 2. 类型别名 → TypeHandler 映射(如 "string" → StringTypeHandler)
private final Map<String, TypeHandler<?>> typeHandlerAliasMap = new ConcurrentHashMap<>();
// 3. 基础类型(如 int、long)的 TypeHandler 快速查找
private final TypeHandler<Object> unknownTypeHandler;
// 4. JDBC 类型 → 默认 TypeHandler 映射(如 JDBC_TYPE.VARCHAR → StringTypeHandler)
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);

// 注册 String 类型处理器
register(String.class, new StringTypeHandler());
// 注册 Integer 类型处理器
register(Integer.class, new IntegerTypeHandler());
register(int.class, new IntegerTypeHandler());
// 注册 Date 类型处理器
register(Date.class, new DateTypeHandler());
// 注册 BigDecimal 类型处理器
register(BigDecimal.class, new BigDecimalTypeHandler());
// ... 省略其他内置类型(如 Boolean、Long、Float 等)的注册
}

自定义 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>
<!-- 1. 单个注册:指定 handler 类,可关联 JavaType 和 JdbcType -->
<typeHandler
javaType="com.example.enums.UserStatusEnum" <!-- 目标 Java 类型(如枚举) -->
jdbcType="VARCHAR" <!-- 目标 JDBC 类型 -->
handler="com.example.handler.UserStatusEnumTypeHandler"/> <!-- 自定义 TypeHandler 类 -->

<!-- 2. 包扫描批量注册:扫描指定包下所有继承 BaseTypeHandler 的类 -->
<package name="com.example.handler"/> <!-- 批量注册该包下的所有 TypeHandler -->
</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;

// 注解指定:该 TypeHandler 处理的 Java 类型(UserStatusEnum)
@MappedTypes({UserStatusEnum.class})
// 注解指定:该 TypeHandler 处理的 JDBC 类型(VARCHAR)
@MappedJdbcTypes({JdbcType.VARCHAR})
public class UserStatusEnumTypeHandler extends BaseTypeHandler<UserStatusEnum> {
// 实现抽象方法...
}
注册逻辑:
  • 包扫描时,MyBatis 会扫描指定包下所有 TypeHandler 实现类;
  • 若类上有 @MappedTypes@MappedJdbcTypes,则自动关联对应的 Java 类型和 JDBC 类型;
  • 若无注解,MyBatis 会通过 TypeReferenceBaseTypeHandler 的父类)获取泛型指定的 Java 类型(如 BaseTypeHandler<UserStatusEnum> 则 Java 类型为 UserStatusEnum)。

TypeHandler 匹配逻辑

当 MyBatis 需要转换类型时(如参数绑定、结果映射),会通过 TypeHandlerRegistry 按以下优先级匹配对应的 TypeHandler:

  1. 精确匹配:根据 “Java 类型 + JDBC 类型” 查找(如 UserStatusEnum + VARCHAR);
  2. Java 类型匹配:仅根据 Java 类型查找(适用于结果映射,无需指定 JDBC 类型);
  3. 默认匹配:若未找到,使用 UnknownTypeHandler(尝试自动推断类型)。
示例:参数绑定时的匹配

ParameterHandler.setParameters() 中,MyBatis 会调用 TypeHandlerRegistry.getTypeHandler() 匹配 TypeHandler:

1
2
3
4
5
6
7
// ParameterHandler 中获取 TypeHandler 的逻辑
TypeHandler<?> typeHandler = parameterMapping.getTypeHandler();
if (typeHandler == null) {
// 从 TypeHandlerRegistry 匹配 TypeHandler
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 0UserStatusEnum.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;
}

// 根据 code 反查枚举(用于结果映射)
public static UserStatusEnum getByCode(int code) {
for (UserStatusEnum status : values()) {
if (status.code == code) {
return status;
}
}
throw new IllegalArgumentException("无效的用户状态码:" + code);
}

// getter
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;

// 注解指定:Java类型=UserStatusEnum,JDBC类型=INTEGER
@MappedTypes({UserStatusEnum.class})
@MappedJdbcTypes({JdbcType.INTEGER})
public class UserStatusEnumTypeHandler extends BaseTypeHandler<UserStatusEnum> {

// 1. 参数绑定:UserStatusEnum → JDBC INTEGER
@Override
public void setNonNullParameter(PreparedStatement ps, int i, UserStatusEnum parameter, JdbcType jdbcType) throws SQLException {
// 将枚举的 code 作为 JDBC 值绑定
ps.setInt(i, parameter.getCode());
}

// 2. 结果映射:JDBC INTEGER → UserStatusEnum(按列名)
@Override
public UserStatusEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
int code = rs.getInt(columnName);
// 若数据库值为 null(rs.wasNull() 判断),返回 null;否则根据 code 反查枚举
return rs.wasNull() ? null : UserStatusEnum.getByCode(code);
}

// 3. 结果映射:JDBC INTEGER → UserStatusEnum(按列索引)
@Override
public UserStatusEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
int code = rs.getInt(columnIndex);
return rs.wasNull() ? null : UserStatusEnum.getByCode(code);
}

// 4. 结果映射:JDBC INTEGER → UserStatusEnum(存储过程)
@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
<!-- mybatis-config.xml -->
<configuration>
<typeHandlers>
<!-- 扫描自定义 TypeHandler 所在包 -->
<package name="com.example.handler"/>
</typeHandlers>
</configuration>

步骤 4:在 Mapper 中使用

无需额外配置,MyBatis 会自动匹配 TypeHandler:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 插入用户:status 字段自动使用 UserStatusEnumTypeHandler 转换 -->
<insert id="insertUser">
INSERT INTO user (username, status) VALUES (#{username}, #{status})
</insert>

<!-- 查询用户:status 字段自动转换为 UserStatusEnum -->
<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
// 插入用户(参数绑定:UserStatusEnum.ACTIVE → 1)
User user = new User();
user.setUsername("张三");
user.setStatus(UserStatusEnum.ACTIVE);
userMapper.insertUser(user);

// 查询用户(结果映射:1 → UserStatusEnum.ACTIVE)
User queryUser = userMapper.selectUserById(user.getId());
System.out.println(queryUser.getStatus()); // 输出:ACTIVE

常见问题与解决方案

1. 自定义 TypeHandler 不生效

  • 原因 1:未注册 TypeHandler(未配置 <typeHandlers> 或包路径错误);
  • 原因 2@MappedTypes/@MappedJdbcTypes 注解配置错误(Java 类型或 JDBC 类型不匹配);
  • 原因 3:Mapper 中显式指定了其他 TypeHandler(如 #{status, typeHandler=OtherHandler});
  • 解决方案:
    1. 检查 mybatis-config.xml<typeHandlers> 配置,确保包路径正确;
    2. 验证注解的 Java 类型和 JDBC 类型与实际匹配;
    3. 移除 Mapper 中显式指定的 TypeHandler(或改为自定义 Handler)。

2. 枚举类型转换报 “无效代码” 异常

  • 原因:结果映射时,数据库值无对应的枚举(如数据库值为 2,但枚举中无该 code);
  • 解决方案:
    1. 在枚举 getByCode() 方法中处理无效 code(返回默认值或抛出明确异常);
    2. 数据库添加约束(如 CHECK (status IN (0,1))),避免无效值存入。

3. null 值处理异常

  • 原因:参数为 null 但未指定 jdbcType(MyBatis 无法确定 JDBC 类型);
  • 解决方案:
    1. Mapper 中显式指定 jdbcType#{status, jdbcType=INTEGER}
    2. 全局配置默认 null 类型:mybatis-config.xml 中添加 <setting name="jdbcTypeForNull" value="NULL"/>

总结

TypeHandler 是 MyBatis 解决 Java 与 JDBC 类型不兼容的核心组件,其设计遵循 “接口定义规范 + 抽象基类简化实现 + 注册中心统一管理” 的思路:

  1. 接口TypeHandler 定义类型转换的标准方法;
  2. 基类BaseTypeHandler 抽离 null 处理,降低自定义成本;
  3. 注册TypeHandlerRegistry 管理所有 TypeHandler,支持内置和自定义;
  4. 使用:无需手动调用,MyBatis 自动在参数绑定和结果映射时触发转换

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10