JDBC 数据类型与 Java 语言中的数据类型并不是完全对应的,所以在 PreparedStatement 为 SQL 语句绑定参数时,需要从 Java 类型转换成 JDBC 类型;而从结果集中获取数据时,则需要从 JDBC 类型转换成 Java 类型。MyBatis 使用类型处理器完成上述两种转换。
TypeHandler
MyBatis 中所有的类型转换器都继承了 TypeHandler 接口,在 TypeHandler 接口中定义了如下四个方法:
public interface TypeHandler<T> {
// 在通过PreparedStatement为SQL语句绑定参数时,会将数据由JdbcType转成Java类型
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
// 从ResultSet中获取数据时会调用此方法,会将数据由Java类型转成JdbcType
T getResult(ResultSet rs, String columnName) throws SQLException;
T getResult(ResultSet rs, int columnIndex) throws SQLException;
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
为方便用户自定义 TypeHandler 实现,MyBatis 提供了 BaseTypeHandler 抽象类,它实现了 TypeHandler 接口,并继承了 TypeReference 抽象类,其结构如下图所示:
在 BaseTypeHandler 中,简单实现了 TypeHandler 接口的 setParameter() 方法和 getResult() 方法,并在其内部做了非空和异常处理。
- 在 setParameter() 实现中,会判断传入的 parameter 参数是否为空,为空则调用 PreparedStatement 的 setNull() 方法进行设置;不为空则委托 setNonNullParameter() 这个抽象方法进行处理,该方法由 BaseTypeHandler 的子类提供具体实现。
- 在 getResult() 的三个重载实现中,会直接调用相应的 getNullableResult() 抽象方法,这里有三个重载的 getNullableResult() 抽象方法,它们都由 BaseTypeHandler 的子类提供具体实现。
BaseTypeHandler 实现类有很多,但大多是直接调用 PreparedStatement、ResultSet 或 CallableStatement 的对应方法,实现比较简单。我们以 IntegerTypeHandler 为例简单介绍,至于其他 BaseTypeHandler 的实现,同样也都是依赖了 JDBC 的 API。
public class IntegerTypeHandler extends BaseTypeHandler<Integer> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
throws SQLException {
// 调用PreparedStatement.setInt()实现参数绑定
ps.setInt(i, parameter);
}
@Override
public Integer getNullableResult(ResultSet rs, String columnName)
throws SQLException {
// 调用ResultSet.getInt()获取指定列值
return rs.getInt(columnName);
}
@Override
public Integer getNullableResult(ResultSet rs, int columnIndex)
throws SQLException {
// 调用ResultSet.getInt()获取指定列值
return rs.getInt(columnIndex);
}
@Override
public Integer getNullableResult(CallableStatement cs, int columnIndex)
throws SQLException {
// 调用CallableStatement.getInt()获取指定列值
return cs.getInt(columnIndex);
}
}
一般情况下,TypeHandler 用于完成单个参数以及单个列值的类型转换,如果存在多列值转换成一个 Java 对象的需求,应该优先考虑使用在映射文件中定义合适的映射规则(
TypeHandlerRegistry
1. 注册 TypeHandler
在使用 MyBatis 时,我们可以在 mybatis-config.xml 中通过 <typeHandlers> 标签来配置自定义的 TypeHandler 实现,也可以在 Mapper.xml 配置文件定义的时候指定 typeHandler 属性。无论采用的是哪种配置方式,MyBatis 都会在初始化过程中,获取所有已知的 TypeHandler(包括内置实现和自定义实现),然后创建所有 TypeHandler 实例并注册到 TypeHandlerRegistry 中,由 TypeHandlerRegistry 负责统一管理所有的 TypeHandler 实例。TypeHandlerRegistry 管理 TypeHandler 时,用到了以下四个最核心的集合:
public final class TypeHandlerRegistry {
// 记录JdbcType与TypeHandler之间的对应关系,用于从结果集读取数据时,将数据从JDBC类型转成Java类型
private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<>(JdbcType.class);
// 记录了Java类型向指定JdbcType转换时,需要使用的TypeHandler对象,一对多关系。
// 例如Java类型中的String可能转换成数据库中的varchar、char、text等多种类型。
private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new ConcurrentHashMap<>();
// 空TypeHandler集合的标识
private final TypeHandler<Object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this);
// 记录了全部TypeHandler的类型以及该类型相应的TypeHandler对象
private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<>();
}
TypeHandlerRegistry 的 register() 方法实现了注册 TypeHandler 对象的功能,该注册过程会向上述四个集合中添加 TypeHandler 对象。register() 方法有多个重载实现,这些重载中最基础的实现是三个参数的重载实现,具体实现如下:
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
// 检测是否明确指定了TypeHandler能够处理的Java类型
if (javaType != null) {
// 根据指定的Java类型,从TYPE_HANDLER_MAP集合中获取相应的TypeHandler集合
Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<JdbcType, TypeHandler<?>>();
TYPE_HANDLER_MAP.put(javaType, map);
}
map.put(jdbcType, handler);
}
ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}
除了上面的 register() 重载,在有的 register() 重载中会尝试从 TypeHandler 类中的 @MappedTypes 注解和 @MappedJdbcTypes 注解中读取信息。其中,@MappedTypes 注解中可以配置 TypeHandler 能够处理的 Java 类型的集合,@MappedJdbcTypes 注解中可以配置该 TypeHandler 能够处理的 JDBC 类型集合。
TypeHandlerRegistry 除了提供注册单个 TypeHandler 的 register() 重载,还可以扫描整个包下的 TypeHandler 接口实现类。在该重载中,会首先读取指定包下面的全部的 TypeHandler 实现类,然后再交给 register() 重载读取 @MappedTypes 注解和 @MappedJdbcTypes 注解,并最终完成注册。
public void register(String packageName) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
// 扫描指定包路径下的TypeHandler实现类
resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
for (Class<?> type : handlerSet) {
// 过滤掉内部类、接口、抽象类
if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
// 注册TypeHandler实现类
register(type);
}
}
}
2. 查找 TypeHandler
下面我们再来看看 MyBatis 是如何从 TypeHandlerRegistry 底层的这几个集合中查找到正确的 TypeHandler 实例,该功能的具体实现是在 TypeHandlerRegistry 的 getTypeHandler() 方法中。这个 getTypeHandler() 方法也有多个重载,最核心的重载是 getTypeHandler(Type, JdbcType) 这个重载方法,其中会根据传入的 Java 类型和 JDBC 类型,从底层的几个集合中查询相应的 TypeHandler 实例,具体实现如下:
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
if (ParamMap.class.equals(type)) {
return null;
}
// 查找(或初始化)Java类型对应的TypeHandler集合
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
TypeHandler<?> handler = null;
if (jdbcHandlerMap != null) {
// 根据JdbcType类型查找TypeHandler对象
handler = jdbcHandlerMap.get(jdbcType);
if (handler == null) {
// 没有对应的TypeHandler实例,则使用null对应的TypeHandler
handler = jdbcHandlerMap.get(null);
}
if (handler == null) {
// 如果jdbcHandlerMap只注册了一个TypeHandler,则使用此TypeHandler对象
handler = pickSoleHandler(jdbcHandlerMap);
}
}
return (TypeHandler<T>) handler;
}
在 getTypeHandler() 方法中会调用 getJdbcHandlerMap() 方法检测 typeHandlerMap 集合中相应的 TypeHandler 集合是否已经初始化。如果已初始化,则直接使用该集合进行查询;如果未初始化,则尝试以传入的 Java 类型的、已初始化的父类对应的 TypeHandler 集合作为初始集合;如果该 Java 类型的父类没有关联任何已初始化的 TypeHandler 集合,则将该 Java 类型对应的 TypeHandler 集合初始化为 NULL_TYPE_HANDLER_MAP 标识。
自定义 TypeHandler
除了 MyBatis 本身提供的 TypeHandler 实现,我们也可以添加自定义的 TypeHandler 接口实现,添加方式有如下几种,下面我们分别来看下:
1. mybatis-config.xml
可以在 mybatis-config.xml 配置文件中的 <typeHandlers> 节点下,添加相应的 <typeHandler> 节点配置,并指定自定义的 TypeHandler 接口实现类。在 MyBatis 初始化时会解析该节点,并将该 TypeHandler 类型的对象注册到 TypeHandlerRegistry 中,供 MyBatis 后续使用。
如下示例是把数据库中的数字类型转换成 Java 中的枚举类型:
public class IntEnumTypeHandler extends BaseTypeHandler<IntEnum> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, IntEnum parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.getCode());
}
@Override
public IntEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
int code = rs.getInt(columnName);
return IntEnum.get(code);
}
@Override
public IntEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
int code = rs.getInt(columnIndex);
return IntEnum.get(code);
}
@Override
public IntEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
int code = cs.getInt(columnIndex);
return IntEnum.get(code);
}
}
然后在 mybatis-config.xml 配置文件中进行如下配置:
<!-- 自定义TypeHandler -->
<typeHandlers>
<typeHandler handler="org.xl.mybatis.type.IntEnumTypeHandler" javaType="org.xl.mybatis.type.IntEnum" jdbcType="INTEGER" />
</typeHandlers>
2. Mapper.xml
此外,我们还可以在 Mapper.xml 中的
<resultMap id="UserInfoResultMap" type="UserInfo">
<result column="code" property="code"
javaType="org.xl.mybatis.type.IntEnum"
jdbcType="INTEGER"
typeHandler="org.xl.mybatis.type.IntEnumTypeHandler"/>
</resultMap>
但这种方式只适用于查询操作,即只在查询的过程中系统会启用我们自定义的 TypeHandler,如果我们想要在插入或更新的时候启用自定义的 TypeHandler,需要我们进行如下配置:
<insert id="insertUser" parameterType="org.xl.mybatis.UserInfo">
INSERT INTO user_infp(name, code, address) VALUES (#{name}, #{code,javaType=org.xl.mybatis.type.IntEnum,jdbcType=INTEGER,typeHandler=org.xl.mybatis.type.IntEnumTypeHandler}, #{address})
</insert>
3. 注解
上面提到,在注册 TypeHandler 时有一个注册方法是递归解析指定包路径下的所有 TypeHandler 实例,并根据该类上的注解进行注册。使用方式如下:
@MappedTypes(IntEnum.class)
@MappedJdbcTypes(JdbcType.INTEGER)
public class IntEnumTypeHandler extends BaseTypeHandler<IntEnum> {
......
}
通过注解表明了相关的 Java 类型和 JDBC 类型,然后在 mybatis-config.xml 配置文件中进行如下配置:
<typeHandlers>
<package name="org.xl.mybatis.type"/>
</typeHandlers>