MyBatis核心源码解析(一)
在前段时间,我们通过二十多个章节对SpringFramework的部分内容做了深度分析,并对核心部分的源码做了详细介绍,本章开始将对MyBatis的核心源码做深度分析,MyBatis学习将分为几个阶段,一是MyBatis单独源码做分析,二是对MyBatis扩展做分析,三是Spring整合MyBatis做分析,但是笔者在Spring的章节为了让大家能够理解Spring拓展的特性,已经对Spring整合MyBatis的核心设计思想做了深入的分析,因此有部分内容会简要介绍,只会针对其他一些整合特性做详细分析。
MyBatis框架搭建
一、mybatis.xml
<?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>
<!-- <plugins>
<plugin interceptor="com.SQLInterceptor">
<property name="better" value="better" />
</plugin>
<plugin interceptor="com.MyPageInterceptor">
</plugin>
</plugins>-->
<!-- 和Spring整合后environment配置都会被干掉 -->
<environments default="development">
<environment id="development">
<!-- 使用jdbc事务管理,目前由mybatis来管理 -->
<transactionManager type="JDBC" />
<!-- 数据库连接池,目前由mybatis来管理 -->
<dataSource type="POOLED"><!--有关于mysql数据库的各种信息-->
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mybatis" />
<property name="username" value="root" />
<property name="password" value="root" />
</dataSource>
</environment>
</environments>
<mappers>
<!--将操作配置文件User.xml系添加进mapper-->
<mapper resource="UserMapper.xml" />
</mappers>
</configuration>
二、UserMapper.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="com.UserMapper"> <!-- 注意,因为这边没有用到mapper接口,所以这里的namespace不需要是完全的类名 -->
<!-- 通过id查询用户 -->
<select id="findUserById" parameterType="int" resultType="com.User">
select * from user where id = #{id}
</select>
</mapper>
三、Mapper接口
package com;
import java.util.List;
public interface UserMapper {
List<User> findUserById(int id);
}
四、实体类
package com;
public class User {
private int id;
private String name;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
五、测试类
import com.User;
import com.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.stream.Collectors;
public class Test {
public static void main(String[] args){
/* ClassPathXmlApplicationContext applicationContext =new ClassPathXmlApplicationContext("spring-config.xml");
UserMapper userMapper= (UserMapper) applicationContext.getBean("userMapper");
userMapper.findUserById(1);*/
try {
InputStream inputStream= Resources.getResourceAsStream("mybatis.xml");
// 创建sqlSessionFactory
SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
// 根据sqlSessionFactory获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 这里采用了两种方式,一种是通过接口的方式,另外一种是直接使用sqlSession对象调用对应的方法
List<User> users= sqlSession.selectList("findUserById",2);
for (User user : users) {
System.out.println("Age : "+user.getAge()+"; Name : "+user.getName()+
"; Id : "+user.getId());
}
System.out.println("---------------------------");
UserMapper userMapper=sqlSession.getMapper(UserMapper.class);
List<User> list = userMapper.findUserById(1);
for (User user : list) {
System.out.println("Age : "+user.getAge()+"; Name : "+user.getName()+
"; Id : "+user.getId());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
然后sql数据
运行结果
"C:\Program Files\Java\jdk1.8.0_201\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2018.3.5\lib\idea_rt.jar=53209:C:\Program Files\JetBrains\IntelliJ IDEA 2018.3.5\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_201\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_201\jre\lib\rt.jar;C:\Users\HP\IdeaProjects\Spring\Mybatis\target\classes;C:\Users\HP\.m2\repository\org\springframework\spring-webmvc\5.1.3.RELEASE\spring-webmvc-5.1.3.RELEASE.jar;C:\Users\HP\.m2\repository\org\springframework\spring-beans\5.1.3.RELEASE\spring-beans-5.1.3.RELEASE.jar;C:\Users\HP\.m2\repository\org\springframework\spring-context\5.1.3.RELEASE\spring-context-5.1.3.RELEASE.jar;C:\Users\HP\.m2\repository\org\springframework\spring-core\5.1.3.RELEASE\spring-core-5.1.3.RELEASE.jar;C:\Users\HP\.m2\repository\org\springframework\spring-jcl\5.1.3.RELEASE\spring-jcl-5.1.3.RELEASE.jar;C:\Users\HP\.m2\repository\org\springframework\spring-expression\5.1.3.RELEASE\spring-expression-5.1.3.RELEASE.jar;C:\Users\HP\.m2\repository\org\springframework\spring-web\5.1.3.RELEASE\spring-web-5.1.3.RELEASE.jar;C:\Users\HP\.m2\repository\org\springframework\spring-aop\5.1.3.RELEASE\spring-aop-5.1.3.RELEASE.jar;C:\Users\HP\.m2\repository\org\springframework\spring-tx\5.1.3.RELEASE\spring-tx-5.1.3.RELEASE.jar;C:\Users\HP\.m2\repository\org\springframework\spring-test\5.1.3.RELEASE\spring-test-5.1.3.RELEASE.jar;C:\Users\HP\.m2\repository\org\springframework\spring-jdbc\5.1.3.RELEASE\spring-jdbc-5.1.3.RELEASE.jar;C:\Users\HP\.m2\repository\org\springframework\spring-aspects\5.1.3.RELEASE\spring-aspects-5.1.3.RELEASE.jar;C:\Users\HP\.m2\repository\org\aspectj\aspectjweaver\1.9.2\aspectjweaver-1.9.2.jar;C:\Users\HP\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.9.7\jackson-core-2.9.7.jar;C:\Users\HP\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.9.7\jackson-databind-2.9.7.jar;C:\Users\HP\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.9.7\jackson-annotations-2.9.7.jar;C:\Users\HP\.m2\repository\org\mybatis\mybatis-spring\2.0.0\mybatis-spring-2.0.0.jar;C:\Users\HP\.m2\repository\org\mybatis\mybatis\3.5.1\mybatis-3.5.1.jar;C:\Users\HP\.m2\repository\mysql\mysql-connector-java\5.1.47\mysql-connector-java-5.1.47.jar;C:\Users\HP\.m2\repository\com\mchange\c3p0\0.9.5.2\c3p0-0.9.5.2.jar;C:\Users\HP\.m2\repository\com\mchange\mchange-commons-java\0.2.11\mchange-commons-java-0.2.11.jar;C:\Users\HP\.m2\repository\org\mybatis\generator\mybatis-generator-core\1.3.5\mybatis-generator-core-1.3.5.jar;C:\Users\HP\.m2\repository\com\github\pagehelper\pagehelper\5.0.0\pagehelper-5.0.0.jar;C:\Users\HP\.m2\repository\com\github\jsqlparser\jsqlparser\0.9.5\jsqlparser-0.9.5.jar;C:\Users\HP\.m2\repository\com\alibaba\druid\1.1.14\druid-1.1.14.jar;C:\Users\HP\.m2\repository\jstl\jstl\1.2\jstl-1.2.jar;C:\Users\HP\.m2\repository\junit\junit\4.12\junit-4.12.jar;C:\Users\HP\.m2\repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar;C:\Users\HP\.m2\repository\cglib\cglib\3.2.5\cglib-3.2.5.jar;C:\Users\HP\.m2\repository\org\ow2\asm\asm\5.2\asm-5.2.jar;C:\Users\HP\.m2\repository\org\apache\ant\ant\1.9.6\ant-1.9.6.jar;C:\Users\HP\.m2\repository\org\apache\ant\ant-launcher\1.9.6\ant-launcher-1.9.6.jar" Test
Age : 123; Name : dasdada; Id : 1
---------------------------
Age : 23; Name : 32132321sdsda; Id : 2
Process finished with exit code 0
可以看到这两种方式都能实现,我们先关注第一种方式,也就是直接通过SqlSession这个接口去操作数据库,我们先看一下这个接口
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.apache.ibatis.session;
import java.io.Closeable;
import java.sql.Connection;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.executor.BatchResult;
public interface SqlSession extends Closeable {
<T> T selectOne(String var1);
<T> T selectOne(String var1, Object var2);
<E> List<E> selectList(String var1);
<E> List<E> selectList(String var1, Object var2);
<E> List<E> selectList(String var1, Object var2, RowBounds var3);
<K, V> Map<K, V> selectMap(String var1, String var2);
<K, V> Map<K, V> selectMap(String var1, Object var2, String var3);
<K, V> Map<K, V> selectMap(String var1, Object var2, String var3, RowBounds var4);
<T> Cursor<T> selectCursor(String var1);
<T> Cursor<T> selectCursor(String var1, Object var2);
<T> Cursor<T> selectCursor(String var1, Object var2, RowBounds var3);
void select(String var1, Object var2, ResultHandler var3);
void select(String var1, ResultHandler var2);
void select(String var1, Object var2, RowBounds var3, ResultHandler var4);
int insert(String var1);
int insert(String var1, Object var2);
int update(String var1);
int update(String var1, Object var2);
int delete(String var1);
int delete(String var1, Object var2);
void commit();
void commit(boolean var1);
void rollback();
void rollback(boolean var1);
List<BatchResult> flushStatements();
void close();
void clearCache();
Configuration getConfiguration();
<T> T getMapper(Class<T> var1);
Connection getConnection();
}
可以看到 这里的看到这个sqlsession接口包含了我们所需要的所有操作数据库的功能,没错这个接口是MyBatis的核心类,它就是操作数据库的核心,好了,现在我们来跟踪一下源码,首先第一步是
// 创建sqlSessionFactory
SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
其次我们主要关注build方法,点进去
然后再跟踪
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
这个XMLConfigBuilder 我们可以简单理解成一个xml管理容器,这里对XMLConfigBuilder 进行实例化,随后执行 return build(parser.parse());这里我们就要注意了,先进入parse()方法
然后跟踪parseConfiguration(parser.evalNode("/configuration"));
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
从名字我们应该可以看出,这里就是对Mybatis的配置文件进行解析,这里包含了properties、plugins等等,其中包含了environments和mappers,这里我们回到xml配置文件就知道了
通过配置文件我们就能够对应上这里到底是什么,我们将代码跑完,然后看看这个root到底扫描了那些东西
将这个root内容复制出来
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="UserMapper.xml"/>
</mappers>
</configuration>
可以看到这里就解析出来了我们配置的信息,好了,这里我们再回到mapperElement(root.evalNode("mappers"));进入这个方法
注意这里的mapper,然后前面都是一些取值、赋值,不用管,直接找到
其实大家也猜得到它要干嘛了,既然已经知道了Mapper类以及它的一些基本信息,那么久开始对它的基本信息解析了,里面其实也就是对我们的Mapper解析并绑定在一个Configuration类里面,这里就不继续跟踪了,我讲部分重点截图下来
那么对到我们的test类,
// 创建sqlSessionFactory
SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
这里的sqlSessionFactory 我们就创建成功了,现在我们再来看第二句代码
// 根据sqlSessionFactory获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
然后我们跟踪进去
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
再跟踪进去
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
这里会根据我们前面定义的Configuration去获取一个Environment
这里就是我们配置的数据源的Environment标签,通过数据源创建事务,然后创建了Executor,我们先不关注这个Executor是干什么的,继续看代码,最后就带着这些创建的对象返回了一个SqlSession,好了我们获取了SqlSession,现在我们来看我们的SqlSession能做什么事情,
其实可以看到,它基本上包含了所有执行数据库的方法,也就是说我们拿到了SqlSession,我们就可以操作数据库。下面我们看实例代码。
// 这里采用了两种方式,一种是通过接口的方式,另外一种是直接使用sqlSession对象调用对应的方法
List<User> users= sqlSession.selectList("findUserById",2);
我们调试进去
然后这里会调到
然后这里会知道我们对应的我们初始化的mapper
关键的是下一段代码,
executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
可以看到这里出现了executor,那么我们既然出现了它,就说明Sqlsession它只是通过执行器代理做事,真正做事的是执行器,我们再进一步深入
然后先是拼接sql,然后再是创建缓存,然后执行到
query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
我们在深入进去,
再次获取缓存是否存在,如果不存在就调用查询方法
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
前面都是做一些缓存工作,我们直接看到
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
进入后调用
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
这里MyBatis自己封装了一个StatementHandler,而这个类其实才是真正底层操作数据库的,我们一直调用下去最终我们会
可以看到这里的PreparedStatement,这里的就是java.sql的api了,就不做细节分析,然后我们进入resultSetHandler.handleResultSets(ps);然后我们一直深入找到下面这个类,请注意,此时我们已经执行了sql了,现在是要将数据库返回的东西转换成我们的对象,
然后发现此时的数据并没有赋值,我们执行下去
也就是说foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;这个方法十分的关键。
然后看到第一段代码List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);我们进入找到这段代码
可以看到,这里出现了TypeHandler,我们看看这个接口
可以看到Mybatis自己实现了一套数据转换的处理器,我们在Spring中也提到过数据转换,那么为什么 Mybatis要搞一套自己的转换器呢?这道理也很简单,因为我们的sql字段和java类型差距较大,所以不能直接用Spring提供的转换器,因此Mybatis就自己实现了一套转换机制。那么这里是如何选择自己的转换器的呢?
这里也是根据参数类型直接返回,有了这个转换器就可以直接实现数据转换了,就可以解答我刚才提出的问题。好了,我们现在对Mybatis的第一种获取方式做了详细的介绍
List<User> list = userMapper.findUserById(1);
for (User user : list) {
System.out.println("Age : "+user.getAge()+"; Name : "+user.getName()+
"; Id : "+user.getId());
}
这种Mapper方式就在下个章节续写吧。