编者注:本文为历史博文归档;涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效,阅读时请注意时效性。

ORM 框架的核心功能在于建立面向对象模型与关系型数据库表之间的映射关系。在这一关联过程中,必然涉及对象中的数据类型与数据库表字段类型之间的转换。MyBatis 中的 org.apache.ibatis.type 包主要便是为了实现这一功能。

1. org.apache.ibatis.type 基础类

在 MyBatis 官网(http://mybatis.github.io/mybatis-3/configuration.html#typeHandlers)关于类型转换有如下的描述:

Whenever MyBatis sets a parameter on a PreparedStatement or retrieves a value from a ResultSet, a TypeHandler is used to retrieve the value in a means appropriate to the Java type.

当 MyBatis 为 PreparedStatement 设置参数时或者从 ResultSet 中获取数据时,会根据 Java 类型使用 TypeHandler 去获取相应的值。

官网中也列出了每一个 TypeHandler 用来处理对应的 JDBC 类型和 Java 类型。

1.1 TypeHandler 接口

这个接口主要有三个方法:

  • 一个 set 方法:用来给 PreparedStatement 对象对应的列设置参数。
  • 两个 get 方法:从 ResultSetCallableStatement 获取对应列的值。不同之处在于一个是取第几个位置的值,一个是取具体列名所对应的值。

set 用来将 Java 对象中的数据类型转换为 JDBC 中对应的数据类型,get 用来将 JDBC 中对应的数据类型转换为 Java 对象中的数据类型。

 title=

1.2 BaseTypeHandler 抽象类

在进行软件设计时提倡面向接口的设计,但接口只是一个规范,并不做任何实质性的操作,还需有一系列的实现才可以真正的达到目标。BaseTypeHandler 类便是对 TypeHandler 接口的初步实现。在实现 TypeHandler 接口的三个函数外,又引入了 3 个抽象函数用于 null 值的处理。

 title=

1.3 DateTypeHandler 示例

承接上文,BaseTypeHandler 类也是一个抽象类,按照 Java 的规定抽象类并不能初始化,也不能直接使用,因而还需要有具体的实现类。在 type 包中有十多个具体的类来具体处理类型转换,每一个类处理一个数据类型,像 longintdouble 等等。我们以一个稍微复杂些的 DateTypeHandler 类为例,了解下对日期是如何进行处理的。

1.3.1 setNonNullParameter

public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setTimestamp(i, new java.sql.Timestamp(((Date) parameter).getTime()));
}

首先将参数 parameter 这个 Object 转换为 Date 类型,而后通过 Date 对象的 getTime() 将日期转为毫秒数,而后再将毫秒数转换为 java.sql.Timestamp 对象。即将 java.util.Date 对象转换为 java.sql.Timestamp 对象。

1.3.2 getNullableResult

public Object getNullableResult(ResultSet rs, String columnName)
      throws SQLException {
    java.sql.Timestamp sqlTimestamp = rs.getTimestamp(columnName);
    if (sqlTimestamp != null) {
      return new java.util.Date(sqlTimestamp.getTime());
    }
    return null;
}

public Object getNullableResult(CallableStatement cs, int columnIndex)
      throws SQLException {
    java.sql.Timestamp sqlTimestamp = cs.getTimestamp(columnIndex);
    if (sqlTimestamp != null) {
      return new java.util.Date(sqlTimestamp.getTime());
    }
    return null;
}

从上面的代码可以看出,这两个函数的作用就是将 java.sql.Timestamp 对象转换为 java.util.Date 对象。

1.4 类图

综合而言,type 包基础类的类图示例如下:

 title=

2. 自定义类型处理或覆盖默认处理

在 MyBatis 官网中(http://mybatis.github.io/mybatis-3/configuration.html#typeHandlers)关于有如下的描述:

You can override the type handlers or create your own to deal with unsupported or non-standard types. To do so, simply extend the org.apache.ibatis.type.BaseTypeHandler class and optionally map your new TypeHandler class to a JDBC type.

可以覆盖 TypeHandler 或者创建自己的 TypeHandler 去处理 MyBatis 不支持的或者非标准的数据类型。实现这个功能,只需要继承 org.apache.ibatis.type.BaseTypeHandler 类然后选择新的 TypeHandler 类和 JDBC 类型的对应关系即可。

在官网中也给出了详细了示例,这里不再进行重复。其中用到了两个注解:MappedJdbcTypesMappedTypes,我们可以看下这两个注解的定义文件:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MappedJdbcTypes {
    public JdbcType[] value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MappedTypes {
    public Class[] value();
}

MappedJdbcTypes 传入的是 JdbcType 类型的(JdbcTypetype 包中的一个枚举类型),MappedTypes 传入的是 Class 类型。

在声明完自己的类型转换之后,还需要让 MyBatis 知道这些新的类型转换类,这可以通过在配置文件中添加 typeHandlers 节点来实现。可以添加一个类,也可以添加一个包中所有的类。

<typeHandlers>
  <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
  <package name="org.mybatis.example"/>
</typeHandlers>

3. TypeHandler 的注册:TypeHandlerRegistry

前面介绍了 MyBatis 中已定义好的 TypeHandler,也介绍了如何自定义 TypeHandler,以及如何让 MyBatis 知道这次自定义的 TypeHandler。现在剩下最关键的步骤:在 MyBatis 初始化后如何将这些 TypeHandler 注册到 MyBatis 中,以便在执行数据库操作去使用这些类。这些操作是由 TypeHandlerRegistry 类实施的。

我们已经知道,MyBatis 在使用时需要一个配置文件来进行各种各样的设置,与这个配置文件相对应的是 org.apache.ibatis.session.Configuration 这个类,配置文件中每一项都对应 Configuration 类中的一个属性,typeHandler 就是 Configuration 类中的一个属性。

protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();

我们现在来看看 TypeHandlerRegistry 类中常用的方法及其作用。

3.1 TypeHandlerRegistry 类中的属性和常用方法

private static final Map<Class<?>, Class<?>> reversePrimitiveMap = new HashMap<Class<?>, Class<?>>() {
    {
      put(Byte.class, byte.class);
      put(Short.class, short.class);
      put(Integer.class, int.class);
      put(Long.class, long.class);
      put(Float.class, float.class);
      put(Double.class, double.class);
      put(Boolean.class, boolean.class);
    }
};

private final Map<JdbcType, TypeHandler> JDBC_TYPE_HANDLER_MAP = new EnumMap<JdbcType, TypeHandler>(JdbcType.class);
private final Map<Class<?>, Map<JdbcType, TypeHandler>> TYPE_HANDLER_MAP = new HashMap<Class<?>, Map<JdbcType, TypeHandler>>();
private final TypeHandler UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this);

3.1.1 reversePrimitiveMap

可以看出,这个 Map 就是将 Java 中的基本数据类型和他们对应的包装类一一关联起来,像 Bytebyte。在进行注册时会用到这个属性,详见第三小节。

3.1.2 JDBC_TYPE_HANDLER_MAP

JdbcTypeTypeHandler 的对应关系,通过如下的函数进行维护:

public void register(JdbcType jdbcType, TypeHandler handler) {
    JDBC_TYPE_HANDLER_MAP.put(jdbcType, handler);
}

3.1.3 TYPE_HANDLER_MAP

TYPE_HANDLER_MAP 属性是一个关键的属性,Java 类型和 JdbcType 的对应关系及处理类都存在这个属性中。从这个 Map 的定义中可以看到多个 JDBC 类型能够对应到一个 Java 类型。通过如下的函数进行维护:

public void register(Class<?> type, JdbcType jdbcType, TypeHandler handler) {
    // 先查看这个 Java 类型是否已经绑定过了,如果没有绑定过,创建了一个 Map,否则就直接添加新的
    Map<JdbcType, TypeHandler> map = TYPE_HANDLER_MAP.get(type);
    if (map == null) {
      map = new HashMap<JdbcType, TypeHandler>();
      TYPE_HANDLER_MAP.put(type, map);
    }
    map.put(jdbcType, handler);
    // 如果当前添加的是属于 Byte、Long 等类型,将其对应的基本类型也进行注册
    if (reversePrimitiveMap.containsKey(type)) {
      register(reversePrimitiveMap.get(type), jdbcType, handler);
    }
}

3.1.4 注册自定义 TypeHandler

在注册自定义的 TypeHandler 之前需要先定位到具体的类或者包。类的处理比较简单,直接利用 Java 的反射机制就可以知道这个类的 Class 属性了。对于包就稍微复杂些,在 MyBatis 中是利用 ioResolverUtil 类中的 find 函数来实现的,这里不做详细介绍,等介绍到 MyBatis 的 io 包时再详细说明。我们来看注册包的函数:

public void register(String packageName) {
    // 先声明一个 ResolverUtil 对象
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    // 利用 find 函数找到这个包里所有的类,并且放到 resolverUtil 的 matches 属性中
    resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
    Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
    // 通过 for 循环依次将加载
    for (Class<?> type : handlerSet) {
      // Ignore inner classes and interfaces (including package-info.java) and abstract classes 
      // 不处理内部类、接口、抽象类以及 package-info 类     
      if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
        try {
          // 利用反射机制创建一个 TypeHandler
          TypeHandler handler = (TypeHandler) type.getConstructor().newInstance();
          // 注册
          register(handler);
        } catch (Exception e) {
          throw new RuntimeException("Unable to find a usable constructor for " + type, e);
        }
      }
    }
}

从上面的代码中可以看到,真正进行注册用到的是如下的函数,先处理 MappedTypes 这个注解:

public void register(TypeHandler handler) {
    boolean mappedTypeFound = false;
    // 判断这个类是否有 MappedTypes 这个注解
    MappedTypes mappedTypes = (MappedTypes) handler.getClass().getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
      for (Class<?> handledType : mappedTypes.value()) {
          // 进行注册并设置 mappedTypeFound 变量为 true  
          register(handledType, handler);
          mappedTypeFound = true;
      }
    }
    // 如果 mappedTypeFound 为 false,则抛出一个异常
    // 注意,这里和官网上的示例不同,官网上的示例并没有这个注解
    // 但是在源代码中如果没有这个注解则不会继续下去
    // 因而如果是要自定义类型转换,还是需要添加这个注解
    if (!mappedTypeFound) {
      throw new RuntimeException("Unable to get mapped types, check @MappedTypes annotation for type handler " + handler);
    }
}

上面的函数又调用了如下的函数,去处理 MappedJdbcTypes 注解:

public void register(Class<?> type, TypeHandler handler) {
    MappedJdbcTypes mappedJdbcTypes = (MappedJdbcTypes) handler.getClass().getAnnotation(MappedJdbcTypes.class);
    // 对注解进行判断,如果有 MappedJdbcTypes 这个注解,则对其对应的 JdbcType 依次进行注册,否则注册为 null
     if (mappedJdbcTypes != null) {
      for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
        register(type, handledJdbcType, handler);
      }
    } else {
      register(type, null, handler);
    }
}

最后调用第三小节中提到的函数进行注册到 Map 中,供程序执行时调用。

4. TypeHandlerRegistry 的初始化

前面介绍了如何自定义类型转换处理类并注册到 MyBatis 中,那 type 包中有不少 MyBatis 已实现的类型转换处理类,这些类是如何及在何时注册到 MyBatis 中的呢?

MyBatis 中的实现很简单,它把这些都放在了 TypeHandlerRegistry 的构造函数中了。由于构造函数比较大,也没有用到什么新方法,在此就不贴代码了,有兴趣的读者可以自己去观看。

5. 小结

本文对 MyBatis 中的 Type 转换做了介绍,介绍了其设计结构、常用的方法、如何自定义类型处理类、如何进行注册等内容,希望对大家理解 MyBatis 如何实现 Java 类型到 JDBC 类型转换有所帮助。

当然,这里只是介绍了如何转换,但是这些转换是怎么使用的并没有涉及,这些内容将放到 MyBatis 的 Executor 中进行介绍。


说明:本文基于 MyBatis 早期版本源代码分析(参考图片链接显示时间为 2013 年),部分实现细节(如注解强制要求)可能随版本迭代有所变化,请以当前官方文档及最新源码为准。