JDBC 数据类型与 Java 语言中的数据类型并不是完全对应的,所以在 PreparedStatement 为 SQL 语句绑定参数时,需要从 Java 类型转换成 JDBC 类型;而从结果集中获取数据时,则需要从 JDBC 类型转换成 Java 类型。MyBatis 使用类型处理器完成上述两种转换。
image.png

TypeHandler

MyBatis 中所有的类型转换器都继承了 TypeHandler 接口,在 TypeHandler 接口中定义了如下四个方法:

  1. public interface TypeHandler<T> {
  2. // 在通过PreparedStatement为SQL语句绑定参数时,会将数据由JdbcType转成Java类型
  3. void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
  4. // 从ResultSet中获取数据时会调用此方法,会将数据由Java类型转成JdbcType
  5. T getResult(ResultSet rs, String columnName) throws SQLException;
  6. T getResult(ResultSet rs, int columnIndex) throws SQLException;
  7. T getResult(CallableStatement cs, int columnIndex) throws SQLException;
  8. }

为方便用户自定义 TypeHandler 实现,MyBatis 提供了 BaseTypeHandler 抽象类,它实现了 TypeHandler 接口,并继承了 TypeReference 抽象类,其结构如下图所示:
image.png
在 BaseTypeHandler 中,简单实现了 TypeHandler 接口的 setParameter() 方法和 getResult() 方法,并在其内部做了非空和异常处理。

  • 在 setParameter() 实现中,会判断传入的 parameter 参数是否为空,为空则调用 PreparedStatement 的 setNull() 方法进行设置;不为空则委托 setNonNullParameter() 这个抽象方法进行处理,该方法由 BaseTypeHandler 的子类提供具体实现。


  • 在 getResult() 的三个重载实现中,会直接调用相应的 getNullableResult() 抽象方法,这里有三个重载的 getNullableResult() 抽象方法,它们都由 BaseTypeHandler 的子类提供具体实现。

BaseTypeHandler 实现类有很多,但大多是直接调用 PreparedStatement、ResultSet 或 CallableStatement 的对应方法,实现比较简单。我们以 IntegerTypeHandler 为例简单介绍,至于其他 BaseTypeHandler 的实现,同样也都是依赖了 JDBC 的 API。

  1. public class IntegerTypeHandler extends BaseTypeHandler<Integer> {
  2. @Override
  3. public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
  4. throws SQLException {
  5. // 调用PreparedStatement.setInt()实现参数绑定
  6. ps.setInt(i, parameter);
  7. }
  8. @Override
  9. public Integer getNullableResult(ResultSet rs, String columnName)
  10. throws SQLException {
  11. // 调用ResultSet.getInt()获取指定列值
  12. return rs.getInt(columnName);
  13. }
  14. @Override
  15. public Integer getNullableResult(ResultSet rs, int columnIndex)
  16. throws SQLException {
  17. // 调用ResultSet.getInt()获取指定列值
  18. return rs.getInt(columnIndex);
  19. }
  20. @Override
  21. public Integer getNullableResult(CallableStatement cs, int columnIndex)
  22. throws SQLException {
  23. // 调用CallableStatement.getInt()获取指定列值
  24. return cs.getInt(columnIndex);
  25. }
  26. }

一般情况下,TypeHandler 用于完成单个参数以及单个列值的类型转换,如果存在多列值转换成一个 Java 对象的需求,应该优先考虑使用在映射文件中定义合适的映射规则( 节点)完成映射。

TypeHandlerRegistry

1. 注册 TypeHandler

在使用 MyBatis 时,我们可以在 mybatis-config.xml 中通过 <typeHandlers> 标签来配置自定义的 TypeHandler 实现,也可以在 Mapper.xml 配置文件定义的时候指定 typeHandler 属性。无论采用的是哪种配置方式,MyBatis 都会在初始化过程中,获取所有已知的 TypeHandler(包括内置实现和自定义实现),然后创建所有 TypeHandler 实例并注册到 TypeHandlerRegistry 中,由 TypeHandlerRegistry 负责统一管理所有的 TypeHandler 实例。TypeHandlerRegistry 管理 TypeHandler 时,用到了以下四个最核心的集合:

  1. public final class TypeHandlerRegistry {
  2. // 记录JdbcType与TypeHandler之间的对应关系,用于从结果集读取数据时,将数据从JDBC类型转成Java类型
  3. private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<>(JdbcType.class);
  4. // 记录了Java类型向指定JdbcType转换时,需要使用的TypeHandler对象,一对多关系。
  5. // 例如Java类型中的String可能转换成数据库中的varchar、char、text等多种类型。
  6. private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new ConcurrentHashMap<>();
  7. // 空TypeHandler集合的标识
  8. private final TypeHandler<Object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this);
  9. // 记录了全部TypeHandler的类型以及该类型相应的TypeHandler对象
  10. private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<>();
  11. }

TypeHandlerRegistry 的 register() 方法实现了注册 TypeHandler 对象的功能,该注册过程会向上述四个集合中添加 TypeHandler 对象。register() 方法有多个重载实现,这些重载中最基础的实现是三个参数的重载实现,具体实现如下:

  1. private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
  2. // 检测是否明确指定了TypeHandler能够处理的Java类型
  3. if (javaType != null) {
  4. // 根据指定的Java类型,从TYPE_HANDLER_MAP集合中获取相应的TypeHandler集合
  5. Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
  6. if (map == null || map == NULL_TYPE_HANDLER_MAP) {
  7. map = new HashMap<JdbcType, TypeHandler<?>>();
  8. TYPE_HANDLER_MAP.put(javaType, map);
  9. }
  10. map.put(jdbcType, handler);
  11. }
  12. ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
  13. }

除了上面的 register() 重载,在有的 register() 重载中会尝试从 TypeHandler 类中的 @MappedTypes 注解和 @MappedJdbcTypes 注解中读取信息。其中,@MappedTypes 注解中可以配置 TypeHandler 能够处理的 Java 类型的集合,@MappedJdbcTypes 注解中可以配置该 TypeHandler 能够处理的 JDBC 类型集合。

TypeHandlerRegistry 除了提供注册单个 TypeHandler 的 register() 重载,还可以扫描整个包下的 TypeHandler 接口实现类。在该重载中,会首先读取指定包下面的全部的 TypeHandler 实现类,然后再交给 register() 重载读取 @MappedTypes 注解和 @MappedJdbcTypes 注解,并最终完成注册。

  1. public void register(String packageName) {
  2. ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
  3. // 扫描指定包路径下的TypeHandler实现类
  4. resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
  5. Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
  6. for (Class<?> type : handlerSet) {
  7. // 过滤掉内部类、接口、抽象类
  8. if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
  9. // 注册TypeHandler实现类
  10. register(type);
  11. }
  12. }
  13. }

2. 查找 TypeHandler

下面我们再来看看 MyBatis 是如何从 TypeHandlerRegistry 底层的这几个集合中查找到正确的 TypeHandler 实例,该功能的具体实现是在 TypeHandlerRegistry 的 getTypeHandler() 方法中。这个 getTypeHandler() 方法也有多个重载,最核心的重载是 getTypeHandler(Type, JdbcType) 这个重载方法,其中会根据传入的 Java 类型和 JDBC 类型,从底层的几个集合中查询相应的 TypeHandler 实例,具体实现如下:

  1. private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
  2. if (ParamMap.class.equals(type)) {
  3. return null;
  4. }
  5. // 查找(或初始化)Java类型对应的TypeHandler集合
  6. Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
  7. TypeHandler<?> handler = null;
  8. if (jdbcHandlerMap != null) {
  9. // 根据JdbcType类型查找TypeHandler对象
  10. handler = jdbcHandlerMap.get(jdbcType);
  11. if (handler == null) {
  12. // 没有对应的TypeHandler实例,则使用null对应的TypeHandler
  13. handler = jdbcHandlerMap.get(null);
  14. }
  15. if (handler == null) {
  16. // 如果jdbcHandlerMap只注册了一个TypeHandler,则使用此TypeHandler对象
  17. handler = pickSoleHandler(jdbcHandlerMap);
  18. }
  19. }
  20. return (TypeHandler<T>) handler;
  21. }

在 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 中的枚举类型:

  1. public class IntEnumTypeHandler extends BaseTypeHandler<IntEnum> {
  2. @Override
  3. public void setNonNullParameter(PreparedStatement ps, int i, IntEnum parameter, JdbcType jdbcType) throws SQLException {
  4. ps.setInt(i, parameter.getCode());
  5. }
  6. @Override
  7. public IntEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
  8. int code = rs.getInt(columnName);
  9. return IntEnum.get(code);
  10. }
  11. @Override
  12. public IntEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
  13. int code = rs.getInt(columnIndex);
  14. return IntEnum.get(code);
  15. }
  16. @Override
  17. public IntEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
  18. int code = cs.getInt(columnIndex);
  19. return IntEnum.get(code);
  20. }
  21. }

然后在 mybatis-config.xml 配置文件中进行如下配置:

  1. <!-- 自定义TypeHandler -->
  2. <typeHandlers>
  3. <typeHandler handler="org.xl.mybatis.type.IntEnumTypeHandler" javaType="org.xl.mybatis.type.IntEnum" jdbcType="INTEGER" />
  4. </typeHandlers>

2. Mapper.xml

此外,我们还可以在 Mapper.xml 中的 标签中直接映射指定的 TypeHandler,配置如下:

  1. <resultMap id="UserInfoResultMap" type="UserInfo">
  2. <result column="code" property="code"
  3. javaType="org.xl.mybatis.type.IntEnum"
  4. jdbcType="INTEGER"
  5. typeHandler="org.xl.mybatis.type.IntEnumTypeHandler"/>
  6. </resultMap>

但这种方式只适用于查询操作,即只在查询的过程中系统会启用我们自定义的 TypeHandler,如果我们想要在插入或更新的时候启用自定义的 TypeHandler,需要我们进行如下配置:

  1. <insert id="insertUser" parameterType="org.xl.mybatis.UserInfo">
  2. 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})
  3. </insert>

3. 注解

上面提到,在注册 TypeHandler 时有一个注册方法是递归解析指定包路径下的所有 TypeHandler 实例,并根据该类上的注解进行注册。使用方式如下:

  1. @MappedTypes(IntEnum.class)
  2. @MappedJdbcTypes(JdbcType.INTEGER)
  3. public class IntEnumTypeHandler extends BaseTypeHandler<IntEnum> {
  4. ......
  5. }

通过注解表明了相关的 Java 类型和 JDBC 类型,然后在 mybatis-config.xml 配置文件中进行如下配置:

  1. <typeHandlers>
  2. <package name="org.xl.mybatis.type"/>
  3. </typeHandlers>