类型转换

1. 概述

​ JDBC数据类型与Java语言中的数据类型并不是完全对应的,所以在PreparedStatementSQL语句绑定参数是,需要从Java类型转换成JDBC类型,而从ResultSet中获取数据时,则需要从JDBC类型转换成Java类型。Mybatis使用类型转换器完成上述两种转换。据图如下图所示:

​ 在Mybatis中使用JdbcType这种枚举类型代表JDBC中的数据类型,该枚举类型中定义了TYPE_CODE字段,记录了JDBC类中在java.sql.Types中相应的常量编码,并通过一个静态集合codeLookUp(HashMap<Integer,JdbcType>类型)维护了常量编码与JdbcType之间的对应关系。

2. TypeHandler

​ Mybatis中所有的类型转换器都继承了TypeHandler接口,在TypeHandler接口中定义了如下四种方法,这四种方法分为两类:

  • setParameter()方法
    • 负责将数据由Java类型转换成JdbcType类型
  • getResultSet()方法
    • 负责将数据由JdbcType类型转换成Java类型
package org.apache.ibatis.type;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * 类型处理器
 * 说白了typeHandlers就是用来完成javaType和jdbcType之间的转换
 * @author Clinton Begin
 */
public interface TypeHandler<T> {

  /**
   * 通过PreparedStatement为Sql语句绑定参数是,会将数据从Java类型转换成JdbcType类型
   * @param ps
   * @param i 转换第几个参数
   * @param parameter 参数
   * @param jdbcType  要转换的jdbcType的类型
   * @throws SQLException
   */
  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  /**
   * 从ResultSet中获取数据时会调用此方法,将数据有JdbcType类型转换为Java类型
   * @param columnName Colunm name, when configuration <code>useColumnLabel</code> is <code>false</code>
   */
  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中实现了setParameter()getResult()方法,具体如下所示。

/**
   * 在设置参数的时候,只处理为null的数据,不为空的数据都交给了子类实现
   * @param ps
   * @param i 转换第几个参数
   * @param parameter 参数
   * @param jdbcType  要转换的jdbcType的类型
   * @throws SQLException
   */
  @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 {
        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);
      }
    }
  }

public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  @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);
    }
  }
  /**
   * 3.5.0版本之后getResult方法,不管是空还是非空数据都要交给子类去处理
   * @param columnName Colunm name, when configuration <code>useColumnLabel</code> is <code>false</code>
   */
  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;

需要注意的是:Mybatis3.5及其之后版本,BaseTypeHandler.setParemeter()只处理空参,非空参数交于子类处理,BaseTypeHandler.getResult()不管是空值还是非空都交于子类处理,而Mybatis3.5版本之前,BaseTypeHandler.setParemeter()BaseTypeHandler.getResult()都是只对空参进行处理的。

BaseTypeHandler的实现类是比较多的,但是实现比较简单。

这里以IntergerTypeHandler为例简单介绍:

package org.apache.ibatis.type;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * @author Clinton Begin
 */
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获取指定列值
    int result = rs.getInt(columnName);
    return result == 0 && rs.wasNull() ? null : result;
  }

  @Override
  public Integer getNullableResult(ResultSet rs, int columnIndex)
      throws SQLException {
    //调用ResultSet.getInt获取指定列值
    int result = rs.getInt(columnIndex);
    return result == 0 && rs.wasNull() ? null : result;
  }

  @Override
  public Integer getNullableResult(CallableStatement cs, int columnIndex)
    //调用ResultSet.getInt获取指定列值
      throws SQLException {
    int result = cs.getInt(columnIndex);
    return result == 0 && cs.wasNull() ? null : result;
  }
}

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

3. TypeHandlerRegistry

​ 介绍完TypeHandler接口及其功能之后,Mybatis如何管理众多的TypeHandler接口实现,如何知道何时使用哪个TypeHandler接口实现完成转换呢?这是有本小节介绍的TypeHandlerRegistry完成的,在Mybatis初始化过程中,会为所有已知的TypeHandler创建对象,并实现注册到TypeHandlerRegistry中,有TypeHandlerRegistry负责管理这些TypeHandler对象。
​ 下面先来看看TypeHandlerRegistry中的核心字段的含义:

  /**
   * 记录jdbcType与TypeHandler之间的对应关系,其中JdbcType是一个枚举类型,它定义对应了的JDBC类型
   * 该集合主要用于从结果集读取数据是,将数据从jdbc类型转换成Java类型
   */
  private final Map<JdbcType, TypeHandler<?>>  jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);
  /**
   * 记录了Java类型向指定的jdbcType转换时,需要使用的TypeHandler对象。
   * 例如:Java类型中的String 可能转换为数据库的char、varchar等多种类型,所以存在一对多关系
   */
  private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
  /**
   * 未知类型TypeHandler
   */
  private final TypeHandler<Object> unknownTypeHandler;
  /**
   * 记录了全部的TypeHandler的类型以及该类型相应的TypeHandler对象
   */
  private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();

  /**
   * 空TypeHandler集合的标识
   */
  private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();

  /**
   * 默认枚举类型处理器
   */
  private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;

3.1. TypeHandlerRegistry构造方法

TypeHandlerRegistry构造中综合起来做了三步操作:

  1. 创建了一个Configuration,或者通过mybatis-config.xml的初始化传入一个ConfigurationTypeHandlerRegistry构造中;
  2. 利用传入的configuration对象,创建一个UnknownTpyeHandler,以备后续使用;
  3. 注册一堆Mybatis为我们提供的默认的TypeHandler

3.2. 注册TypeHandler对象

TypeHandlerRegistry.register()方法实现了注册TypeHandler对象的功能,register()方法有多个重载,这些重载之间的调用关系如下图所示。

由上图可以看出,多数的register()方法最终会调用重载7完成注册功能,所以先分析重载7,该方法有三个参数分别是:

  • Type javaType
  • JdbcType jdbcType
  • TypeHandler<T> handler
 /**
   * 最终调用到的重载方法
   * @param javaType  能够处理的Java类型
   * @param jdbcType
   * @param handler
   */
  // 7
  private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    //检测是否明确指定了TypeHandler能够处理的Java类型
    if (javaType != null) {
      //获取指定Java类型在typeHandlerMap集合中对应的TypeHandler集合
      Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
      //如果map为空或者等于NULL_TYPE_HANDLER_MAP,创建新的TypeHandler集合,并添加到typeHandlerMap中
      if (map == null || map == NULL_TYPE_HANDLER_MAP) {
        map = new HashMap<>();
        typeHandlerMap.put(javaType, map);
      }
      //将TypeHandler对象注册到typeHandlerMap集合中
      map.put(jdbcType, handler);
    }
    //向allTypeHandlersMap集合注册TypeHandler类型和对应的TypeHandler对象
    allTypeHandlersMap.put(handler.getClass(), handler);
  }

3.3. 查找TypeHandler

介绍完注册TypeHandler对象的功能之后,再来介绍TypeHandlerRegistry提供的查找TypeHandler对象的功能。

4. TypeAliasRegistry

在编写SQL语句时,使用别名可以方便理解以及维护,例如表名或列名很长时,我们一般会为其设计易懂易维护的别名。MybatisSQL语句中的别名的概念进行了延伸和扩展,Mybatis可以为一个类添加一个别名,之后就可以通过别名引用该类。

Mybati通过TypeAliasRegistry类完成别名的注册和管理功能,TypeAliasRegistry的结构比较简单,它通过typeAliases字段(Map<String, Class<?>>类型)管理别名与Java类型之间的对应关系,通过TypeAliasRegistry.registerAlias()方法完成注册别名,该方法的实现如下所示:

  /**
   * 注册类型别名
   * @param alias 被注册类的别名
   * @param value  被注册的类
   */
  public void registerAlias(String alias, Class<?> value) {
    if (alias == null) {
      throw new TypeException("The parameter alias cannot be null");
    }
    // issue #748
    //所有的类型最终都被转换为了小写,所以之前不管你是大写还是小写,最后都不转化了,不要因为大小写是不一样的。
    String key = alias.toLowerCase(Locale.ENGLISH);
    if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
      throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
    }
    typeAliases.put(key, value);
  }

TypeAliasRegistry还有很多registerAlias()的重载,如下图所示:

其中还有两个重载需要我们注意:

  • 扫描包的

      /**
       * 扫描指定包下面所有的类,并为其类的子类添加别名
       * @param packageName
       * @param superType
       */
      public void registerAliases(String packageName, Class<?> superType) {
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
        resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
        Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
        for (Class<?> type : typeSet) {
          // Ignore inner classes and interfaces (including package-info.java)
          // Skip also inner classes. See issue #6
          if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
            registerAlias(type);
          }
        }
      }
  • 尝试读取@Alias注解的

      public void registerAlias(Class<?> type) {
        //获取类的简单名称,不包括包名
        String alias = type.getSimpleName();
        //读取Alias注解
        Alias aliasAnnotation = type.getAnnotation(Alias.class);
        if (aliasAnnotation != null) {
          alias = aliasAnnotation.value();
        }
        registerAlias(alias, type);
      }

TypeAliasRegistry的构造方法中,默认为Java的基本类型及其数组类型、基本类型的包装类型以及数组类型、DataBigDecimalResultSet等类型添加了别名,可以参考Mybatis官网文档(typeAliases模块)。



Mybatis源码分析   基础支持层      Mybatis源码分析

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!