解析Mybatis的typeHandlers元素,配置Mybatis的类型转换器
在学习本章内容之前,可以通过类型转换器(typeHandlers)来了解关于
TypeHandler
的用法。
示例
为了更好的理解mybatis
的TypeHandler
对象,我们在测试包org.apache.learning
下,新建一个typehandler
包,该包下的所有数据,均用于演示TypeHandler
对象的用法。
首先我们在包下新建一个CreateDB.sql
的脚本文件:
drop table users if exists;
create table users
(
id varchar(36),
name varchar(20),
gender varchar(6)
);
insert into users (id, name,gender) values ('38400000-8cf0-11bd-b23e-10b96e4ef00d', 'Panda', '男');
该脚本用于创建一个users
表,并插入一条数据以供使用。
之后我们新建一个mybatis-config.xml
文件,该文件用于为mybatis
提供一个基本的数据源环境配置:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="" value=""/>
</transactionManager>
<!-- 使用 hsql 内存数据库 -->
<dataSource type="UNPOOLED">
<property name="driver" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:mem:type_handler_test"/>
<property name="username" value="sa"/>
</dataSource>
</environment>
</environments>
</configuration>
然后创建一个名为EUserGender
的枚举对象:
/**
* 用户性别
*
* @author HanQi [Jpanda@aliyun.com]
* @version 1.0
* @since 2020/3/14 10:47
*/
@Getter
@AllArgsConstructor
public enum EUserGender {
MALE("男"), FEMALE("女");
private String name;
}
并为EUserGender
创建一个专属的TypeHandler
——EUserGenderTypeHandler
:
public class EUserGenderTypeHandler extends BaseTypeHandler<EUserGender> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, EUserGender parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter.getName());
}
@Override
public EUserGender getNullableResult(ResultSet rs, String columnName) throws SQLException {
String columnValue = rs.getString(columnName);
return findEUserGender(columnValue);
}
@Override
public EUserGender getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String columnValue = rs.getString(columnIndex);
return findEUserGender(columnValue);
}
@Override
public EUserGender getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String columnValue = cs.getString(columnIndex);
return findEUserGender(columnValue);
}
private EUserGender findEUserGender(String dbName) {
return Stream.of(EUserGender.values())
.filter(g -> g.getName().equals(dbName))
.findFirst()
.orElse(null);
}
}
再新建一个名为User
的实体对象,对象中的属性定义和创建的users
表中的列保持一致:
@Getter
@Setter
@NoArgsConstructor
public class User {
private String id;
private String name;
private EUserGender gender;
}
紧接着新建一个Mapper
接口,该接口定义了操作User
对象的两个方法:
public interface Mapper {
List<User> selectByGender(EUserGender gender);
List<User> selectByGenderWithoutSpecify(EUserGender gender);
}
以及Mapper
对应的xml
文件——Mapper.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.apache.learning.typehandler.Mapper">
<resultMap type="org.apache.learning.typehandler.User"
id="userResult">
<result property="id" column="id"/>
<result property="name" column="name"/>
<!-- 指定属性对应的TypeHandler-->
<result property="gender" column="gender"
typeHandler="org.apache.learning.typehandler.EUserGenderTypeHandler"/>
</resultMap>
<!-- 指定属性对应的TypeHandler-->
<select id="selectByGender" resultMap="userResult">
select *
from users
where gender = #{gender, typeHandler=org.apache.learning.typehandler.EUserGenderTypeHandler}
</select>
<!-- 未指定属性对应的TypeHandler-->
<select id="selectByGenderWithoutSpecify" resultType="org.apache.learning.typehandler.User">
select *
from users
where gender = #{gender}
</select>
</mapper>
其中selectByGender()
的方法针对gender
属性明确指定了EUserGenderTypeHandler
作为类型转换器,selectByGenderWithoutSpecify()
方法只是一个普通的方法定义。
完成上述的基础数据的准备之后,新建一个名为typeHanlderTest
的单元测试类,测试常见的三种场景:
/**
* 类型转换器测试类
*
* @author HanQi [Jpanda@aliyun.com]
* @version 1.0
* @since 2020/3/14 10:45
*/
public class TypeHandlerTest {
private static SqlSessionFactory sqlSessionFactory;
@BeforeEach
@SneakyThrows
public void setup() {
@Cleanup
Reader reader = Resources.getResourceAsReader("org/apache/learning/typehandler/mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
sqlSessionFactory.getConfiguration().addMapper(Mapper.class);
BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), "org/apache/learning/typehandler/CreateDB.sql");
}
/**
* 声明语句时,指定类型转换器
*/
@Test
public void testForCustomTypeHandler() {
// 执行准备工作
@Cleanup
SqlSession sqlSession = sqlSessionFactory.openSession();
// 执行
Mapper mapper = sqlSession.getMapper(Mapper.class);
List<User> users = mapper.selectByGender(EUserGender.MALE);
Assertions.assertEquals("Panda", users.get(0).getName());
}
/**
* 普通方法且无类型转换器
*/
@Test
public void testForCustomTypeHandlerWithoutSpecify() {
// 执行准备工作
@Cleanup
SqlSession sqlSession = sqlSessionFactory.openSession();
// 执行
Mapper mapper = sqlSession.getMapper(Mapper.class);
List<User> users = mapper.selectByGenderWithoutSpecify(EUserGender.MALE);
Assertions.assertTrue(users.isEmpty());
}
/**
* 测试普通方法触发全局类型转换器
*/
@Test
public void testForCustomTypeHandlerWithoutSpecifyNeedSuccess() {
// 注册全局的类型转换器
sqlSessionFactory.getConfiguration().getTypeHandlerRegistry().register(EUserGenderTypeHandler.class);
// 执行准备工作
@Cleanup
SqlSession sqlSession = sqlSessionFactory.openSession();
// 执行
Mapper mapper = sqlSession.getMapper(Mapper.class);
List<User> users = mapper.selectByGenderWithoutSpecify(EUserGender.MALE);
Assertions.assertEquals("Panda", users.get(0).getName());
}
}
毫无疑问,上面的单元测试都能正常运行,这也就意味着我们自定义的TypeHandler
生效了,那么TypeHandler
到底是一个怎样的东西呢?
类型转换器——TypeHandler
TypeHandler
是myabtis
中定义的负责jdbc
类型和java
类型互相转换的策略接口,我称之为类型转换器
。
TypeHandler
接口有一个泛型参数定义,这个泛型参数就是要处理的java
类型,该定义还约束了标准的类型转换器需要提供下列四种方法:
- 根据参数索引和jdbc类型设置
PreparedStatement
语句中的值,即将?
替换为实际值的setParameter(PreparedStatement,int,T,JdbcType)
. - 获取指定
ResultSet
中指定名称的列对应的值,并转换为相应的JAVA类型的getResult(ResultSet,String)
。 - 获取指定
ResultSet
中指定索引下面的列对应的值,并转换为相应的JAVA类型的getResult(ResultSet,int)
。 - 获取指定存储过程(
CallableStatement
)中指定索引下标对应的值,并转换为相应的JAVA类型的getResult(CallableStatement,int
。
/**
* 类型转换处理器
*
* @author Clinton Begin
*/
public interface TypeHandler<T> {
/**
* 根据参数索引和jdbc类型设置PreparedStatement语句中的值,即将? 替换为实际值。
*
* @param ps PreparedStatement 语句
* @param i 参数索引
* @param parameter 参数
* @param jdbcType jdbc类型
* @throws SQLException SQL异常
*/
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
/**
* 获取指定ResultSet中指定名称的列对应的值,并转换为相应的JAVA类型
*
* @param rs ResultSet
* @param columnName 列名称
* @return 值
* @throws SQLException SQL异常
*/
T getResult(ResultSet rs, String columnName) throws SQLException;
/**
* 获取指定ResultSet中指定索引下面的列对应的值,并转换为相应的JAVA类型
*
* @param rs ResultSet
* @param columnIndex 列索引
* @return 值
* @throws SQLException SQL异常
*/
T getResult(ResultSet rs, int columnIndex) throws SQLException;
/**
* 获取指定存储过程(CallableStatement)中指定索引下标对应的值,并转换为相应的JAVA类型
*
* @param cs 存储过程
* @param columnIndex 列索引
* @return 值
* @throws SQLException SQL异常
*/
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
为了简化我们的操作,Mybatis
定义了一个名为BaseTypeHandler
的抽象类,实现了TypeHandler
接口,他是Mybatis
提供的一个默认的类型转换器的基类。
public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {
/**
* Mybatis配置
*
* @deprecated Since 3.5.0 - See https://github.com/mybatis/mybatis-3/issues/1203. This field will remove future.
*/
@Deprecated
protected Configuration configuration;
/**
* @deprecated Since 3.5.0 - See https://github.com/mybatis/mybatis-3/issues/1203. This property will remove future.
*/
@Deprecated
public void setConfiguration(Configuration c) {
this.configuration = c;
}
/**
* @param ps PreparedStatement
* @param i 参数索引
* @param parameter 参数值
* @param jdbcType jdbc类型
*/
@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);
}
}
}
@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);
}
}
public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
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;
}
BaseTypeHandler
实现了TypeHandler
接口定义的默认方法,添加了基本的数据校验,并暴露出了新的抽象方法供子类实现。
虽然他还添加了对Mybatis配置类
Configuration
的引用,并提供了对应的设值方法,但是这一功能已被弃用,所以这里就不在讨论。
获取泛型定义原始类型——TypeReference
除此之外,BaseTypeHandler
还继承了TypeReference
抽象类,提供了获取泛型定义原始类型的能力。
public abstract class TypeReference<T> {
/**
* TypeReference<T> 中T的泛型类型
*/
private final Type rawType;
protected TypeReference() {
// 获取当前类的泛型类型
rawType = getSuperclassTypeParameter(getClass());
}
// ... 省略 getSuperclassTypeParameter() 方法
}
TypeReference
抽象类中提供了一个Type
类型的rawType
属性,并对外了暴露了该属性的getter
方法,该属性的作用是保存类上泛型定义的类型,他的初始化工作是在TypeReference
的构造方法中完成的。
有关于泛型解析相关的知识,我们在介绍TypeParameterResolver
时已经做了很完整的分析,这里就不在赘述。
我们自己定义的
EUserGenderTypeHandler
就继承了BaseTypeHandler
基类,并通过泛型参数指定了要处理的java
类型是EUserGender
。
存储类型转换器的注册表——TypeHandlerRegistry
在简单的了解了TypeHandler
之后,我们回过头来继续看TypeHandlerRegistry
。
Tips:
我想换一种方式来描述对象的功能和实现,之前描述一个类的源码时,都是站在程序员的角度去想,去写,按照方法调用栈来解析源代码。现在我想站在产品的角度来解析代码,
先给出实例存在的目的和宗旨,并围绕着这个目的和宗旨,逐步的扩展出代码。
TypeHandlerRegistry的作用
TypeHandlerRegistry
作为存储类型转换器的注册表,他存在的目的就是为了保存所有类型转换器,并提供相应的注册和获取类型转换器的功能。
编程是一件开放性很高的事情,一千个人眼中就有一千个哈姆雷特,一千个程序员对于同一个功能可能有不止一万种实现方案。
所以在接下来我们会根据需求去梳理TypeHandlerRegistry
的实现,而不是去重新实现一个新的TypeHandlerRegistry
。
TypeHandlerRegistry的属性定义
首先我们看一下TypeHandlerRegistry
中的属性定义:
/**
* jdbc类型和处理器的映射关系
*/
private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<>(JdbcType.class);
/**
* java类型和映射处理器的关系
*/
private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new ConcurrentHashMap<>();
/**
* 未知类型的对应的默认处理器
*/
private final TypeHandler<Object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this);
/**
* 处理器类型和处理器实例的映射关系
*/
private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<>();
/**
* 空的类型转换器映射关系,仅仅是一个标志性对象
*/
private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();
/**
* 默认的枚举类型转换器
*/
private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
仔细看前两个属性的定义,JDBC_TYPE_HANDLER_MAP
用于存放JdbcType
和TypeHandler
的对应关系。
TYPE_HANDLER_MAP
的key
值是一个java
类型,value
是Map<JdbcType, TypeHandler<?>>
集合。
这两个属性的定义很容易让人误认为:TYPE_HANDLER_MAP
的value
存放是的指定java
类型对应的JDBC_TYPE_HANDLER_MAP
集合。
但是,事实上,在梳理源码后,我们会发现二者毫无关系。
JDBC_TYPE_HANDLER_MAP
的作用是:维护一个JdbcType
和其默认的TypeHandler
之间的对应关系。
TYPE_HANDLER_MAP
的作用是:维护java
类型和其所有可转换的JdbcType
的关系,并保留负责进行转换操作的TypeHandler
实例。
换句话,TYPE_HANDLER_MAP
集合中的value
虽然类型也是Map<JdbcType, TypeHandler<?>>
,但是他存储的JdbcType
和TypeHandler
的对应关系可能会因为所属的java
类型的不同而不同。
比如:
var TYPE_HANDLER_MAP={
'Integer':{
"JdbcType.INTEGER":"IntegerTypeHandler"
},
'Double':{
"JdbcType.INTEGER":"DoubleTypeHandler"
}
}
在上面这个通过JS
描述的伪对象TYPE_HANDLER_MAP
中,我们可以看到随着java
类型由Integer
变成Double
,JdbcType.INTEGER
对应的TypeHandler
实例也由IntegerTypeHandler
变成了DoubleTypeHandler
。
除了这个容易让人混淆的问题之外,还有一个问题困扰了我很久,按照前面的理解,一个jdbc
类型应该是可以对应多个TypeHandler
对象的,毕竟,一个jdbc
类型可能会转换成不同的JAVA
对象,可是按照JDBC_TYPE_HANDLER_MAP
的定义,一个jdbc
类型只能对应一个TypeHandler
,这是为什么呢?
这是因为JDBC_TYPE_HANDLER_MAP
负责存储的是JdbcType
和其默认的TypeHandler
之间的对应关系,JDBC_TYPE_HANDLER_MAP
是TYPE_HANDLER_MAP
的一个补充,当TYPE_HANDLER_MAP
中无法获取TypeHandler
时,就会考虑根据jdbc
类型从JDBC_TYPE_HANDLER_MAP
中获取一个默认的TypeHandler
来完成类型转换的处理操作。
TypeHandlerRegistry
还有其他几个属性,被final
修饰的UNKNOWN_TYPE_HANDLER
属性是一个UnknownTypeHandler
类型的类型转换器。
UnknownTypeHandler
虽然听起来像是用于处理未知类型,但是实际上在他的实现中,我们会发现他更偏向于一个代理对象,他会在运行期间,动态的去选择一个有效的TypeHandler
实例来完成类型转换的工作。
类型为Map<JdbcType, TypeHandler<?>>
的NULL_TYPE_HANDLER_MAP
属性和JDBC_TYPE_HANDLER_MAP
也没有任何关系,他是一个标志性的对象,用来表示一个空的类型转换器映射关系。
类型为Map<Class<?>, TypeHandler<?>>
的ALL_TYPE_HANDLERS_MAP
属性,他的key
是TypeHandler
实例的具体类型,他的value
则是TypeHandler
实例本身,他的作用是维护所有有效的TypeHandler
。
最后一个类型为Class<? extends TypeHandler>
的defaultEnumTypeHandler
属性,则定义了默认的枚举类型转换器。
枚举类型转换器实际上是一个比较特殊的处理器,他有别于普通的处理器只用于处理特定的类,枚举类型转换器会处理任意继承了Enum
的类。
注册类型转换器功能的实现
TypeHandlerRegistry
的几个集合类型的属性已经为我们提供了保存类型转换器的功能,现在我们看一下TypeHandlerRegistry
如何实现注册类型转换器的功能。
现在已经知道一个类型转换器核心的数据有:被处理的Java类型
,被处理的JDBC类型
以及类型转换器TypeHandler
本身这三个核心要素,同时Java类型
和JDBC类型
之间是一个多对多的关系。
那么就注册功能的实现来讲,最好的场景就是我们能够直接拿到Java类型
和JDBC类型
以及相应的类型转换器TypeHandler
实例这三个参数。
如果不能,那就退而求其次,获取类型转换器TypeHandler
加上Java类型
与JDBC类型
中的一个。
如果还不行,只有类型转换器TypeHandler
也可以。
对于那些缺失的参数,我们的实现通常是想办法推导出它们,然后转为完美场景进行处理。
TypeHandlerRegistry
在注册类型转换器的功能实现方面,提供了较多的register
方法重载方法。
其中有一个方法签名为private void register(Type javaType, JdbcType jdbcType,TypeHandler<?> handler)
的重载方法,用于处理三要素齐全的场景,完成了事实上的类型转换器的注册工作:
/**
* 注册类型转换器
*
* @param javaType java类型
* @param jdbcType jdbc类型
* @param handler 类型转换器
*/
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
// 获取该java类型对应的jdbc类型转换器的集合
Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<>();
// 注册java类型和【jdbc和处理器的映射关系】的映射关系
TYPE_HANDLER_MAP.put(javaType, map);
}
map.put(jdbcType, handler);
}
// 注册处理器类型和实例的关系
ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}
这是register
重载方法中最核心的方法,多数register
重载方法的调用,最终会落在该方法上来完成最终的处理操作。
他的实现比较简单,唯一值得注意的就是NULL_TYPE_HANDLER_MAP
作为标识符的作用就体现在这里。
还有一些register
重载方法的入参会缺失Java类型
与JDBC类型
中的一个,甚至是二者全部缺失。
我们前面说过,当我们缺少某一参数时,会尽可能的去推导出缺少属性的参数类型,然后转为完美场景进行处理。
推导Java
类型参数
在缺失java
类型参数的场景下,TypeHandlerRegistry
给出的解决方案是提供MappedTypes
注解和TypeReference
抽象父类。
MappedTypes
注解可以直接提供TypeHandler
对象可转换的java
类型集合:
/**
* 该注解用于给类型处理器指定可处理的JAVA类型
*
* @author Eduardo Macarron
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MappedTypes {
/**
* 可转换的JAVA类型集合
*/
Class<?>[] value();
}
TypeReference
抽象父类则可以获取其实现类的泛型参数定义。
具体的解析方法可以参考register(TypeHandler<T> typeHandler)
方法:
public <T> void register(TypeHandler<T> typeHandler) {
boolean mappedTypeFound = false;
// 获取MappedTypes注解
MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
for (Class<?> handledType : mappedTypes.value()) {
// 根据注解,注册类型转换器
register(handledType, typeHandler);
mappedTypeFound = true;
}
}
// 解析泛型
// @since 3.1.0 - try to auto-discover the mapped type
if (!mappedTypeFound && typeHandler instanceof TypeReference) {
try {
TypeReference<T> typeReference = (TypeReference<T>) typeHandler;
register(typeReference.getRawType(), typeHandler);
mappedTypeFound = true;
} catch (Throwable t) {
// maybe users define the TypeReference with a different type and are not assignable, so just ignore it
}
}
// 特殊的null类型
if (!mappedTypeFound) {
register((Class<T>) null, typeHandler);
}
}
优先解析MappedTypes
注解中指定的类型,其次是根据TypeReference
获取泛型类型,如果还没能获取java
类型,那就使用特殊的类型null
。
推导Jdbc
类型参数
推导jdbc
类型参数,TypeHandlerRegistry
给出的解决方案是提供MappedJdbcTypes
注解,和MappedTypes
相似。
MappedJdbcTypes
注解的作用是标注出指定TypeHandler
可转换的jdbc
类型集合:
/**
* 给TypeHandler指定处理的JdbcType
* @author Eduardo Macarron
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MappedJdbcTypes {
// 可处理的jdbc类型集合
JdbcType[] value();
// 是否额外注册JDBC类型为null的处理器
boolean includeNullJdbcType() default false;
}
MappedJdbcTypes
除了可以指定jdbc
类型集合之外,可有一个includeNullJdbcType()
方法,用于指定是否额外注册Jdbc
类型为null
的处理器。
负责推导jdbc
类型参数的方法是private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler)
:
/**
* 注册java类型和类型转换出处理器的关系
*
* @param javaType java类型
* @param typeHandler 类型转换处理器实例
* @param <T> java类型
*/
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
// 寻找mappedJdbcTypes注解,mappedJdbcTypes用于给TypeHandler指定处理的JdbcType
MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
if (mappedJdbcTypes != null) {
for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
// 注册java类型处理和jdbc类型以及类型转换处理器的关系
register(javaType, handledJdbcType, typeHandler);
}
if (mappedJdbcTypes.includeNullJdbcType()) {
// 注册JDBC类型为null的处理器
register(javaType, null, typeHandler);
}
} else {
// 注册JDBC类型为null的处理器
register(javaType, null, typeHandler);
}
}
上面的两个方法的实现上,分别
上面提到的三个register()
方法,是TypeHandlerRegistry
中比较核心的三个注册方法,这三个方法相互配合使用基本可以完成绝大多数注册场景。
还有一些其他的register()
重载方法,这些重载方法中有一部分用于处理java
类型的变种表现:
比如,将String
类型描述的Java
类型转换为实际的Java
类型:
/**
* 注册指定java类型的指定类型转换器
*
* @param javaTypeClassName java类型名称
* @param typeHandlerClassName 类型转换处理器名称
*/
public void register(String javaTypeClassName, String typeHandlerClassName) throws ClassNotFoundException {
register(Resources.classForName(javaTypeClassName), Resources.classForName(typeHandlerClassName));
}
比如,获取TypeReference
对应的泛型类型,然后进行注册:
/**
* 根据泛型注册类型转换处理器
*
* @param javaTypeReference java泛型
* @param handler 处理器
* @param <T> 泛型
*/
public <T> void register(TypeReference<T> javaTypeReference, TypeHandler<? extends T> handler) {
register(javaTypeReference.getRawType(), handler);
}
还有一部分register()
方法用于处理特殊的场景,比如:
/**
* 注册JDBC类型转换器
*
* @param jdbcType jdbc类型
* @param handler 处理器
*/
public void register(JdbcType jdbcType, TypeHandler<?> handler) {
// 注册jdbc类型转换器
JDBC_TYPE_HANDLER_MAP.put(jdbcType, handler);
}
这个方法注册的是指定的Jdbc
类型与其对应的默认转换器,和前面提到那些register()
方法在实际意义上完全不同。
还有一个register()
方法用于批量注册类型转换器:
// 注册指定包下所有的java类
public void register(String packageName) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
// 返回当前已经找到的所有TypeHandler的子类
resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
for (Class<?> type : handlerSet) {
//Ignore inner classes and interfaces (including package-info.java) and abstract classes
if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
// 忽略匿名类、接口以及抽象类
// 注册类型转换处理器
register(type);
}
}
}
在这个方法中,他通过ResolverUtils
的find()
方法来获取初步符合预期的类集合,之后排除掉匿名类、成员类和接口,对剩下的类执行别名注册的操作。
有关于更多
ResolverUtil
的信息,在解析TypeAliases
元素时,已经给出。
获取类型转换器功能的实现
在实现上TypeHandlerRegistry
获取类型转换器的方法大致可以分为两类,其中一类是用于判断指定的类型转换器是否存在的hasTypeHandler()
方法,一类是实际获取类型转换器对象的getTypeHandler()
方法。
这两类方法的实现逻辑大致是一致的,hasTypeHandler()
方法仅仅是对getTypeHandler()
方法的返回结果做了一层非空判断,并包装成Boolean
值而已。
getTypeHandler()
方法的核心重载实现是:
/**
* 获取指定类型的处理器
*
* @param type java类型
* @param jdbcType jdbc类型
* @param <T> 处理器类型
*/
@SuppressWarnings("unchecked")
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
if (ParamMap.class.equals(type)) {
return null;
}
/*
* 获取所有指定类型的java处理器
*/
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
TypeHandler<?> handler = null;
if (jdbcHandlerMap != null) {
// 获取对应的jdbc处理器
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;
}
可以看到,方法实现比较简单,先获取指定java
类型对应的已注册的jdbc
类型及其转换处理器,之后根据jdbc
类型参数获取转换器,
如果没有指定jdbc
类型,则调用pickSoleHandler(jdbcHandlerMap)
方法,尝试获取一个有效转换器:
/**
* 从一组类型转换处理器中选择唯一的一个转换器,如果超过一个类型转换处理器,返回null。
*
* @param jdbcHandlerMap jdbc类型对应的类型转换处理器集合
*/
private TypeHandler<?> pickSoleHandler(Map<JdbcType, TypeHandler<?>> jdbcHandlerMap) {
TypeHandler<?> soleHandler = null;
for (TypeHandler<?> handler : jdbcHandlerMap.values()) {
if (soleHandler == null) {
soleHandler = handler;
} else if (!handler.getClass().equals(soleHandler.getClass())) {
// More than one type handlers registered.
return null;
}
}
return soleHandler;
}
pickSoleHandler(jdbcHandlerMap)
方法对于有效转换器的定义比较简单,那就是返回java
类型对应的唯一的类型转换器,这个唯一的类型转换器可以当做是默认的类型转换器来使用。
负责获取指定java
类型对应的已注册jdbc
类型及其转换处理器的方法getJdbcHandlerMap()
在实现上兼容了枚举和子类的处理操作。
// 获取指定java类型对应所有类型转换处理器
private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMap(Type type) {
// 获取当前java类型对应的类型转换器集合
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(type);
if (NULL_TYPE_HANDLER_MAP.equals(jdbcHandlerMap)) {
return null;
}
if (jdbcHandlerMap == null && type instanceof Class) {
Class<?> clazz = (Class<?>) type;
if (clazz.isEnum()) {
// 根据枚举查找类型转换处理器
jdbcHandlerMap = getJdbcHandlerMapForEnumInterfaces(clazz, clazz);
if (jdbcHandlerMap == null) {
// 注册类型转换处理器
register(clazz, getInstance(clazz, defaultEnumTypeHandler));
// 返回类型转换处理器
return TYPE_HANDLER_MAP.get(clazz);
}
} else {
// 根据父类查找类型转换处理器
jdbcHandlerMap = getJdbcHandlerMapForSuperclass(clazz);
}
}
// 注册类型转换处理器
TYPE_HANDLER_MAP.put(type, jdbcHandlerMap == null ? NULL_TYPE_HANDLER_MAP : jdbcHandlerMap);
return jdbcHandlerMap;
}
枚举兼容
在处理枚举类型时,如果该枚举没有定义相关的类型转换器,将会递归调用getJdbcHandlerMapForEnumInterfaces()
方法,尝试从实现的接口定义中获取类型转换器。
如果其接口也没有对应的类型转换器,那么将会使用TypeHandlerRegistry
的defaultEnumTypeHandler
属性对应的类型转换器作为当前枚举的类型处理器。
/**
* 根据枚举接口获取类型转换器
*
* @param clazz java类型
* @param enumClazz 枚举类型
* @return 类型转换处理器
*/
private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMapForEnumInterfaces(Class<?> clazz, Class<?> enumClazz) {
for (Class<?> iface : clazz.getInterfaces()) {
// 从指定java类的接口上找类型转换处理器
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(iface);
if (jdbcHandlerMap == null) {
// 没有对应的类型转换器,递归往上找
jdbcHandlerMap = getJdbcHandlerMapForEnumInterfaces(iface, enumClazz);
}
if (jdbcHandlerMap != null) {
// 如果找到了类型转换器
// Found a type handler regsiterd to a super interface
HashMap<JdbcType, TypeHandler<?>> newMap = new HashMap<>();
for (Entry<JdbcType, TypeHandler<?>> entry : jdbcHandlerMap.entrySet()) {
// Create a type handler instance with enum type as a constructor arg
// 创建一个对应该类的类型转换器
newMap.put(entry.getKey(), getInstance(enumClazz, entry.getValue().getClass()));
}
return newMap;
}
}
return null;
}
子类兼容
在处理普通类型时,如果该类型没有定义相关的类型转换器,将会递归调用getJdbcHandlerMapForSuperclass()
方法,尝试从继承的父类定义中获取类型转换器。
private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMapForSuperclass(Class<?> clazz) {
Class<?> superclass = clazz.getSuperclass();
if (superclass == null || Object.class.equals(superclass)) {
return null;
}
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(superclass);
if (jdbcHandlerMap != null) {
return jdbcHandlerMap;
} else {
return getJdbcHandlerMapForSuperclass(superclass);
}
}
无论是针对普通类还是枚举,在查找到新的类型转换器关系之后,都会重新注册到
TypeHandlerRegistry
中。
我思考了很久,为什么在做子类兼容时,只递归查找父类对应的类型转换器集合,而不是连父接口对应的类型转换器一并查找?
先说结论:因为没必要。
首先,我们要明确一点:接口,是抽象方法的集合,方法定义的是一个对象的行为,所以接口定义的是一类对象的行为,而不是特性。
被类型转换器处理的java
类型通常需要具有一定程度上的一致性和可预见性,比如,给定Jdbc
数值A
,可以获取java
对象a
,那么理论上可预见的是:通过java
对象a
可以获得Jdbc
数值A
。
这种特性意味着,被类型转换器处理的java
类型通常具有简单性,不会是一个复杂的业务对象,这时候强调的往往是该对象的某一特性,而不是行为,这时候,这种对象通常会被直接定义为类而不是接口。
那么枚举对象为什么适配的是接口而不是类呢?
枚举对象也具有简单性,但是枚举默认继承了Enum
父类,所以只能实现接口,没有办法直接定义属性,所以退而求其次,通过方法来间接的限制属性。
getTypeHandler()
方法还有一些其他重载实现,逻辑大致与register()
方法重载实现一致,这里就不做讨论了。
到这里,TypeHandlerRegistry
就完成了保存所有类型转换器,以及相应的注册和获取类型转换器的功能的实现。
其他
TypeHandlerRegistry
中还有一些其他方法定义,比如defaultEnumTypeHandler
属性的getter/setter
方法和UNKNOWN_TYPE_HANDLER
属性的getter
方法,以及通过反射获取TypeHandler
对象实例的getInstance
方法。
这些方法的实现比较简单,这里就不在赘述了。
解析typeHandlers
元素
现在我们回到typeHandlers
元素的解析工作中。
在Mybatis中关于typeHandlers
的DTD
是如此定义的:
<!ELEMENT typeHandlers (typeHandler*,package*)>
在typeHandlers
下面可以出现零个或多个typeHandler
或者package
标签。
typeHandler
元素用于注册单个TypeHandler
实例,它有三个属性javaType
,jdbcType
以及handler
,其中javaType
和jdbcType
是可选的,他们分别表示java类型和jdbc类型,handler
属性是必填的,他表示类型转换器的类型,这三个属性都可以使用类型别名。
<!ELEMENT typeHandler EMPTY>
<!ATTLIST typeHandler
javaType CDATA #IMPLIED
jdbcType CDATA #IMPLIED
handler CDATA #REQUIRED
>
package
元素用于批量注册TypeHandler
实例,它只有一个必填的name
属性,他表示用户需要注册TypeHandler
的基础包名,Mybatis
将会递归处理该基础包及其子包下所有可用的TypeHandler
实例。
<!ELEMENT package EMPTY>
<!ATTLIST package
name CDATA #REQUIRED
>
调用解析的入口(XmlConfigBuilder
):
private void parseConfiguration(XNode root) {
// ...
// 注册类型转换器
typeHandlerElement(root.evalNode("typeHandlers"));
// ...
}
解析并注册TypeHandler
实例:
// 解析typeHandlers元素
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 {
// 单个注册
// 获取java类型的名称
String javaTypeName = child.getStringAttribute("javaType");
// 获取jdbc类型的名称
String jdbcTypeName = child.getStringAttribute("jdbcType");
// 获取类型转换处理器的名称
String handlerTypeName = child.getStringAttribute("handler");
// 解析出java类型
Class<?> javaTypeClass = resolveClass(javaTypeName);
// 解析jdbc类型
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
实例的解析注册也分为两种:
一类是解析typeHandler
元素,尝试获取注册TypeHandler
实例的三要素完成注册工作。
一类是解析package
元素,获取用于批量注册的包名,调用TypeHandlerRegistry
的register()
方法完成整包批量注册。
关于具体注册方法的实现,前面已经有了详细的阐述。
至此,整个typeHandlers
元素的解析也已经完成,Mybatis
的基础准备工作也准备的差不多了。
接下来就是解析Mybatis
的Mapper
配置文件。