TypeHandler被称作类型处理器,MyBatis在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时,都会用类型处理器将Java对象转化为数据库支持的类型或者将获取到数据库值以合适的方式转换成 Java类型。
mybatis提供了31个默认的类型处理器,它们都位于org.apache.ibatis.type
包下,这些默认的处理器能够满足我们大部分场景的需求。
本文接下来首先介绍TypeHandler接口的定义,之后介绍其在mybatis中如何使用,最后介绍一些mybatis如何注册TypeHandler。
一、TypeHandler
TypeHandler在mybatis中定义为接口,先来看一下接口代码:
public interface TypeHandler<T> {
//用于将java类型设置到预处理语句中
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
//将数据库的返回结果转换为java类型
T getResult(ResultSet rs, String columnName) throws SQLException;
//将数据库的返回结果转换为java类型
T getResult(ResultSet rs, int columnIndex) throws SQLException;
//将数据库的返回结果转换为java类型,该方法在存储过程里面使用
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
通过接口中的方法很容易知道,TypeHandler可以将java类型设置到PreparedStatement中,或者将数据库字段值转换为java类型。
二、TypeHandler使用原理
在这一小节里面,首先介绍TypeHandler是如何将java类型设置到PreparedStatement中。注意只有PreparedStatement才会使用到TypeHandler。
mybatis通过Executor执行器执行SQL语句,执行器首先创建PreparedStatement对象,PreparedStatement使用PreparedStatementHandler封装。接下来,mybatis调用
PreparedStatementHandler.setParameters()方法向PreparedStatement设置参数。
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
//获取PreparedStatement中需要设置的参数信息,里面包含了参数的java类型和数据库类型
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
//遍历每个参数
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();//获取参数名
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
//该分支用于处理mapper方法的入参为XXXExample对象
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
//parameterObject表示被调用mapper方法的入参,
//如果参数有多个,则parameterObject为Map对象,
//如果入参是一个,则parameterObject为该入参对象,如果没有入参,则parameterObject=null
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
//如果参数类型有对应的TypeHandler,则下面使用TypeHandler处理该参数
value = parameterObject;
} else {
//如果上面的分支都不符合要求,那么说明parameterObject是一个POJO,下面根据参数名从parameterObject获取对应的值
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);//可以根据属性名从入参对象中获取值
}
//TypeHandler在处理SQL语句的时候就已经找到了合适的,每个参数都有一个对应的TypeHandler
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();//一般在参数为null的时候,jdbcType才有用
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
//调用TypeHandler向PreparedStatement中对应的的位置设置参数
//一般的,最后的入参jdbcType没有用处
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
setParameters()方法遍历PreparedStatement中所有的参数信息,从mapper方法的入参得到每个参数的值,然后找到对应的TypeHandler ,调用TypeHandler.setParameter()设置参数值。
TypeHandler是在根据mapper方法入参处理SQL语句的时候,mybatis对每个参数进行匹配的,如果mapper文件中该参数设置了typeHandler,那么便直接使用typeHandler指定的类,如果没有设置,那么使用如下匹配规则查找合适的TypeHandler:
- 查找java类型,规则如下:如果mapper文件中配置了javaType,比如
create_time = #{createTime,javaType=java.util.Date,jdbcType=TIMESTAMP}
,那么使用配置值;如果mapper方法的入参是POJO,那么查找对应getter方法,该方法的返回类型就是java类型;如果mapper方法的入参有符合类型要求的TypeHandler,那么入参类型就是java类型;如果上面这些找不到java类型,那么默认java类型为Object; - 查找数据库类型,如果mapper文件中配置了jdbcType,比如
create_time = #{createTime,jdbcType=TIMESTAMP}
,那么数据库类型使用配置的类型,如果没有配置,那么为null; - 找到了javaType和数据库类型后,根据这两个值调用TypeHandlerRegistry.getTypeHandler():
//第一个入参是javaType,第二个入参是数据库类型,可能为null
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
//首先根据javaType找到所有数据库类型与TypeHandler的对应关系
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
TypeHandler<?> handler = null;
if (jdbcHandlerMap != null) {
//根据数据库类型再匹配
handler = jdbcHandlerMap.get(jdbcType);
if (handler == null) {
//如果没有找到,则认为数据库类型为null,再次查找
//一般这里找到的TypeHandler为UnknownTypeHandler
handler = jdbcHandlerMap.get(null);
}
if (handler == null) {
//如果还是没有找到,则检查jdbcHandlerMap中是否有多个TypeHandler类,
//如果是,则pickSoleHandler返回null,否则返回该TypeHandler类
handler = pickSoleHandler(jdbcHandlerMap);
}
}
// type drives generics here
return (TypeHandler<T>) handler;
}
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;
}
根据上面的匹配规则,如果最终没有找到合适的TypeHandler,那么mybatis会抛出IllegalStateException异常。
当使用jdbcType=null查找TypeHandler时,一般匹配到的是UnknownTypeHandler,UnknownTypeHandler在设置PreparedStatement的参数前,会根据mapper方法的入参类型和jdbcType再进行一次匹配,如果还是没有匹配到,那么便使用ObjectTypeHandler作为最终的ObjectTypeHandler。ObjectTypeHandler处理PreparedStatement的参数很简单,代码如下,调用PreparedStatement.setObject()即可。
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
throws SQLException {
ps.setObject(i, parameter);
}
PreparedStatement.setObject()内部会通过instance of判断这个参数到底是哪个类型的对象,从而调用相应类型的set方法。
下面介绍TypeHandler如何将数据库返回值转换为java类型。先介绍一下mybatis如何查找处理数据库返回值的TypeHandler。
在mybatis的mapper文件中,每个select节点都需要配置resultType或者resultMap(resultType和resultMap必须输入一个,否则抛异常),这两个属性指定了mybatis如何将数据库返回结果构造成java对象。这两个属性对查找TypeHandler的方式也是不一样的,resultType是在数据库返回结果之后才进行查找,而resultMap在启动的时候就已经匹配完毕。不过无论使用哪个属性,mybatis都是先解析出返回字段的javaType和jdbcType,之后也是调用TypeHandlerRegistry.getTypeHandler()方法定位出TypeHandler。
下面介绍一下mybatis针对resultMap和resultType如何查找TypeHandler。
- 当使用resultType时,javaType就是resultType属性指定的类型,jdbcType是根据数据库执行SQL后返回的元数据信息确定的;
- 当使用resultMap时,javaType的确定方式是:mybatis可以直接使用resultMap里面配置的javaType,如果没有配置javaType,那么从parameterType指定的类里面找到对应属性,属性的类型即为javaType,如果上述两种都没有找到,那么默认为Object;jdbcType的确定方式是:直接使用resultMap里面配置的jdbcType,如果没有配置jdbcType,那么默认为null。
根据上面的方式找到javaType和jdbcType后,之后就可以定位出TypeHandler了。定位出TypeHandler之后,调用下面两个方法就可以获得ResultSet中每个字段的值了。
T getResult(ResultSet rs, String columnName) throws SQLException;
T getResult(ResultSet rs, int columnIndex) throws SQLException;
三、注册TypeHandler
mybatis启动的时候,将所有的TypeHandler都注册到TypeHandlerRegistry中。TypeHandlerRegistry构造方法里面提供很多默认的TypeHandler,我们常用的类型都包括在内。
所有的TypeHandler都注册到TypeHandlerRegistry的TYPE_HANDLER_MAP属性中,该属性第一个Map的key是javaType,第二个Map的key是jdbcType。上一小节里面,匹配TypeHandler也是从TYPE_HANDLER_MAP中查询的。
private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP
除了使用默认的之外,我们还可以自定义TypeHandler,并使用配置文件或者自动配置的方式将TypeHandler注册到TypeHandlerRegistry中。下面介绍三种注册的方式。
1、使用包扫描注册
如果需要配置的类非常多,我们可以在配置文件中配置一个包名,比如下面的方式。mybaits会自动读取该包下面实现TypeHandler接口的类,并将这些类自动注册到TypeHandlerRegistry中。mybatis不仅扫描指定包下的类,还扫描子包里面的类。
<typeHandlers>
<package name="org.mybatis.example"/>
</typeHandlers>
mybatis每扫描到一个TypeHandler接口实现类之后,读取该类上的两个注解:MappedTypes
和MappedJdbcTypes
,前者指定了该实现类可以处理的java类型,也就是javaType,后者实行了该类可以处理的数据库类型,也就是jdbcType。这两个注解都可以指定多个类型。如果没有设置这两个注解,那就注册jdbcType和javaType都为null的类型处理器。
2、注册指定的TypeHandler
除了上面这种方式之外,还可以注册指定的TypeHandler,比如:
<typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
或者
<typeHandlers>
<typeHandler javaType="String" jdbcType="VARCHAR" handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
使用这种方式,mybatis自动加载handler指定的类,并将其注册。
如果配置文件中没有指定javaType和jdbcType,那么mybatis读取MappedTypes
和MappedJdbcTypes
注解,使用注解指定的信息注册。如果指定了,那么就以指定的信息为准。
3、自动配置
自动配置是借助spring boot完成的,可以在spring boot的配置文件使用如下配置指定TypeHandler的包路径,这个效果和在配置文件中配置包路径是一样的。不过,使用自动配置可以配置多个路径,中间使用“,”或者“;”分隔。
mybatis.typeHandlersPackage=org.apache.ibatis.type