Skip to the content.

2-2 类型处理器源码分析

与类型别名注册表类似,类型处理器注册表也是由BaseBuilder管理的。同样一些其余的XXXBuilder需要使用类型处理器注册表,因此,BaseBuilder负责维护该属性。

接下来首先介绍一下类型处理器的功能。

2-2.1 类型处理器的功能

MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。

上面的解释摘自Mybatis文档,如果你对JDBC有一定使用经验的话,应该对这句话有比较深的理解,这里笔者使用JDBC构架另一个Demo,使用的库仍然是第一节创建的库,只不过本次不再使用Mybatis操作数据库,而是使用原生JDBC。

这次完成的功能仅仅是通过id对数据进行查询:

public class JdbcExample {

    public static void main(String[] args) {
        try {
            // 驱动加载
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            System.exit(-1);
        }
        String url = "jdbc:mysql://127.0.0.1:3306/mybatis";
        String username = "root";
        String password = "123456";
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            // 创建连接
            connection = DriverManager.getConnection(url, username, password);
            // 准备PrepareStatement
            preparedStatement = connection.prepareStatement("select id,title,content from blog where id = ?");
            // 为PrepareStatement拼接参数
            preparedStatement.setInt(1, 4);
            // 执行SQL
            resultSet = preparedStatement.executeQuery();
            while (resultSet.next()) {
                // 读取结果
                System.out.println(resultSet.getString(1));
                System.out.println(resultSet.getString(2));
                System.out.println(resultSet.getString(3));
            }
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            // 清理资源
            ...
        }
    }
}

为了减少代码行数,笔者省略了一些清除资源用的代码。通常情况下,使用JDBC进行数据库操作,我们需要执行如下几步:

  1. 驱动加载
  2. 创建连接
  3. 准备PrepareStatement
  4. 设置查询参数
  5. 读取数据
  6. 清理资源

这里我们主要关注第3-5步。如果使用PrepareStatement操作数据库时,为了防止出现SQL注入,我们通常都会将语句写成这种形式:

select id,title,content from blog where id = ?

其中参数通过PrepareStatement的各个set方法设置到对应的位置,例如本例中的:

preparedStatement.setInt(1, 4);

这意味着将preparedStatement中第一个位置的参数按照Int型的方式设置为4。

等到查询出数据后,JDBC会通过ResultSet接到数据,然后可以通过其提供的get方法获取数据,例如:

System.out.println(resultSet.getString(1));

上面代码的意思是,使用获取String的方式读取resultSet中第一列的数据。

在这之后我们考察一下TypeHandler这个接口,即Mybatis的类型处理器:

public interface TypeHandler<T> {

  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  T getResult(ResultSet rs, String columnName) throws SQLException;

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

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

}

可以看到,这里提供了两类方法:

  1. 通过列索引(i)为PreparedStatement设置参数
  2. 通过列索引(columnIndex)读取ResultSet中的结果集

其实TypeHandler就是对PreparedStatement的简单封装,可以让JDBC_TYPE映射到JavaType更加规范化,而不用自己手写了罢了。

不过需要注意,TypeHandler仅仅是将每行的单项数据转化为Java类型,而不是将整行数据进行转换。

你可以通过按照如下步骤创建一个新的TypeHandler(这里引用Mybatis文档中的例子),自定义Java类型与JDBC类型的对应关系。

  1. 实现TypeHandler接口 或者 继承BaseTypeHandler
     // ExampleTypeHandler.java
     @MappedJdbcTypes(JdbcType.VARCHAR)
     public class ExampleTypeHandler extends BaseTypeHandler<String> {
    
         @Override
         public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
             ps.setString(i, parameter);
         }
    
         @Override
         public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
             return rs.getString(columnName);
         }
    
         @Override
         public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
             return rs.getString(columnIndex);
         }
    
         @Override
         public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
             return cs.getString(columnIndex);
         }
     }
    
  2. 将TypeHandler配置到Mybatis的配置文件中。
     <!-- mybatis-config.xml -->
     <typeHandlers>
         <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
     </typeHandlers>
    

使用上述的类型处理器将会覆盖已有的处理 Java String 类型的属性以及 VARCHAR 类型的参数和结果的类型处理器。 要注意 MyBatis 不会通过检测数据库元信息来决定使用哪种类型,所以你必须在参数和结果映射中指明字段是 VARCHAR 类型, 以使其能够绑定到正确的类型处理器上。这是因为 MyBatis 直到语句被执行时才清楚数据类型。

通过类型处理器的泛型,MyBatis 可以得知该类型处理器处理的 Java 类型,不过这种行为可以通过两种方法改变:

可以通过两种方式来指定关联的 JDBC 类型:

当在 ResultMap 中决定使用哪种类型处理器时,此时 Java 类型是已知的(从结果类型中获得),但是 JDBC 类型是未知的。 因此 Mybatis 使用 javaType=[Java 类型], jdbcType=null 的组合来选择一个类型处理器。 这意味着使用 @MappedJdbcTypes 注解可以限制类型处理器的作用范围,并且可以确保,除非显式地设置,否则类型处理器在 ResultMap 中将不会生效。 如果希望能在 ResultMap 中隐式地使用类型处理器,那么设置 @MappedJdbcTypes 注解的 includeNullJdbcType=true 即可。 然而从 Mybatis 3.4.0 开始,如果某个 Java 类型只有一个注册的类型处理器,即使没有设置 includeNullJdbcType=true,那么这个类型处理器也会是 ResultMap 使用 Java 类型时的默认处理器。

最后,可以让 MyBatis 帮你查找类型处理器:

<!-- mybatis-config.xml -->
<typeHandlers>
  <package name="org.mybatis.example"/>
</typeHandlers>

注意在使用自动发现功能的时候,只能通过注解方式来指定 JDBC 的类型。

通过上面的描述可以知道,TypeHandler提供了一个三元组<JavaType,JdbcType,TypeHandler>其中,前两者中的任意一个可以为null。

2-2.2 类型处理器源码解析

对于类型解析器的配置解析在XmlConfigBuildertypeHandlerElement(XNode parent)方法中,代码如下:

private void typeHandlerElement(XNode parent) {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        // 处理包扫描类型解析器逻辑
        String typeHandlerPackage = child.getStringAttribute("name");
        typeHandlerRegistry.register(typeHandlerPackage);
      } else {
        // 处理通过typeHandler标签设置类型解析器的逻辑
        String javaTypeName = child.getStringAttribute("javaType");
        String jdbcTypeName = child.getStringAttribute("jdbcType");
        String handlerTypeName = child.getStringAttribute("handler");
        Class<?> javaTypeClass = resolveClass(javaTypeName);
        JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
        Class<?> typeHandlerClass = resolveClass(handlerTypeName);
        if (javaTypeClass != null) {
          if (jdbcType == null) {
            typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
          } else {
            typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
          }
        } else {
          typeHandlerRegistry.register(typeHandlerClass);
        }
      }
    }
  }
}

可以看到,typeHandler的解析逻辑与typeAlias相同,也分为两种,分别是扫描整包和解析单个TypeHandler。前文笔者已经提过,typeHandler的配置是一个三元组:<JavaType,JdbcType,TypeHandler>,所以,解析配置也就是将配置文件转化为这三元组的集合,然后存储到TypeHandlerRegistry中。

TypeHandlerRegistry中,注册单个TypeHandler的方法有如下几种:

负责扫描整个包的方法是register(String packageName)

与分析TypeAliasRegistry一样,我们先对注册单个TypeHandler的方法进行分析。在分析具体的方法之前,我们首先对TypeHandlerRegistry的属性进行分析:

public final class TypeHandlerRegistry {
  /**
   * 存储JdbcType与TypeHandler的关系
   */
  private final Map<JdbcType, TypeHandler<?>>  jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);
  /**
   * 存储Java类型与JDBC类型以及TypeHandler的关系
   */
  private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
  /**
   * 未知类型处理器
   */
  private final TypeHandler<Object> unknownTypeHandler;
  /**
   * 类型处理器汇总,通过类型处理器Class对象获取类型处理器
   */
  private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();
  /**
   * 空表
   */
  private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();
  /**
   * 枚举类型处理器
   */
  private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
}

TypeHandlerRegistry主要提供了4种配置:

  1. 未知类型的类型处理器
  2. 枚举类型的类型处理器
  3. Jdbc类型对应的类型处理器
  4. Java类型与Jdbc类型之间的关系

首先查看register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass)方法,该方法提供了三元组中的全部信息,是所有方法的根方法:

  public <T> void register(Class<T> type, JdbcType jdbcType, TypeHandler<? extends T> handler) {
    register((Type) type, jdbcType, handler);
  }

  private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    if (javaType != null) {
      // 注册JdbcType与TypeHandler的关系
      Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
      if (map == null || map == NULL_TYPE_HANDLER_MAP) {
        map = new HashMap<>();
      }
      map.put(jdbcType, handler);
      // 注册Java类型与JDBCType的关系
      typeHandlerMap.put(javaType, map);
    }
    // 统计所有类型处理器
    allTypeHandlersMap.put(handler.getClass(), handler);
  }

可以看到,注册过程大致如下:

  1. 构建JdbcType与TypeHandler的关系
  2. 构建Java类型与JdbcType的关系
  3. 构建TypeHandler的Class对象与TypeHandler的关系

如果是Java类型直接注册TypeHandler,处理方法是register(Class<?> javaTypeClass, Class<?> typeHandlerClass):

  public <T> void register(Class<T> javaType, TypeHandler<? extends T> typeHandler) {
    register((Type) javaType, typeHandler);
  }

  private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
    // 获取对应的JdbcType
    MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
    if (mappedJdbcTypes != null) {
      for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
        // JdbcType不为空,调用上述接口注册
        register(javaType, handledJdbcType, typeHandler);
      }
      if (mappedJdbcTypes.includeNullJdbcType()) {
        // jdbcType是空,那么,直接将JdbcType设置为null,然后注册
        register(javaType, null, typeHandler);
      }
    } else {
      register(javaType, null, typeHandler);
    }
  }