05 数据库类型体系与 Java 类型体系之间的“爱恨情仇” 作为一个 Java 程序员,你应该已经具备了使用 JDBC 操作数据库的基础技能。在使用 JDBC 的时候,你会发现 JDBC 的数据类型与 Java 语言中的数据类型虽然有点对应关系,如下图所示,但还是无法做到一一对应,也自然无法做到自动映射。
数据库类型与 Java 类型对应图表
在使用 PreparedStatement 执行 SQL 语句之前,都是需要手动调用 setInt()、setString() 等 set 方法绑定参数,这不仅仅是告诉 JDBC 一个 SQL 模板中哪个占位符需要使用哪个实参,还会将数据从 Java 类型转换成 JDBC 类型。当从 ResultSet 中获取数据的时候,则是一个逆过程,数据会从 JDBC 类型转换为 Java 类型。
可以使用 MyBatis 中的类型转换器,完成上述两次类型转换,如下图所示:
JDBC 类型数据与 Java 类型数据转换示意图
深入 TypeHandler
说了这么多,类型转换器到底是怎么定义的呢?其实,MyBatis 中的类型转换器就是 TypeHandler 这个接口,其定义如下:
public interface TypeHandler
MyBatis 中定义了 BaseTypeHandler 抽象类来实现一些 TypeHandler 的公共逻辑,BaseTypeHandler 在实现 TypeHandler 的同时,还实现了 TypeReference 抽象类。其继承关系如下图所示:
TypeHandler 继承关系图
在 BaseTypeHandler 中,简单实现了 TypeHandler 接口的 setParameter() 方法和 getResult() 方法。
- 在 setParameter() 实现中,会判断传入的 parameter 实参是否为空,如果为空,则调用 PreparedStatement.setNull() 方法进行设置;如果不为空,则委托 setNonNullParameter() 这个抽象方法进行处理,setNonNullParameter() 方法由 BaseTypeHandler 的子类提供具体实现。
- 在 getResult() 的三个重载实现中,会直接调用相应的 getNullableResult() 抽象方法,这里有三个重载的 getNullableResult() 抽象方法,它们都由 BaseTypeHandler 的子类提供具体实现。
BaseTypeHandler 的具体实现比较简单,这里就不再展示,你若感兴趣的话可以参考源码进行学习。
下图展示了 BaseTypeHandler 的全部实现类,虽然实现类比较多,但是它们的实现方式大同小异。
BaseTypeHandler 实现类示意图
这里我们以 LongTypeHandler 为例进行分析,具体实现如下:
public class LongTypeHandler extends BaseTypeHandler
可以看到:LongTypeHandler 的核心还是通过 PreparedStatement.setLong() 方法以及 ResultSet.getLong() 方法实现的。至于其他 BaseTypeHandler 的核心实现,同样也是依赖了 JDBC 的 API,这里就不再展开介绍了。
TypeHandler 注册
了解了 TypeHandler 接口实现类的核心原理之后,我们就来解决下面两个问题:
- MyBatis 如何管理这么多的 TypeHandler 接口实现呢?
- 如何在合适的场景中使用合适的 TypeHandler 实现进行类型转换呢?
你若使用过 MyBatis 的话,应该知道我们可以在 mybatis-config.xml 中通过 标签配置自定义的 TypeHandler 实现,也可以在 Mapper.xml 配置文件定义 的时候指定 typeHandler 属性。无论是哪种配置方式,MyBatis 都会在初始化过程中,获取所有已知的 TypeHandler(包括内置实现和自定义实现),然后创建所有 TypeHandler 实例并注册到 TypeHandlerRegistry 中,由 TypeHandlerRegistry 统一管理所有 TypeHandler 实例。 TypeHandlerRegistry 管理 TypeHandler 的时候,用到了以下四个最核心的集合。
- jdbcTypeHandlerMap(Map>类型):该集合记录了 JdbcType 与 TypeHandler 之间的关联关系。JdbcType 是一个枚举类型,每个 JdbcType 枚举值对应一种 JDBC 类型,例如,JdbcType.VARCHAR 对应的就是 JDBC 中的 varchar 类型。在从 ResultSet 中读取数据的时候,就会从 JDBC_TYPE_HANDLER_MAP 集合中根据 JDBC 类型查找对应的 TypeHandler,将数据转换成 Java 类型。
- typeHandlerMap(Map»类型):该集合第一层 Key 是需要转换的 Java 类型,第二层 Key 是转换的目标 JdbcType,最终的 Value 是完成此次转换时所需要使用的 TypeHandler 对象。那为什么要有两层 Map 的设计呢?这里我们举个例子:Java 类型中的 String 可能转换成数据库中的 varchar、char、text 等多种类型,存在一对多关系,所以就可能有不同的 TypeHandler 实现。
- allTypeHandlersMap(Map类型):该集合记录了全部 TypeHandler 的类型以及对应的 TypeHandler 实例对象。
- NULL_TYPE_HANDLER_MAP(Map>类型):空 TypeHandler 集合的标识,默认值为 Collections.emptyMap()。
在 MyBatis 初始化的时候,实例化全部 TypeHandler 对象之后,会立即调用 TypeHandlerRegistry 的 register() 方法完成这些 TypeHandler 对象的注册,这个注册过程的核心逻辑就是向上述四个核心集合中添加 TypeHandler 实例以及与 Java 类型、JDBC 类型之间的映射。
TypeHandlerRegistry.register() 方法有多个重载实现,这些重载中最基础的实现是三个参数的重载实现,具体实现如下: private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) { if (javaType != null) { // 检测是否明确指定了TypeHandler能够处理的Java类型 // 根据指定的Java类型,从typeHandlerMap集合中获取相应的TypeHandler集合 Map<JdbcType, TypeHandler<?» map = typeHandlerMap.get(javaType); if (map == null || map == NULL_TYPE_HANDLER_MAP) { map = new HashMap<>(); } // 将TypeHandler实例记录到typeHandlerMap集合 map.put(jdbcType, handler); typeHandlerMap.put(javaType, map); } // 向allTypeHandlersMap集合注册TypeHandler类型和对应的TypeHandler对象 allTypeHandlersMap.put(handler.getClass(), handler); }
除了上面的 register() 重载,在有的 register() 重载中会尝试从 TypeHandler 类中的@MappedTypes 注解和 @MappedJdbcTypes 注解中读取信息。其中,@MappedTypes 注解中可以配置 TypeHandler 实现类能够处理的 Java 类型的集合,@MappedJdbcTypes 注解中可以配置该 TypeHandler 实现类能够处理的 JDBC 类型集合。
如下就是读取 @MappedJdbcTypes 注解的 register() 重载方法:
private
下面是读取 @MappedTypes 注解的 register() 方法重载:
public
我们接下来看最后一个 register() 重载。TypeHandlerRegistry 提供了扫描一个包下的全部 TypeHandler 接口实现类的 register() 重载。在该重载中,会首先读取指定包下面的全部的 TypeHandler 实现类,然后再交给 register() 重载读取 @MappedTypes 注解和 @MappedJdbcTypes 注解,并最终完成注册。这个 register() 重载的具体实现比较简单,这里就不再展示,你若感兴趣的话可以参考源码进行学习。
最后,我们再来看看 TypeHandlerRegistry 的构造方法,其中会通过 register() 方法注册多个 TypeHandler 对象,下面就展示了为 String 类型注册 TypeHandler 的核心实现: public TypeHandlerRegistry() { // StringTypeHandler可以实现String类型与char、varchar、longvarchar类型之间的转换 register(String.class, JdbcType.CHAR, new StringTypeHandler()); register(String.class, JdbcType.VARCHAR, new StringTypeHandler()); register(String.class, JdbcType.LONGVARCHAR, new StringTypeHandler()); // ClobTypeHandler可以完成String类型与clob类型之间的转换 register(String.class, JdbcType.CLOB, new ClobTypeHandler()); // NStringTypeHandler可以完成String类型与NVARCHAR、NCHAR类型之间的转换 register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler()); register(String.class, JdbcType.NCHAR, new NStringTypeHandler()); // NClobTypeHandler可以完成String类型与NCLOB类型之间的转换 register(String.class, JdbcType.NCLOB, new NClobTypeHandler()); // 省略其他TypeHandler实现的注册逻辑 }
TypeHandler 查询
分析完注册 TypeHandler 实例的具体实现之后,我们接下来就来看看 MyBatis 是如何从 TypeHandlerRegistry 底层的这几个集合中查找正确的 TypeHandler 实例,该功能的具体实现是在 TypeHandlerRegistry 的 getTypeHandler() 方法中。
这里的 getTypeHandler() 方法也有多个重载,最核心的重载是 getTypeHandler(Type,JdbcType) 这个重载方法,其中会根据传入的 Java 类型和 JDBC 类型,从底层的几个集合中查询相应的 TypeHandler 实例,具体实现如下:
private
在 getTypeHandler() 方法中会调用 getJdbcHandlerMap() 方法检测 typeHandlerMap 集合中相应的 TypeHandler 集合是否已经初始化。
- 如果已初始化,则直接使用该集合进行查询;
- 如果未初始化,则尝试以传入的 Java 类型的、已初始化的父类对应的 TypeHandler 集合作为初始集合;
- 如果该 Java 类型的父类没有关联任何已初始化的 TypeHandler 集合,则将该 Java 类型对应的 TypeHandler 集合初始化为 NULL_TYPE_HANDLER_MAP 标识。
getJdbcHandlerMap() 方法具体实现如下: private Map<JdbcType, TypeHandler<?» getJdbcHandlerMap(Type type) { // 首先查找指定Java类型对应的TypeHandler集合 Map<JdbcType, TypeHandler<?» jdbcHandlerMap = typeHandlerMap.get(type); if (NULL_TYPE_HANDLER_MAP.equals(jdbcHandlerMap)) { // 检测是否为空集合标识 return null; } // 初始化指定Java类型的TypeHandler集合 if (jdbcHandlerMap == null && type instanceof Class) { Class<?> clazz = (Class<?>) type; if (Enum.class.isAssignableFrom(clazz)) { // 针对枚举类型的处理 Class<?> enumClass = clazz.isAnonymousClass() ? clazz.getSuperclass() : clazz; jdbcHandlerMap = getJdbcHandlerMapForEnumInterfaces(enumClass, enumClass); if (jdbcHandlerMap == null) { register(enumClass, getInstance(enumClass, defaultEnumTypeHandler)); return typeHandlerMap.get(enumClass); } } else { // 查找父类关联的TypeHandler集合,并将其作为clazz对应的TypeHandler集合 jdbcHandlerMap = getJdbcHandlerMapForSuperclass(clazz); } } // 如果上述查找皆失败,则以NULL_TYPE_HANDLER_MAP作为clazz对应的TypeHandler集合 typeHandlerMap.put(type, jdbcHandlerMap == null ? NULL_TYPE_HANDLER_MAP : jdbcHandlerMap); return jdbcHandlerMap; }
这里调用的 getJdbcHandlerMapForSuperclass() 方法会判断传入的 clazz 的父类是否为空或 Object。如果是,则方法直接返回 null;如果不是,则尝试从 typeHandlerMap 集合中获取父类对应的 TypeHandler 集合,但如果父类没有关联 TypeHandler 集合,则递归调用 getJdbcHandlerMapForSuperclass() 方法顺着继承树继续向上查找父类,直到查找到父类的 TypeHandler 集合,然后直接返回。
下面是 getJdbcHandlerMapForSuperclass() 方法的具体实现: private Map<JdbcType, TypeHandler<?» getJdbcHandlerMapForSuperclass(Class<?> clazz) { Class<?> superclass = clazz.getSuperclass(); if (superclass == null || Object.class.equals(superclass)) { return null; // 父类为Object或null则查找结束 } Map<JdbcType, TypeHandler<?» jdbcHandlerMap = typeHandlerMap.get(superclass); if (jdbcHandlerMap != null) { return jdbcHandlerMap; } else { // 顺着继承树,递归查找父类对应的TypeHandler集合 return getJdbcHandlerMapForSuperclass(superclass); } }
别名管理
在《02 | 订单系统持久层示例分析,20 分钟带你快速上手 MyBatis》分析的 MyBatis 示例中,我们在 mybatis-config.xml 配置文件中使用 |
更多学习
更多实时资讯,前沿技术,生活趣事。尽在【老马啸西风】
交流社群:[交流群信息](https://mp.weixin.qq.com/s/rkSvXxiiLGjl3S-ZOZCr0Q)