MyBatis源码解析 - 类型转换模块
前言
JDBC数据类型与Java语言中的数据类型并不是完全对应的,所以在PreparedStatement为SQL语句绑定参数时,需要从Java类型转换成JDBC类型,而从结果集中获取数据时,则需要从JDBC类型转换成Java类型。MyBatis 使用类型处理器完成上述两种转换,如图所示。
在MyBatis中使用JdbeType
这个枚举类型代表JIDBC中的数据类型,该枚举类型中定义了TYPECODE
字段,记录了JDBC类型在javasql.Types
中相应的常量编码,并通过一个静态集合codelookup
(HashMap
类型)维护了常量编码与JdbcType之间的对应关系。
TypeHandler
TypeHandler
是类型处理器接口,MyBatis中所有的类型转换器都继承了TypeHandler
接口,在TypeHandler
接口中定义了如下四个方法,这四个方法分为两类:setParameter()
方法负责将数据由JdbeType类型转换成Java
类型;getResult()
方法及其重载负责将数据由Java类型转换成JdbcType类型。
public interface TypeHandler {
//在通过PreparedStatement为SQL语句绑定参数时,会将数据由JdbcType类型转换成Java类型
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
//从 ResultSet 中获取数据时会调用此方法,会将数据由Java类型转换成JdbcType类型
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
中实现TypeHandler.setParameter()
方法和TypeHandler.getResult()
方法, 具体实现如下所示。需要注意的是,这两个方法对于非空数据的处理都交给了子类实现。
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 {
//绑定参数为null的处理
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 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);
}
}
BaseTypeHandler
的实现类比较多,如图所示,但大多是直接调用PreparedStatement
和ResultSet
或CallableStatement
的对应方法,实现比较简单。
这里以IntegerTypeHandler
为例简单介绍,其他的实现类请读者参考相关源码:
public class IntegerTypeHandler extends BaseTypeHandler {
@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 {
//调用PreparedStatement.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)
throws SQLException {
//调用CallableStatement.getInt()获取指定列值
int result = cs.getInt(columnIndex);
return result == 0 && cs.wasNull() ? null : result;
}
}
一般情况下, TypeHandler
用于完成单个参数以及单个列值的类型转换,如果存在多列值转换成一个Java对象的需求,应该优先考虑使用在映射文件中定义合适的映射规则(
节点)完成映射。
TypeHandlerRegistry
介绍完TypeHandler
接口及其功能后,我们来看一下MyBatis
如何管理众多的TypeHandler
接口实现,如何知道何时使用哪个TypeHandler
接口完成转换。这些都是由TypehandlerRegistry
完成的,在MyBatis
初始化过程中,会为所有已知的TypeHandler
创建对象,并实现注册到TypeHandlerRegistry
中,由TypeHandlerRegistry
负责挂你这些TypeHandler
对象。
TypeHandlerRegistry
的核心字段的含义如下:
//记录JdbcType与TypeHandler之间的对应关系,其中JdbcType是一个枚举类型,它定义对应的JDBC类型
//该集合主要用于从结果集读取数据时,将数据从Jdbc类型转换成Java类型
private final Map> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);
//记录了Java类型向指定JdbcType转换时,需要使用的TypeHandler对象。例如: Java 类型中的String可能
//转换成数据库的char、varchar等多种类型,所以存在一对多关系
private final Map>> typeHandlerMap = new ConcurrentHashMap<>();
private final TypeHandler
1. 注册TypeHandler
对象
TypeHandlerRegistry.register()
方法实现了注册TypeHandler对象的功能,该注册过程会向上述四个集合中添加TypeHandler
对象。register()
方法有多个重载,这些重载之间的调用关系如图所示。
从图中可以看出,多数register()方法最终会调用重载④完成注册功能,我们先来介绍该方法的实现,其三个参数分别指定了TypeHandler 能够处理的Java 类型、Jdbc 类型以及TypeHandler对象。
//register()方法的重载④的实现如下:
private void register(Type javaType, JdbcType jdbcType, TypeHandler> handler) {
if (javaType != null) { //检验是否明确指定了TypeHandler能够处理的Java类型
//获取指定Java类型在TYPE_ HANDLER_ MAP集合中对应的TypeHandler集合
Map> map = typeHandlerMap.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
//创建新的TypeHandler集合,并添加到TYPE HANDLER MAP中
map = new HashMap<>();
typeHandlerMap.put(javaType, map);
}
//将TypeHandler对象注册到typeHandlerMap集合中
map.put(jdbcType, handler);
}
//向AllTypeHandler集合注册TypeHandler类型和对应的TypeHandler对象【以Class为key handler对象为value】
allTypeHandlersMap.put(handler.getClass(), handler);
}
在①~③这个三个registr()
方法重载中会尝试读取TypeHandler
类中定义的@MappedTypes
注解和@MappedJdbcTypes
注解,@MappedTypes
注解用于指明该TypeHandler
实现类能够处理的Java类型的集合,@MappedJdbcTypes
注解用于指明该TypeHandler
实现类能够处理的JDBC
类型集合。register()
方法的重载①~③的具体实现如下:
//register()方法的重载①的实现如下:
public void register(Class> typeHandlerClass) {
boolean mappedTypeFound = false;
//获取 @MappedTypes 注解
MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
for (Class> javaTypeClass : mappedTypes.value()) {
//经过强制类型转换 以及反射创建TypeHandler对象之后,交由重载③继续处理
register(javaTypeClass, typeHandlerClass);
mappedTypeFound = true;
}
}
if (!mappedTypeFound) {
//未@MappedTypes 注解,调用重载方法 ② 处理
register(getInstance(null, typeHandlerClass));
}
}
//register()方法的重载②的实现如下:
public void register(TypeHandler typeHandler) {
boolean mappedTypeFound = false;
//获取@MappedTypes注解 并根据MappedTypes注解指定的java类型进行注册,逻辑与重载①类似
MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
for (Class> handledType : mappedTypes.value()) {
//交由重载③进行处理
register(handledType, typeHandler);
mappedTypeFound = true;
}
}
//从3.1.0版本开始,可以根据TypeHandler类型自动查找对应的Java类型,这需要
//我们的TypeHandler实现类同时继承TypeReference这个抽象类
// @since 3.1.0 - try to auto-discover the mapped type
if (!mappedTypeFound && typeHandler instanceof TypeReference) {
try {
TypeReference typeReference = (TypeReference) 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
}
}
if (!mappedTypeFound) {
//类型转换后,交由重载③继续处理
register((Class) null, typeHandler);
}
}
//register()方法的重载③的实现如下:
private void register(Type javaType, TypeHandler extends T> typeHandler) {
//获取MappedJdbcTypes注解
MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
if (mappedJdbcTypes != null) {
//根据指定类型进行注册 读取@MappedJdbcTypes value值 获取handlerJdbcType
for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
register(javaType, handledJdbcType, typeHandler);
}
if (mappedJdbcTypes.includeNullJdbcType()) {
register(javaType, null, typeHandler);
}
} else {
//未指定MappedJdbcTypes注解 交由重载④继续处理
register(javaType, null, typeHandler);
}
}
上述全部的register()
方法重 载都是在向TYPE_ HANDLER_ _MAP
集合和ALL_TYPE_HANDLERS MAP
集合注册TypeHandler 对象,而重载⑤是向JDBC_ TYPE_ HANDLER_ MAP
集合注册TypeHandler
对象,其具体实现如下:
//register()方法重载⑤的实现如下:
public void register(JdbcType jdbcType, TypeHandler> handler) {
// 注册JdbcType类型对一个的TypeHandler
jdbcTypeHandlerMap.put(jdbcType, handler);
}
TypeHandlerRegistry
除了提供注册单个TypeHandler
的register()
重载,还可以扫描整个包下的TypeHandler
接口实现类,并将完成这些TypeHandler
实现类的注册。下面来看重载⑥的具体实现:
//register()方法重载⑥的实现,主要用来自动扫描指定包下的TypeHandler实现并完成注册
public void register(String packageName) {
ResolverUtil> resolverUtil = new ResolverUtil<>();
//查找指定包下的TypeHandler接口实现类
resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
Set>> 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);
}
}
}
最后来看TypeHandlerRegistry
构造方法,会通过上述register()
方法为很多基础类型注册对应的TypeHandler
对象,简略代码如下:
public TypeHandlerRegistry(Configuration configuration) {
//...这里重点看下String相关的TypeHandler对象的注册 其他类型的TypeHandler的注册类型(略)
// stringTypeHandler 能够将数据从String 类型转换成null ( JdbcType ),所以向
TYPE HANDLER MAP
//集合注册该对象,并向ALL_ TYPE_ HANDLERS_ MAP集合注册StringTypeHandler
register(String.class, new StringTypeHandler());
register(JdbcType.NVARCHAR, new NStringTypeHandler());
//向JDBC_TYPE_HANDLER_MAP集合注册NVARCHAR对应的NStringTypeHandler
register(JdbcType.NVARCHAR, new NStringTypeHandler());
// ...注册其他JdbcType类型对应的TypeHandler的过程类似(略)
}
2. 查找TypeHandler对象
介绍完注册TypeHandler对象的功能之后,再来介绍TypeHandlerRegistry提供的查找TypeHandler对象的功能。TypeHandlerRegistry.getTypeHandler()方法实现了从上述四个集合中获取对应TypeHandler对象的功能。TypeHandlerRegistry.getTypeHandler()方法有多个重载,这些重载之间的关系如图所示。
经过一系列类型转 换之后,TypeHandlerRegistry.getTypeHandler()
方法的多个重载都会调用TypeHandlerRegistry.getTypeHandle(Type,JdbcType)
这个重载方法,它会根据指定的Java
类型和JdbcType
类型查找相应的TypeHandler
对象,具体实现如下:
private TypeHandler getTypeHandler(Type type, JdbcType jdbcType) {
if (ParamMap.class.equals(type)) {
return null;
}
//查找或初始化Java类型对一个的TypeHandler对象
Map> jdbcHandlerMap = getJdbcHandlerMap(type);
TypeHandler> handler = null;
if (jdbcHandlerMap != null) {
//根据JdbcType查找TypeHandler
handler = jdbcHandlerMap.get(jdbcType);
if (handler == null) {
handler = jdbcHandlerMap.get(null);
}
if (handler == null) {
// #591
//如果JdbcType只注册了一个TypeHandler,使用此TypeHandler对象
handler = pickSoleHandler(jdbcHandlerMap);
}
}
// type drives generics here
return (TypeHandler) handler;
}
在TypeHandlerRegitry.getJdbcHandlerMap()
方法中,会检测TYPE_HANDLER_MAP
集合中指定Java
类型对应的TypeHandler
集合是否已经初始化。如果未初始化,则尝试以该Java类型的、已初始化的父类对应的TypeHandler
集合为初始集合;如不存在己初始化的父类,则将其对应的TypeHandler
集合初始化为NULL_TYPE_HANDLER_MAP
标识。getJdbcHandlerMap()
方法具体实现如下:
private Map> getJdbcHandlerMap(Type type) {
//查找指定Java类型对应的TypeHandler集合
Map> 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集合,并作为初始化集合
jdbcHandlerMap = getJdbcHandlerMapForSuperclass(clazz);
}
}
// 以NULL_TYPE_HANDLER_MAP作为TypeHandler集合
typeHandlerMap.put(type, jdbcHandlerMap == null ? NULL_TYPE_HANDLER_MAP : jdbcHandlerMap);
return jdbcHandlerMap;
}
// getJdbcHandlerMapForSuperclass()实现
private Map> getJdbcHandlerMapForSuperclass(Class> clazz) {
Class> superclass = clazz.getSuperclass();
if (superclass == null || Object.class.equals(superclass)) {
return null; //父类为Object或null则查找结束
}
Map> jdbcHandlerMap = typeHandlerMap.get(superclass);
if (jdbcHandlerMap != null) {
return jdbcHandlerMap;
} else {
//继续递归查找父类对应的TypeHandler集合
return getJdbcHandlerMapForSuperclass(superclass);
}
}
TypeHandlerRegistry.getMappingTypeHandler()
方法会根据指定的TypeHandler类型,直接从ALL_TYPE_HANDLERS_MAP
集合中查找TypeHandler
对象。TypeHandlerRegistry.getTypeHandler(JdbcType)
方法会根据指定的JdbcType
类型,从JDBC_TYPE_HANDLER_MAP
集合中查找TypeHandler
对象。这两个方法实现相对简单,代码就不贴出来了。
最后,除了MyBatis
本身提供的TypeHandler
实现,我们也可以添加自定义的TypeHandler
接口实现,添加方式是在mybatisconfig.xml
配置文件中的
节点下,添加相应的
节点配置,并指定自定义的TypeHandler
接口实现类。在MyBatis
初始化时会解析该节点,并将该TypeHandler
类型的对象注册到TypeHandlerRegistry
中,供MyBatis
后续使用。在后面介绍MyBatis
初始化时还会提到该配置。
TypeAliasRegistry
在编写SQL语句时,使用别名可以方便理解以及维护,例如表名或列名很长时,我们一般会为其设计易懂易维护的别名。MyBatis
将SQL语句中别名的概念进行了延伸和扩展,MyBatis
可以为一个类添加一一个别名,之后就可以通过别名引用该类。MyBatis
通过TypeAliasRegistry
类完成别名注册和管理的功能,TypeAliasRegistry
的结构比较简单,它通过TYPE_ ALIASES
字段(Map
类型)管理别名与Java
类型之间的对应关系,通过TypeAliasRegistry.registerAlias()
方法完成注册别名,该方法的具体实现如下:
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
的构造方法中,默认为Java的基本类型及其数组类型、基本类型的封装类及其数组类型、Date
、BigDecimal
、BigInteger
、Map
、HashMap
、List
、ArrayList
、Collection
、Iterator
、ResultSet
等类型添加了别名,代码比较简单,请读者参考TypeAliasRegistry
的源码进行学习。
TypeAliasRegistry
中还有两个方法需要介绍一下,registerAliases(String,Class>)
重载会扫描指定包下所有的类,并为指定类的子类添加别名; registerAlias(Class>)
重 载中会尝试读取@Alias
注解。这两个方法的实现如下:
public void registerAliases(String packageName, Class> superType) {
ResolverUtil> resolverUtil = new ResolverUtil<>();
//查找指定包下的superType类型类
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set>> 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);
}
}
}
public void registerAlias(Class> type) {
String alias = type.getSimpleName(); //类型的简单名称(不包含报名)
//读取Alias注解
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
//检测次别名不存在后,会将其记录到TYPE_ALIASES集合中
registerAlias(alias, type);
}
本文由 Janker 创作,采用 CC BY 3.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。