用了幾年的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: