类型转换
1. 概述
JDBC数据类型与Java语言中的数据类型并不是完全对应的,所以在PreparedStatement
为SQL
语句绑定参数是,需要从Java
类型转换成JDBC
类型,而从ResultSet
中获取数据时,则需要从JDBC
类型转换成Java
类型。Mybatis
使用类型转换器完成上述两种转换。据图如下图所示:
在Mybatis
中使用JdbcType
这种枚举类型代表JDBC
中的数据类型,该枚举类型中定义了TYPE_CODE
字段,记录了JDBC
类中在java.sql.Types
中相应的常量编码,并通过一个静态集合codeLookUp(HashMap<Integer,JdbcType>类型)
维护了常量编码与JdbcType
之间的对应关系。
2. TypeHandler
Mybatis中所有的类型转换器都继承了TypeHandler
接口,在TypeHandler
接口中定义了如下四种方法,这四种方法分为两类:
setParameter()
方法- 负责将数据由
Java
类型转换成JdbcType
类型
- 负责将数据由
getResultSet()
方法- 负责将数据由
JdbcType
类型转换成Java
类型
- 负责将数据由
package org.apache.ibatis.type;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 类型处理器
* 说白了typeHandlers就是用来完成javaType和jdbcType之间的转换
* @author Clinton Begin
*/
public interface TypeHandler<T> {
/**
* 通过PreparedStatement为Sql语句绑定参数是,会将数据从Java类型转换成JdbcType类型
* @param ps
* @param i 转换第几个参数
* @param parameter 参数
* @param jdbcType 要转换的jdbcType的类型
* @throws SQLException
*/
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
/**
* 从ResultSet中获取数据时会调用此方法,将数据有JdbcType类型转换为Java类型
* @param columnName Colunm name, when configuration <code>useColumnLabel</code> is <code>false</code>
*/
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
中实现了setParameter()
和getResult()
方法,具体如下所示。
/**
* 在设置参数的时候,只处理为null的数据,不为空的数据都交给了子类实现
* @param ps
* @param i 转换第几个参数
* @param parameter 参数
* @param jdbcType 要转换的jdbcType的类型
* @throws SQLException
*/
@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);
}
}
}
public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
@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);
}
}
/**
* 3.5.0版本之后getResult方法,不管是空还是非空数据都要交给子类去处理
* @param columnName Colunm name, when configuration <code>useColumnLabel</code> is <code>false</code>
*/
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;
需要注意的是:在Mybatis3.5
及其之后版本,BaseTypeHandler.setParemeter()
只处理空参,非空参数交于子类处理,BaseTypeHandler.getResult()
不管是空值还是非空都交于子类处理,而Mybatis3.5
版本之前,BaseTypeHandler.setParemeter()
和BaseTypeHandler.getResult()
都是只对空参进行处理的。
BaseTypeHandler
的实现类是比较多的,但是实现比较简单。

这里以IntergerTypeHandler
为例简单介绍:
package org.apache.ibatis.type;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* @author Clinton Begin
*/
public class IntegerTypeHandler extends BaseTypeHandler<Integer> {
@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 {
//调用ResultSet.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)
//调用ResultSet.getInt获取指定列值
throws SQLException {
int result = cs.getInt(columnIndex);
return result == 0 && cs.wasNull() ? null : result;
}
}
一般情况下,TypeHandler
用于完成单个参数及其单个列值的类型转换,如果存在多列值转换成一个Java
对象的需求,应该优先考虑使用在映射文件中定义合适的映射规则(<resultMap>
节点)完成映射。
3. TypeHandlerRegistry
介绍完TypeHandler
接口及其功能之后,Mybatis
如何管理众多的TypeHandler
接口实现,如何知道何时使用哪个TypeHandler
接口实现完成转换呢?这是有本小节介绍的TypeHandlerRegistry
完成的,在Mybatis
初始化过程中,会为所有已知的TypeHandler
创建对象,并实现注册到TypeHandlerRegistry
中,有TypeHandlerRegistry
负责管理这些TypeHandler
对象。
下面先来看看TypeHandlerRegistry
中的核心字段的含义:
/**
* 记录jdbcType与TypeHandler之间的对应关系,其中JdbcType是一个枚举类型,它定义对应了的JDBC类型
* 该集合主要用于从结果集读取数据是,将数据从jdbc类型转换成Java类型
*/
private final Map<JdbcType, TypeHandler<?>> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);
/**
* 记录了Java类型向指定的jdbcType转换时,需要使用的TypeHandler对象。
* 例如:Java类型中的String 可能转换为数据库的char、varchar等多种类型,所以存在一对多关系
*/
private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
/**
* 未知类型TypeHandler
*/
private final TypeHandler<Object> unknownTypeHandler;
/**
* 记录了全部的TypeHandler的类型以及该类型相应的TypeHandler对象
*/
private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();
/**
* 空TypeHandler集合的标识
*/
private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();
/**
* 默认枚举类型处理器
*/
private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
3.1. TypeHandlerRegistry构造方法
TypeHandlerRegistry
构造中综合起来做了三步操作:
- 创建了一个
Configuration
,或者通过mybatis-config.xml
的初始化传入一个Configuration
到TypeHandlerRegistry
构造中; - 利用传入的
configuration
对象,创建一个UnknownTpyeHandler
,以备后续使用; - 注册一堆
Mybatis
为我们提供的默认的TypeHandler
。
3.2. 注册TypeHandler对象
TypeHandlerRegistry.register()方法实现了注册TypeHandler对象的功能,register()方法有多个重载,这些重载之间的调用关系如下图所示。
由上图可以看出,多数的register()方法最终会调用重载7完成注册功能,所以先分析重载7,该方法有三个参数分别是:
Type javaType
JdbcType jdbcType
TypeHandler<T> handler
/**
* 最终调用到的重载方法
* @param javaType 能够处理的Java类型
* @param jdbcType
* @param handler
*/
// 7
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
//检测是否明确指定了TypeHandler能够处理的Java类型
if (javaType != null) {
//获取指定Java类型在typeHandlerMap集合中对应的TypeHandler集合
Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
//如果map为空或者等于NULL_TYPE_HANDLER_MAP,创建新的TypeHandler集合,并添加到typeHandlerMap中
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<>();
typeHandlerMap.put(javaType, map);
}
//将TypeHandler对象注册到typeHandlerMap集合中
map.put(jdbcType, handler);
}
//向allTypeHandlersMap集合注册TypeHandler类型和对应的TypeHandler对象
allTypeHandlersMap.put(handler.getClass(), handler);
}
3.3. 查找TypeHandler
介绍完注册TypeHandler
对象的功能之后,再来介绍TypeHandlerRegistry
提供的查找TypeHandler
对象的功能。
4. TypeAliasRegistry
在编写SQL
语句时,使用别名可以方便理解以及维护,例如表名或列名很长时,我们一般会为其设计易懂易维护的别名。Mybatis
将SQL
语句中的别名的概念进行了延伸和扩展,Mybatis
可以为一个类添加一个别名,之后就可以通过别名引用该类。
Mybati
通过TypeAliasRegistry
类完成别名的注册和管理功能,TypeAliasRegistry
的结构比较简单,它通过typeAliases
字段(Map<String, Class<?>>
类型)管理别名与Java类型之间的对应关系,通过TypeAliasRegistry.registerAlias()
方法完成注册别名,该方法的实现如下所示:
/**
* 注册类型别名
* @param alias 被注册类的别名
* @param value 被注册的类
*/
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
还有很多registerAlias()
的重载,如下图所示:
其中还有两个重载需要我们注意:
扫描包的
/** * 扫描指定包下面所有的类,并为其类的子类添加别名 * @param packageName * @param superType */ public void registerAliases(String packageName, Class<?> superType) { ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>(); resolverUtil.find(new ResolverUtil.IsA(superType), packageName); Set<Class<? extends Class<?>>> 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); } } }
尝试读取
@Alias
注解的public void registerAlias(Class<?> type) { //获取类的简单名称,不包括包名 String alias = type.getSimpleName(); //读取Alias注解 Alias aliasAnnotation = type.getAnnotation(Alias.class); if (aliasAnnotation != null) { alias = aliasAnnotation.value(); } registerAlias(alias, type); }
在TypeAliasRegistry
的构造方法中,默认为Java的基本类型及其数组类型、基本类型的包装类型以及数组类型、Data
、BigDecimal
、ResultSet
等类型添加了别名,可以参考Mybatis
官网文档(typeAliases模块)。
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!