本文debug mybatis源码版本:3.2.7
mybatis提供了JDBC和MANAGED两种事务管理,本文主要讨论的是JDBC事务管理方式。
如果对本文理解不够详细,请先看另一篇博文,模拟实现mybatis数据源和事务:《动态代理+ThreadLocal实现数据源及事务管理》。
本文主要讨论的内容:
1. mybatis JDBC事务的开始、提交、回滚和关闭
2. mybatis JDBC事务中connection的多线程安全
可以这么错觉的认为,使整个mybatis框架跑起来的是SqlSessionManager,他既是mybatis的“发动机”,也是mybatis的“方向盘”。但是这个类似乎已经被弃用了,被DefaultSqlSessionFactory和DefaultSqlSession替代了,但实际上他们几乎是平等的关系,近乎1=2的概念。我也正在积极寻找被弃用的原因,至少目前从调试框架的结果来看,这个类的确没有被使。但是这个类的思想对于更好的了解Factory和session还是很有帮助的。
mybatis的起点是SqlSessionFactoryBuilder,他通过builder方法加载mybatis-config.xml配置文件,从而使得整个框架获得“跑动“的环境条件。SqlSessionFactoryBuilder有9个builder方法,四个和字节流有关,四个和字符流有关,完全的一一对应,而所有和流有关的builder方法的最终落点是build(Configuration config),也就是说无论是字节流还是字符流,他们最终都是把读取xml配置文件的流输送给xml的解析对象XMLConfigBuilder,而XMLConfigBuilder对象的终极目的就是parse注入的流成为Configuration对象,而Configuration对象几乎将配置文件中的配置标签一一对应的解析成了与之一致的对象,他就是mybatis框架的配置中心。
package org.apache.ibatis.session;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.Properties;
import org.apache.ibatis.builder.xml.XMLConfigBuilder;
import org.apache.ibatis.exceptions.ExceptionFactory;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.session.defaults.DefaultSqlSessionFactory;
public class SqlSessionFactoryBuilder {
/**
* 所有的字符流builder的最终落点为该方法
* 最终将流builder方法传入的流注入到XMLConfigBuilder对象,并通过其parser方法将流解析成Configuration对象
* 解析生成的Configuration对象最终被传入最后一个builder方法构造SqlSessionFactory
*/
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
/**
* 所有字节流有关的builder方法的最终落点,该方法的作用和字符流一致,只是传入的流不同而已
*/
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.
}
}
}
/**
* 所有与流有关的builder方法的最终落点,传入一个由XMLConfigBuilder.parser生成的Configuration对象
* 最终通过Configuration对象生成SqlSessionFactory
* 实际上SqlSessionFactory有两个子类,一个是DefaultSqlSessionFactory,另一个就是SqlSesisonManager
* 下一节详解这两个对象
*/
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
总结:所有和流有关系的8个builder方法,只是为了把流注入XMLConfigBuilder对象,解析生成Configuration,从而从最终的builder(Configuration config)方法中构造出SqlSessionFactory(实际上是DefaultSqlSessionFactory对象,下一节详解)。
SqlSessionFactory是工厂,而SqlSessio n是会话,工厂意味着生产中心,而会话则意味着操作中心。
SqlSessionManager既实现了SqlSessionFactory规范,也实现了SqlSession规范,实际上工厂和会话除了SqlSessionManager之外,还各有一个默认的子类,分别是DefaultSqlSessionFactory和DefaultSqlSession(关系如下图所示)。他们三者之间的关系:SqlSessionManager通过装饰器模式获得了DefaultSqlSessionFactory的所有能力,即openSession的能力,通过SqlSessionManager获得的SqlSession实际上全部来自DefaultSqlSessionFactory;SqlSessionManager在构造的时候就通过JDK动态代理获得了DefaultSqlSession的代理对象,代理的内容是一个判断是否存在本地SqlSession的生成逻辑,进而也获得了DefaultSqlSession和数据库交互的所有能力。
因为被弃用的缘故,在此不再讨论起源码的思想,只是建议如果想要深入了解的话,可以配合DefaultSqlSessionFactory和DefaultSqlSession来一起学习,效果更甚!
而实际上整个mybatis框架中真正使用的是DefaultSqlSessionFactory和DefaultSqlSession,前者用于生产后者,即通过工厂openSession得到了DefaultSqlSession,而DefaultSqlSession封装了和数据库交互的所有逻辑,注意,是除了开启事务之外的所有逻辑。
---------------------------------------------------------
a. 事务开启的时机:事务到底在哪里开启?
b. select到底有没有开启事务?
c. 业务层中多个select到底的事务是怎么样的?
d. IUD操作事务在什么时候开启的?
e. 业务层中多个IUD操作的事务是什么样的?
........
---------------------------------------------------------
如果你想知道,请往下看。本处以insert操作为例进行进行讨论,insert示例源码如下,本节所有内容围绕这段示例代码展开。
SqlSession session = factory.openSession();
session.insert("test.insertUser", new User(null, "zhangsan", "123", "male", 26));
session.insert("test.insertUser", new User(null, "lisi", "123", "male", 26));
session.commit();
session.close();
如以上示例代码,进行了两次insert(首先明确,事务不是在openSession中开启),但是事务只开启了一次,如果你不想更深入的了解,可以直接看debug的日志验证逻辑(如下图debug日志截图),如果你想了解的更多,请往下看,为什么mybatis知道只在第一次IUD的时候开启事务?
CSDN的图片上传,我就艹了······日志节选信息如下所示。
2015-05-10 01:24:54,209 DEBUG JdbcTransaction:132 - Opening JDBC Connection
2015-05-10 01:24:54,647 DEBUG PooledDataSource:380 - Created connection 319977154.
2015-05-10 01:24:54,647 DEBUG JdbcTransaction:98 - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@131276c2]
2015-05-10 01:24:54,647 DEBUG insertUser:139 - ==> Preparing: insert into tbl_user(username, password, gender, age) values(?, ?, ?, ?)
2015-05-10 01:24:54,710 DEBUG insertUser:139 - ==> Parameters: zhangsan(String), 123(String), male(String), 26(Integer)
2015-05-10 01:24:54,725 DEBUG insertUser:139 - <== Updates: 1
2015-05-10 01:24:54,725 DEBUG insertUser:139 - ==> Preparing: insert into tbl_user(username, password, gender, age) values(?, ?, ?, ?)
2015-05-10 01:24:54,725 DEBUG insertUser:139 - ==> Parameters: lisi(String), 123(String), male(String), 26(Integer)
2015-05-10 01:24:54,725 DEBUG insertUser:139 - <== Updates: 1
2015-05-10 01:24:54,725 DEBUG JdbcTransaction:69 - Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@131276c2]
2015-05-10 01:24:54,756 DEBUG JdbcTransaction:120 - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@131276c2]
2015-05-10 01:24:54,756 DEBUG JdbcTransaction:88 - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@131276c2]
2015-05-10 01:24:54,756 DEBUG PooledDataSource:334 - Returned connection 319977154 to pool.
package org.apache.ibatis.session.defaults;
import java.sql.Connection;
import java.sql.SQLException;
import org.apache.ibatis.exceptions.ExceptionFactory;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.TransactionIsolationLevel;
import org.apache.ibatis.session.defaults.DefaultSqlSession;
import org.apache.ibatis.transaction.Transaction;
import org.apache.ibatis.transaction.TransactionFactory;
import org.apache.ibatis.transaction.managed.ManagedTransactionFactory;
public class DefaultSqlSessionFactory implements SqlSessionFactory {
/**
* 获取SqlSession的方法,转调private方法
*/
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
/**
* 实际上在openSession的时候并没有获取Connection,也没有开启事务(设置自动提交为false)
* 只是初始化了数据库操作的必须要对象(和mybatis的架构有关)
* 1. 根据配置文件,初始化了事务对象,但是没有开启事务
* 2. 根据调用的openSession方法,初始化了执行器,默认是SimpleExecutor执行器
* mybatis所有的事务控制(开启、提交、回滚、关闭)的最终落点都在Executor中
* 实际上,Executor不仅仅是事务控制的最终落点,实际上缓存、sql语句的生成也在这里,他才是真正的“发动机”
*/
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
//根据配置文件中配置的类型获得事务工厂:JdbcTransactionFactory和ManagedTransactionFactory
//事务工厂都很简单,主要逻辑落在事务(JdbcTranasaction和ManagedTransaction中)
//工厂的规范:org.apache.ibatis.transaction.TransactionFactory
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
//从事务工厂获取事务对象(JdbcTranasaction或ManagedTransaction)
//此处只是获取了事务对象,并没有开启事务,也没有获得连接
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//获得执行器对象,事务被注入到了执行器中,后续事务的所有操作最终落点都在执行器当中
//微观来看,执行器才是mybatis的“发动机”
final Executor executor = configuration.newExecutor(tx, execType);
//返回SqlSessionFactory对象
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();
}
}
}
本文主要讨论事务,
那么在openConnection的时候并没有获取连接,更没有开启事务,只是根据配置文件构建了事务对象,更关键的点是执行器的构造方法,构建出来的事务对象通过构造方法被注入到了执行器当中,这一点对后续mybatis的事务体系相当重要。
连接和事务并没有在openConnection中打开和开启,而是延迟到了第一次操作的时候。事务的开启,实际上是在执行器
package org.apache.ibatis.session.defaults;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.binding.BindingException;
import org.apache.ibatis.exceptions.ExceptionFactory;
import org.apache.ibatis.exceptions.TooManyResultsException;
import org.apache.ibatis.executor.BatchResult;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.result.DefaultMapResultHandler;
import org.apache.ibatis.executor.result.DefaultResultContext;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSession;
public class DefaultSqlSession implements SqlSession {
/**
* 数据库操作的顶级接口
*/
public int insert(String statement, Object parameter) {
return update(statement, parameter);
}
/**
* 最终,所有的IUD操作都落点在此
* 真正执行参数映射、sql解析、sql执行已经返回值映射的,还是执行器
* 最终开启事务的还是执行器,因此将转到执行器当中继续追踪
*/
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
}
下面开始进行事务开启的追踪,注意源码注释中的数字编号。
3. 回滚
4. 关闭
5. 多线程中Connection的安全保证机制
如果是SqlSessionManager,则是基于ThreadLocal,但是这种方式已经被抛弃了;SqlSessionManager被DefaultSqlSessionFactory和DefaultSqlSessoin替代了以后,现在是给予方法栈的多线程数据安全。