TypeHandler
接口定义了对传入参数和返回结果的处理。
1. 概述
相比较于Spring,Tomcat,Log4j2等源码,Mybatis的源码还是相对简单一些的。
Mybatis源码中的注释相比较其他框架而言还是相当少的,但是其类和方法的命名都非常贴切地表明了其职责和将要完成的任务,所以理解起来并没有过多的障碍。而且Mybatis的出现是建立在JDBC之上;对JDBC的了解越深入,相对应的对MyBatis的理解也会越轻松。
2. 传统的JDBC操作
让我们先来看一下原生的JDBC编程,正如我们上面说过的,对JDBC整体流程的理解越深刻,对Mybatis的设计理念也就越清晰。注意下面代码中的注释
public List<Student> selectByCondition(Student e) throws SQLException {
List<Student> students = new ArrayList<Student>();
String sql = "select * from student where sage>? and sage<? ";
// conn为 java.sql.Connection类型
// 注意这里的变量值类型,正好和上面setParameter方法的第一个参数对应
PreparedStatement pst = conn.prepareStatement(sql);
// 查看PreparedStatement接口里定义方法,就会发现其实类似setXxx的方法占据了至少80%,而在实际的源生JDBC调用时,我们很难确定应该调用其中的哪个方法。
// 所以这里Mybatis使用了一个非常巧妙的方法:将内部生成的PreparedStatement实例直接以方法参数的形式暴露给框架使用者,权限全部下放;
// 这样做的好处是最大化地将重复性代码抽取到了基类里的同时,也确保了子类有着足够的自由去完成自定义需求。
pst.setInt(1, e.getAgestart());
pst.setInt(2, e.getAgeend());
ResultSet rSet = pst.executeQuery();
Student student = null;
while (rSet.next()) {
student = new Student();
// 这里针对ResultSet的处理
// Mybatis对其的处理和上面的PreparedStatement类似,看看上面TypeHandler接口里定义的另外三个方法就会心有所感的
student.setId(rSet.getInt(1));
student.setName(rSet.getString(2));
student.setAge(rSet.getInt(3));
students.add(student);
}
return students;
}
上面这一组代码中,我们可以看到:代码基本可以认为是以ResultSet rSet = pst.executeQuery();
为界限; 以上基本是对PreparedStatement
的设置,以下则是对ResultSet
中值的提取,并赋值给相应的自定义对象。
3. Mybatis的应对之策
在看完上述传统的JDBC操作之后,接下来让我们看看Mybatis是如何进行精妙设计,以向框架使用者隐藏和消除这些枯燥到恶心的操作。
首先需要提及的是,Mybatis肯定不是仅仅只是依仗着一个TypeHandler
就完成了全部的工作。但TypeHandler
绝对是其中重要的一环。并且正如标题提及的,本文的中心是TypeHandler
,所以其他的相关类我只会在必要的时候进行提及。
3.1 TypeHandler
接口定义
我们首先来看看这个TypeHandler
接口的定义,其一共定义了四个方法,除了第一个方法setParameter
是对数据库操作前的参数进行处理之外,另外三个都是在数据库操作完成后对返回值的提取。
// T为 jdbcType 对应的 javaType的类型
public interface TypeHandler<T> {
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
T getResult(ResultSet rs, String columnName) throws SQLException;
T getResult(ResultSet rs, int columnIndex) throws SQLException;
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
3.2 TypeHandler
接口的应用
按照我们上面对接口里所声明的方法的研究,我们应该大致能猜测出TypeHandler
接口的主要应用场景:
数据库操作前的参数处理时。对应到Mybatis中就是
ParameterHandler
接口(唯一实现类DefaultResultSetHandler
)。// DefaultParameterHandler.setParameters public void setParameters(PreparedStatement ps) throws SQLException { // 记录下本方法调用的上下文信息 ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); // 开始本次SQL执行之前, 针对每个参数的处理 // 注意这里parameterMappings中每个Item的顺序, 就是SQL中 ? 的顺序 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 value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { // 如果该类型有相应的typeHandler, 则剩下的参数设置逻辑全部转向typeHandler // 所以在使用 #{}时,如果发现传入的类型有相应的TypeHandler,Mybatis就会直接选择回调TypeHandler。不会在意你在 # {}中写入的属性名。 value = parameterObject; } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } // 获取针对本参数的typeHandler TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); // 获取在配置文件中的设置项jdbcTypeForNull if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull(); // 回调typeHandler的自定义实现 typeHandler.setParameter(ps, i + 1, value, jdbcType); } } } }
数据库操作完成后对返回值的提取。对应到Mybatis中就是
ResultSetHandler
接口(唯一实现类DefaultResultSetHandler
)。
4. TypeHandler
的注册
在本文的前面部分我们讨论了TypeHandler
是如何被应用在执行流程中,以减少传统JDBC编程里的重复性代码的。接下来就让我们看看Mybatis是如何接纳外界自定义TypeHandler
。
4.1 TypeHandler
的配置解析
对于自定义的TypeHandler
,我们一般是在mybatis-config.xml
进行如下配置:
<typeHandlers>
<typeHandler handler="zz.xx.typehandler.NullTypeHandler"/>
</typeHandlers>
针对以上配置,Mybatis是选择在XMLConfigBuilder类的typeHandlerElement方法进行处理的。
// XMLConfigBuilder.typeHandlerElement
private void typeHandlerElement(XNode parent) throws Exception {
// parent就是 <typehandlers>根节点
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 针对子节点<package name="packageName"/>, 简化注册操作
if ("package".equals(child.getName())) {
String typeHandlerPackage = child.getStringAttribute("name");
typeHandlerRegistry.register(typeHandlerPackage);
} else {
// 针对上文中的注册方式
String javaTypeName = child.getStringAttribute("javaType");
String jdbcTypeName = child.getStringAttribute("jdbcType");
String handlerTypeName = child.getStringAttribute("handler");
Class<?> javaTypeClass = resolveClass(javaTypeName);
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都被注册到了TypeHandlerRegistry
中。
4.2 TypeHandler
的容器TypeHandlerRegistry
关于这个TypeHandler
容器,我们首先需要关注如下几点:
1. 首先是其构造函数。Mybatis在package org.apache.ibatis.type
中定义的TypeHandler
实现类都在该构造函数中进行了相应的注册。
2. 然后就是三大类方法register,hasTypeHandler,getTypeHandler。具体的就不赘述了,名称已经很能说明情况。
5. 总结
TypeHandler
契约了自定义的对 参数 和 返回值 的处理方法。 将JDBC里的相关操作进行了尽可能的提取,而将自定义的需求以TypeHandler 的形式向外界暴露。- 针对参数进行处理的
ParameterHandler
和 针对返回值进行处理的ResultSetHandler
,它俩的实例化是在针对Statement进行处理的StatementHandler
的实现类BaseStatementHandler
的构造函数中完成的。 而作为JDBC操作的核心组件的Statement
是伴随着每次数据库操作重新生成的。所以相对应的ParameterHandler
和ResultSetHandler
的实现类也会相应的重新构建一份。 - Mybatis在对JDBC整体流程的深刻理解之上,抽取了尽可能多的重复性代码由框架来完成,在此基础上又尽量保证了灵活性。本文的
TypeHandler
就是一个非常好的例子。 - 并且Mybatis也给了
TypeHandler
足够的地位,专门分配了顶级packageorg.apache.ibatis.type
。在该package中,除了相关辅助类外,绝大部分都是TypeHandler
的实现类,Mybatis预定义了一堆针对常见JDK类型的TypeHandler
。