用了几年的Mybatis,但是一直没有时间去研究下这个框架,当然这段时间也找了事件大概的看了一遍Mybatis主要功能的源码。
总体上感觉Mybatis 属于小巧功能却十分强大的框架,个人以为Mybatis应该作为Java初学者第一个阅读的框架源码。有兴趣可以跟着博主思路读读源码>-<
本文将以以下几个问题展开:
- Mybatis 运行机制是怎样的?
- Mybatis 初始化中,如何获取SqlSessionFactory的?
- Mybatis 中,各大组件是通过怎样的形式串起来的?
- JDBC 的几种事务级别都各是什么?
- 创建 SqlSession时候,有哪些参数可以传?
- Mybatis有哪些事务类型?
简介
MyBatis 是一款基于Java的优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录。
使用上官网并没有提供太多samples,不过提供了很多配置意思,这个可以帮助我们在一些情况下优化代码及减少一些bug。
https://mybatis.org/mybatis-3/zh/index.html
例子
以API 方式作为入口去了解框架运行原理是一种非常好的方式,本文例子以官网例子变形作为分析的起点:
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.getByIndex(11);
log.info("user :{}", user);
}
}
相关xml配置:
<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://127.0.0.1/df?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mappers/UserMapper.xml"/>
</mappers>
</configuration>
具体例子可以看 https://github.com/anLA7856/mybatislearn
通过InputStream 获取到指定输入流的xml配置文件后,则进入了 SqlSessionFactory
的表演时间。
SqlSessionFactory
由 SqlSessionFactoryBuild
构建,SqlSessionFactoryBuild
中有多个不同的build方法,主要有三种类型参数:
- Reader
- String enviroment
- Properties properties
- InputStream
- Configuration
InputStream
、Reader
指传入是字节流还是字符流去解析不同格式的输入流信息,而 enviroment
、properties
则是传入解析的具体配置,可以指定在xml中只返回哪个环境(enviroment) 的配置,或者完全覆盖属性(properties)。而Configuration则是Mybatis的核心对象,整个解析过程出的数据都会放入到Configuration中。
在选择完方法之后,Mybatis就会进入到自己实现的XML解析类XMLConfigBuilder
中,通过XPathParser
作为具体解析器,将解析出的xml存储在org.w3c.dom.Document
中:
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);
this.document = createDocument(new InputSource(inputStream));
}
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
在 XMLConfigBuilder
父类 BaseBuilder
中,有三个变量:
- Configuration configuration:Mybatis中核心配置中心
- TypeAliasRegistry typeAliasRegistry:别名配置中心,以
Map<String, Class<?>>
存储Mybatis配置别名和类对象映射,例如:registerAlias("byte", Byte.class);
- TypeHandlerRegistry typeHandlerRegistry:类型处理配置中心,即值该怎样传递给JDBC,又怎样从JDBC中拿出来?以
Map<JdbcType, TypeHandler<?>>
存储。例如Java对象String,对应数据库可以有多种数据类型varchar, longvarchar, nchar
等。二者交互就是通过TypeHandler来处理的。
当 通过 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
解析后,开始由 build(parser.parse());
将解析后xml依据Mybatis需要组装起来。
Configuration
整个SqlSessionFactory
初始化过程,在我看来一句话描述就是:
Mybatis通过 其内部定义的强大的XML解析,将配置的xml文件和Mapper文件全部解析成为Java类对象形式,并且以Configuration为中心。
当然里面的配置以及存储对象很多。
根据上面源码一路往下,进入到XMLConfigBuilder
的parseConfiguration
方法:
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);
}
}
上面代码中,从xml配置中解析了configuration
节点内容,而后根据节点对Configuration 相关属性进行填充:
- 读取
properties
内容,将其放入到Configuration
的Properties variables
中。 - 读取
settings
内容,如果有则放到Configuration
的Class<? extends VFS> vfsImpl
,并且会加入到VFS
中VFS.addImplClass(this.vfsImpl);
VFS
是mybatis提供的一个接口,用于在服务器中访问资源,即可以利用vfs访问内部文件 - 读取
settings
内容,读取logImpl
配置,将 其设置到Configuration
的logImpl
,以及使用 初始化 LogFactory 的用户配置日志工厂LogFactory.useCustomLogging(this.logImpl);
。 - 读取别名配置,嚷道
Configuration
的TypeAliasRegistry
中。 - 读取 拦截器配置,放入
Configuration
的InterceptorChain
- 配置
ObjectFactory
,并放入Configuration
,Mybatis将会用它来创建所有的需要的Object
。 - 配置
ObjectWrapperFactory
,Mybatis没有默认实现。博主暂时没发现其用途,因为Mybatis默认实现比较简单,hasWrapperFor
返回false,而getWrapperFor
则抛出了异常。 - 配置
ReflectorFactory
,用于作用反射工厂 - 读取并存储所有
settings
节点内容到Configuration
的不同配置中 - 配置事务工厂
TransactionFactory
以及数据源工厂DataSourceFactory
- 配置
databaseIdProvider
,同样存储到Configuration
中 - 配置
typeHandlers
,放入到Configuration
中 - 配置
mappers
节点,加载所有的mappers的xml配置。
- 使用
XMLMapperBuilder
加载 对应mappers配置 - 使用
Set<String>
来存储已经加载过的配置 - 使用
MapperBuilderAssistant
保存 缓存,或者使用别处或者当前自己缓存,以命名空间id为标识,或者是ParameterMapping
,最后交由的都是Configuration
保管。 - 解析
cache、/mapper/parameterMap、/mapper/resultMap、/mapper/sql、select|insert|update|delete
等内容 - 解析完成后,存储
Set<String>
类型的loadedResources
和 将 MapperRegistry 存入mapperRegistry
中。
SqlSession 初始化过程
读取完配置,构建完成 SqlSessionFactory
,就到了SqlSession
的构造事件,SqlSessionFactory
提供了以下几种获取方法:
public SqlSession openSession()
public SqlSession openSession(boolean autoCommit)
public SqlSession openSession(boolean autoCommit)
public SqlSession openSession(ExecutorType execType)
- …
openSession基于三个参数的多个重载方法: - ExecutorType:执行方式,分为
SIMPLE, REUSE, BATCH
- TransactionIsolationLevel level:设置事务级别
NONE、READ_COMMITTED、READ_UNCOMMITTED、REPEATABLE_READ、SERIALIZABLE
- autoCommit:设置自动提交,分
true
和false
- ExecutorType,默认是 SIMPLE,
SIMPLE
:即每次用完Statement
后,都会关闭Statement
,而后下次由重新打开REUSE
: 会将Statement
存储到Map<String, Statement>
中,key
是boundSql.getSql()
BATCH
:这种执行方式,主要用于批量操作,每次执行将statement预存到有序集合,主要用于循环或者多次执行构建一个存储过程或批处理过程
- TransactionIsolationLevel,事务级别,对应jdbc规范中的集中食物级别
NONE
:没有事务,对应 JDBC中Connection.TRANSACTION_NONE
READ_COMMITTED
: 禁止脏读,允许不可重复读和幻读。读已提交的,禁止事务读未提交行。会对相应的行加索,如果是范围读取例如i>100,InnoDB使用间隙锁或下一键(间隙加索引记录)锁来锁定扫描的索引范围,以阻止其他会话插入范围所涵盖的间隙。(会出现不可重复读,例如此时其他session commit了一次操作,那么当前事务还是能够读取到)READ_UNCOMMITTED
:允许脏读,不可重复读和幻读。一个事务可以读取到另一个事务未提交的行(脏读)。如果另一个事务回滚,则当前事务则会读到一个不存在的行(风险)。非锁定方式。REPEATABLE_READ
:禁止脏读和不可重复读,允许幻读。这是Innodb默认事务级别。不允许出现以下情况:
i:事务读取一个未提交的更改行
ii:同样不允许当一个事务读取行而另一个事务尝试修改行
iii:禁止一个事务两次读取获取不同结果(不可重复读)
理解上来说,当有A,B事务,当B事务设为REPEATABLE_READ
时,那么B这个事务中,不管查询多少次某一个内容,都是一样的。
无论是否其他事务对其已经做了提交性的更改。Innodb对其处理是后面查询都只读第一次的快照。(这样会出现幻读)SERIALIZABLE
:禁止脏读,不可重复读和幻读。最严格的。- Mysql可以使用
select @@tx_isolation;
查询当前事务隔离级别。 - 设置事务隔离级别:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
- 如果自动提交,那么每一行语句都会自动提交,Mysql中查看方式为:
select @@autocommit;
,关闭当前session的自动提交为:
set autocommit = off;
下面看 DefaultSqlSessionFactory
的 openSessionFromDataSource
。
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();
}
}
上面代码有以下几种意思:
- 通过Environment获取事务类型,Mybatis有两种事务提供:
- JdbcTransaction,使用Jdbc的事务控制,就是上面讲的四种事务隔离级别,并且直接通过使用JDBC的commit和rollback功能,延迟获取连接知道有需要。当设定了
autocommit
,那么会忽略commit
和rollback
- ManagedTransaction:让容器管理整个事务的生命周期,延迟获取连接。忽略所有的 commit和rollback请求。
- 在
newExecutor
中,使用事务创建一个执行器,默认的ExecutorType
为SIMPLE
类型,如果没有更改默认开启的二级缓存,则会将所有的执行器都包装在一个
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
CachingExecutor
里面。
- 一级缓存是指 在单个SqlSession中的缓存,可以在
- 二级缓存是可以跨SqlSession 共享的缓存
- Mybatis中获取操作为 二级缓存->一级缓存->数据库
- 其实最好不要在Mybatis中开启缓存,使用它作为纯粹的ORM框架就好了
而后,会将所有配置的过滤器串成链:
executor = (Executor) interceptorChain.pluginAll(executor);
遍历所有interceptors,而后拿到属于 Executor
的过滤器链,并封装返回:
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
- 将获取的Executor 包装称为一个
DefaultSqlSession
并返回。
整个 SqlSessionFactory 初始化过程 和SqlSession 初始化过程 已分析完。
下篇文章将开始围绕Mysql插件以及研究Mysql一级、二级缓存相关>-<
觉得博主写的有用,不妨关注博主公众号: 六点A君。
哈哈哈,一起研究Mybatis: