前言
在进行Mybatis的源码流程之前,我们首先应该知道mybatis的使用方法。
<?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">
<!--namespace=绑定一个对应的Dao/Mapper接口-->
<mapper namespace="com.stefan.dao.UserDao">
<!--select查询语句-->
<select id="getUserList" resultType="com.stefan.pojo.User">
select * from demo.user
</select>
</mapper>
<?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>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/demo?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
<environment id="test">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/demo?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<!--每一个Mapper.xml都需要在MyBatis核心配置文件中注册!-->
<mappers>
<mapper resource="com/stefan/dao/UserMapper.xml"/>
</mappers>
</configuration>
我们最常用的应该是上面这种方法,这样就可以很简单的读到数据库里面的内容并且把他们自动的封装成对象返回。
一、Mybatis核心类
我们首先来认识一下Mybatis里面的几个比较重要的类
1.Configuration类
1.这个类里面有非常多的内容,这里面封装了mybatis-config.xml的所有内容,比如 environment ,再有typeAliases,还有mappers等等内容。他还对mappers标签里面的内容进行了汇总。
Configuration还负责创建了mybatis其他的核心对象:newParameterHandler(),newResultSetHandler(),
newStatementHandler(),newExecutor()方法等等
2.MappedStatement类
MappedStatement并不是将每个mapper.xml文件封装为一个个对象,而是将每个文件内的每个CRUD标签单独封装成一个MappedStatement对象。标签里的属性就是MappedStatement类里面的属性。自然,这个对象里面的属性也和crud标签所对应。
值得注意的是,此类里面的id属性,是通过命名空间.id保证其唯一性的。还有 这里的StatementType可以是 STATEMENT, PREPARED,CALLABLE中的一种。默认是PrepareStatement。
自然,MappedStatement还对sql语句进行了封装,封装到了BoundSql这个类里面。
不止如此,MappedStatement中也包含有Configuration属性,也就是说,两者是双向关联的关系,所以能通过MappedStatement也能找到Configuration。
3.操作类对象
Mybatis里面其实封装了很多操作类对象,表面看上去是SqlSession来帮我们完成了一系列的操作,其实是SqlSession调用了其他的一些操作类对象。如下图
- Executor
这个类,通俗一点讲就是执行器,是整个Mybatis处理功能的核心。
1.增删改update 查 query
2.事务操作(提交回滚)
3.缓存相关操作
这个接口主要有三个实现类:
BatchExecutor:JDBC中的批处理的操作
SimpleExecutor:常用Executor,Mybatis推荐默认
ReuseExecutor:复用Statement
Configuration类里面对他的默认类型进行了定义
this.defaultExecutorType = ExecutorType.SIMPLE;
- StatementHandler
StatementHandler是Mybatis封装了JDBC的Statement,真正的Mybatis进行数据库访问操作的核心
功能:增删改查
StatementHandler也是接口,也有一下几个实现类
PreparedStatementHandler,SimpleStatementHandler(常用),CallableStattementHandler
二、Mybatis流程
1.我们知道,mybatis想要读取到xml里面的内容,就需要去解析。我们所知的解析xml的方式,主要是DOM,SAX和XPath三种。而mybatis中使用的就是第三种XPath。我们先用一个简单的示例来了解一下。
(1) 首先定义一个xml文件。
(2) 利用XPath解析
利用XPathParser的evalNodes方法,我们就可以讲xml解析,封装在一个XNode的集合里面进行使用。而里面所传的参数叫做XPath语法,大家可以自行百度,这里不再赘述。
2.下面我们一行一行来分析。
在这句上面,我们已经拿到了mybatis-config.xml的输入流,利用它来得到一个SqlSessionFactory对象,我们进入到这个方法。
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory var5;
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException var13) {
}
}
return var5;
}
我们很清楚的可以看到,它创建出一个XMLConfigBuilder 对象去进行解析xml,那么这个对象里就一定封装了XPathParser对象。
进入到parse方法。
public Configuration parse() {
if (this.parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
} else {
this.parsed = true;
this.parseConfiguration(this.parser.evalNode("/configuration"));
return this.configuration;
}
}
this.parser.evalNode("/configuration")这句代码呢就已经把我们的XML文件封装成一个XNode对象了,我们进入到parseConfiguration方法。
这里面就就很明显了,利用传过来的XNode对象,一个个的去解析各个标签,然后用各个方法将他封装给Configuration对象。值得注意的是,他还会在最后一句代码中去解析mappers标签,this.mapperElement(root.evalNode(“mappers”));,所以这就是为什么我们只需要读取mybatis-config.xml文件而不用去读取xxxDaoMapper.xml文件的原因。接下来我们进入这个方法。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
Iterator var2 = parent.getChildren().iterator();
while(true) {
while(var2.hasNext()) {
XNode child = (XNode)var2.next();
String resource;
if ("package".equals(child.getName())) {
resource = child.getStringAttribute("name");
this.configuration.addMappers(resource);
} else {
resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
XMLMapperBuilder mapperParser;
InputStream inputStream;
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
inputStream = Resources.getResourceAsStream(resource);
mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
inputStream = Resources.getUrlAsStream(url);
mapperParser = new XMLMapperBuilder(inputStream, this.configuration, url, this.configuration.getSqlFragments());
mapperParser.parse();
} else {
if (resource != null || url != null || mapperClass == null) {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
Class<?> mapperInterface = Resources.classForName(mapperClass);
this.configuration.addMapper(mapperInterface);
}
}
}
return;
}
}
}
这个方法,就是用来解析xxxMapper.xml的。
我们可以看到,mappers这个标签下面有两个标签,package和mapper,而mapper下面又有三个属性,所以package和吗,mapper肯定是要分开解析的。源码中也有体现。
再下面,就是对mapper的三个属性的解析,去拿到xxxDaoMapper.xml的资源了。但是,值得注意的是,他的判断语句。
也就是说,每一个mapper标签,都只能在这三个属性中任意存在一个,否则就会抛出异常。
那么,对于package标签来说,拿到内容之后,会把它封装给我们上面所提到的Configuration类。对于mapper标签来说,它会进一步的解析,通过mapperParser.parse();方法。我们进入。
熟悉的代码再一次出现,this.parser.evalNode("/mapper"),我们知道,这句代码就是把一个标签封装成一个XNode对象。传入到configurationElement方法中。
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace != null && !namespace.equals("")) {
this.builderAssistant.setCurrentNamespace(namespace);
this.cacheRefElement(context.evalNode("cache-ref"));
this.cacheElement(context.evalNode("cache"));
this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));
this.resultMapElements(context.evalNodes("/mapper/resultMap"));
this.sqlElement(context.evalNodes("/mapper/sql"));
this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} else {
throw new BuilderException("Mapper's namespace cannot be empty");
}
} catch (Exception var3) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + this.resource + "'. Cause: " + var3, var3);
}
}
这下就比较明了了,一步步的解析mapper标签里的属性,最后会把他们封装到Configuration中,最后返回到最初build方法的位置,接下来,Configuration已经封装了一切想到的东西,xml文件解析完成。
解析完成之后,会调用this.build(parser.parse());方法,去创建SqlSessionFactory。
很明显,这个SqlSessionFactory是一个DefaultSqlSessionFactory。创建完成之后,返回到我们最开始的位置。执行下一句代码,去创建SqlSession,我们进入到openSession方法。
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
DefaultSqlSession var8;
try {
Environment environment = this.configuration.getEnvironment();
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
Executor executor = this.configuration.newExecutor(tx, execType);
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
} catch (Exception var12) {
this.closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12);
} finally {
ErrorContext.instance().reset();
}
return var8;
}
openSession方法中又会调用这个方法,他会先处理一些关于事务的操作,最后创建一个Executor,也就是执行器,执行我们的crud操作。最后再创建一个DefaultSqlSession类型的SqlSession。然后返回。
前面的准备工作已经完成,下面就要开始干活了。我们知道,我们的UserDao是一个接口,那么他就一定要去创建代理对象。我们跟着源码去验证。DeBug进入。
一路进去之后,就到了这个方法。我们看到了一个熟悉的newInstance方法,我们进去之后。
首先创建一个MapperProxy对象,这个对象,它实现了InvocationHandler接口,我们知道,在newInstance创建代理的时候,代理对象调用处理程序的时候,就会去调用这个接口里的invoke方法。
然后下一步newInstance。
protected T newInstance(MapperProxy<T> mapperProxy) {
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}
熟悉的创建代理对象。返回代理对象。一路返回到getMapper方法。此时的UserDao就是一个代理对象了。
创建好代理对象之后,我们就可以去执行具体方法了。这里以查询所有用户的方法getUserList为例。
上面提到,调用方法的时候会直接进入invoke方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
if (method.isDefault()) {
return this.invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
MapperMethod mapperMethod = this.cachedMapperMethod(method);
return mapperMethod.execute(this.sqlSession, args);
}
try里面的语句只是进行一些健壮性的操作,比如被调用的方法是否是Object里面的方法,如果是就直接执行,没必要继续走下面的代码。如果不是就继续。首先创建一个MapperMethod的对象。我们有必要说明一下这个对象。
-
MapperMethod对象
里面有这样两个属性。SqlCommand和MethodSignature,这两个对象都是静态内部类
(1)SqlCommand类
这里面有两个属性,name属性其实就是namespace.id,用来唯一标识一个mapper或者说是sql语句。而type属性,就是表示这个sql是insert ,update,delete和select中的哪一个。(2)MethodSignature类
这里面的属性其实就是记录一些方法返回值和参数,比如是否是多值返回,是否返回空,是否返回一个map等等。
创建好MapperMethod对象之后,继续执行MapperMethod的executor方法。
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Object param;
switch(this.command.getType()) {
case INSERT:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
break;
case UPDATE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
break;
case DELETE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
break;
case SELECT:
if (this.method.returnsVoid() && this.method.hasResultHandler()) {
this.executeWithResultHandler(sqlSession, args);
result = null;
} else if (this.method.returnsMany()) {
result = this.executeForMany(sqlSession, args);
} else if (this.method.returnsMap()) {
result = this.executeForMap(sqlSession, args);
} else if (this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else {
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + this.command.getName());
}
if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
} else {
return result;
}
}
这下就比较明了了,根据type类型来选择调用sqlSession的方法,至于为什么insert,update,delete要和select分开,因为前三种类型调用的是sqlSession的update方法,而且也由于select的类型比较多,我们就以查询所有为例,自然是要调用this.executeForMany(sqlSession, args);方法。
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
Object param = this.method.convertArgsToSqlCommandParam(args);
List result;
if (this.method.hasRowBounds()) {
RowBounds rowBounds = this.method.extractRowBounds(args);
result = sqlSession.selectList(this.command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(this.command.getName(), param);
}
if (!this.method.getReturnType().isAssignableFrom(result.getClass())) {
return this.method.getReturnType().isArray() ? this.convertToArray(result) : this.convertToDeclaredCollection(sqlSession.getConfiguration(), result);
} else {
return result;
}
}
由于我们这里没有分页,就直接执行else里面的内容。
从Configuration中拿到MappedStatement,然后执行query。
继续query。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
this.flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, boundSql);
List<E> list = (List)this.tcm.getObject(cache, key);
if (list == null) {
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
this.tcm.putObject(cache, key, list);
}
return list;
}
}
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
然后再query。
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 (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
this.clearLocalCache();
}
List list;
try {
++this.queryStack;
list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
if (list != null) {
this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
--this.queryStack;
}
if (this.queryStack == 0) {
Iterator var8 = this.deferredLoads.iterator();
while(var8.hasNext()) {
BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
deferredLoad.load();
}
this.deferredLoads.clear();
if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
this.clearLocalCache();
}
}
return list;
}
}
这个方法中,就会去操作数据库了,queryFromDatabase方法。
进入到他的doQuery方法。
我们又看到一个熟悉的对象,Statement,这就是JDBC原生对象了。拿到statement然后执行sql。
执行完sql拿到结果封装到resultSetHandler中,一路返回,最终放到我们自己的list中,调用就结束了。