mybatis源码分析:TypeHandler类型处理器

Scroll Down

概述

JDBC数据类型与Java语言中的数据类型并不是完全对应的,所以在PreparedStatement为SQL语句绑定参数时,需要从Java类型转换成JDBC类型,而从结果集中获取数据时,则需要从JDBC类型转换成Java类型。Mybatis使用类型处理器完成上述两种转换。
在Mybatis中使用JdbcType这个枚举类型代表JDBC中的数据类型,该枚举类型中定义了TYPE_CODE字段,记录了JDBC类型在java.sql.Types中相应的常量编码,并通过一个静态集合codeLookup维护了常量编码与JdbcType之间的对应关系。

TypeHandler

Mybatis中所有的类型转换器都实现了TypeHandler接口,在TypeHandler接口中定义了如下四个方法,这四个方法分为两类:setParameter()方法负责将数据由JdbcType类型转换成Java类型;getResult()及其重载负责将数据由Java类型转换成JdbcType类型。

public interface TypeHandler<T> {
  // 使用PreparedStatement为SQL语句绑定参数,会调用此函数
  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  /**
   * Gets the result.
   *
   * @param rs
   *          the rs
   * @param columnName
   *          Colunm name, when configuration <code>useColumnLabel</code> is <code>false</code>
   * @return the result
   * @throws SQLException
   *           the SQL exception
   */
  // 从ResultSet中获取数据时,会调用此函数
  T getResult(ResultSet rs, String columnName) throws SQLException;

  T getResult(ResultSet rs, int columnIndex) throws SQLException;

  T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}

为了方便实现,Mybatis提供了BaseTypeHandler这个抽象类,它实现了TypeHandler接口,并继承了TypeReference抽象类,结构如下:
image.png
在BaseTypeHandler中实现了TypeHandler.setParameter()方法和TypeHandler.getResult()方法,如下所示:

@Override
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
    if (parameter == null) {
      if (jdbcType == null) {
        throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
      }
      try {
        // 如果参数为null,则调用PreparedStatement的setNull方法设置null值。
        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);
    }
}

@Override
public T getResult(ResultSet rs, int columnIndex) throws SQLException {
    try {
      return getNullableResult(rs, columnIndex);
    } catch (Exception e) {
      throw new ResultMapException("Error attempting to get column #" + columnIndex + " from result set.  Cause: " + e, e);
    }
}

@Override
public T getResult(CallableStatement cs, int columnIndex) throws SQLException {
    try {
      return getNullableResult(cs, columnIndex);
    } catch (Exception e) {
      throw new ResultMapException("Error attempting to get column #" + columnIndex + " from callable statement.  Cause: " + e, e);
    }
}

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

TypeHandlerRegistry

在Mybatis初始化过程中,会为所有已知的TypeHandler创建对象,并实现注册到TypeHandlerRegistry中,由TypeHandlerRegistry负责管理这些TypeHandler对象。
核心字段如下含义:

// 记录JdbcType与TypeHandler之间的对应关系,其中JdbcType是一个枚举类型,它定义对应的JDBC类型
// 该集合主要用于从结果集读取数据时,将数据从JDBC类型转换成Java类型
private final Map<JdbcType, TypeHandler<?>>  jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);
// 记录了Java类型向指定JdbcType转换时,需要使用的TypeHandler对象。
private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
// 记录了全部的TypeHandler的类型及TypeHandler对象
private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();
// 空TypeHandler集合的标识
private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();

1、注册TypeHandler对象

TypeHandlerRegistry.register()方法实现了注册TypeHandler对象的功能,该注册过程会向上述四个集合中添加TypeHandler对象。register()方法有多个重载。

private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    if (javaType != null) {
      Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
      if (map == null || map == NULL_TYPE_HANDLER_MAP) {
        map = new HashMap<>();
      }
      map.put(jdbcType, handler);
      typeHandlerMap.put(javaType, map);
    }
    allTypeHandlersMap.put(handler.getClass(), handler);
}

2、查找TypeHandler

getTypeHandler方法如下:

private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
    if (ParamMap.class.equals(type)) {
      return null;
    }
    Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
    TypeHandler<?> handler = null;
    if (jdbcHandlerMap != null) {
      handler = jdbcHandlerMap.get(jdbcType);
      if (handler == null) {
        handler = jdbcHandlerMap.get(null);
      }
      if (handler == null) {
        // #591
        handler = pickSoleHandler(jdbcHandlerMap);
      }
    }
    // type drives generics here
    return (TypeHandler<T>) handler;
}

最后,除了Mybatis本身提供的TypeHandler实现,我们也可以添加自定义的TypeHandler接口实现,添加方式是在mybatis-config.xml配置文件中的节点下,添加相应的节点配置,并指定自定义的TypeHandler接口实现类。在Mybatis初始化时会解析该节点,并将该TypeHandler类型的对象注册到TypeHandlerRegistry中,供Mybatis后续使用。