MyBatis 之原始碼淺讀
環境簡介與入口
記錄一下嘗試閱讀Mybatis原始碼的過程,這篇筆記是我一邊讀,一遍記錄下來的,雖然內容也不多,對Mybatis整體的架構體系也沒有摸的很清楚,起碼也能把這個過程整理下來,這也是我比較喜歡的一種學習方式吧
單獨Mybatis框架搭建的環境,沒有和其他框架整合
入口點的原始碼如下:
@Test public void test01() { try { this.resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml"); // 2. 建立SqlSessionFactory工廠 this.factory = new SqlSessionFactoryBuilder().build(resourceAsStream); // 3. 建立sqlSession // todo 怎麼理解這個sqlSession? 首先它是執行緒級別的,執行緒不安全, 其次它裡面封裝了大量的CRUD的方法 this.sqlSession = factory.openSession(); IUserDao mapper = this.sqlSession.getMapper(IUserDao.class); List<User> all = mapper.findAll(); for (User user : all) { System.out.println(user); } // 事務性的操作,自動提交 this.sqlSession.commit(); // 6, 釋放資源 this.sqlSession.close(); this.resourceAsStream.close(); } catch (IOException e) { e.printStackTrace(); } }
構建SqlSessionFactory
首先跟進這個,看看如何構建SqlSessionFactory
物件
this.factory = new SqlSessionFactoryBuilder().build(resourceAsStream);
這個SqlSessionFactoryBuilder
類的存在很簡單,取名也叫他構建器,Mybatis的官網是這樣解釋它的,這個類可以被例項化(因為它有且僅有一個預設的無參構造),使用它的目的就是用來建立多個SqlSessionFactory例項,最好不要讓他一直存在,進而保證所有用來解析xml的資源可以被釋放
所以跳過對這個構建器的關注,轉而看的build()
首先會來到這個方法,直接可以看到存在一個XML配置解析器,這其實並不意外,畢竟現在是MyBatis是孤軍一人,就算我們使用的是註解開發模式,不也得存在主配置檔案不是?
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. } } }
接著看看parser.parse()
,它裡面會解析主配置檔案中的資訊,解析哪些資訊呢? 原始碼如下: 很清楚的看到,涵蓋mybatis配置檔案中的所有的標籤
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
...
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
第一個問題: 解析的配置資訊存在哪裡呢? 其實存放在一個叫Configuration
的封裝類中, 這個上面的解析器是XMLConfigBuilder
這個封裝類的儲存者是它的父類BaseBuilder
繼續跟進build()
方法,原始碼如下:
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
小結: 至此也就完成了DefaultSqlSessionFactory()的構建,回想下,這個構建器真的太無私了,犧牲了自己,不僅僅建立了預設的SqlSessionFactory,還將配置檔案的資訊給了他
開啟SqlSession
建立完成SqlSession工廠的建立, 我們繼續跟進this.sqlSession = factory.openSession();
, 從工廠中獲取一個SqlSession
跟進幾個空殼方法,我們很快就能來到下面的方法:
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();
}
}
什麼是SqlSession呢? 註釋是這麼說的,他是MyBatis在java中主要幹活的介面,通過這個介面,你可以執行命令(它裡面定義了大量的 諸如selectList
類似的方法),獲取mapper,合併事務
The primary Java interface for working with MyBatis.
Through this interface you can execute commands, get mappers and manage transactions.
通過上面的我貼出來的函式,大家可以看到,通過事務工廠例項化了一個事物Transaction
,那麼問題來了,這個Transaction
又是什麼呢? 註釋是這麼解釋的: Transaction 包裝了一個數據庫的連線,處理這個連線的生命週期,包含: 它的建立,準備 提交/回滾 和 關閉
緊接著建立執行器:configuration.newExecutor(tx, execType)
,預設建立的是CachingExecutor
它維護了一個SimpleExecutor
, 這個執行器的特點是 在每次執行完成後都會關閉 statement 物件
關於mybatis的執行器,其實挺多事的,打算專門寫一篇筆記
繼續看new DefaultSqlSession()
我們得知,這個sqlSession的預設實現類是DefaultSqlSession
第三個引數autocommit為false, 這也是為什麼我們如果不手動提交事務時,雖然測試會通過,但是事務不會被持久化的原因
小結: 當前函式是 openSession()
, 如果說它是開啟一個session,那跟沒說是一樣的,通過原始碼我們也看到了,這一步其實是Mybatis將 資料庫連線,事務,執行器進行了一下封裝然後返回給程式設計師
獲取Mapper -- maperProxy
我們交給mybatis的mapper是一個介面,看看Mybatis是如何例項化我們的結果,返回給我們一個代理物件的
跟進原始碼:IUserDao mapper = this.sqlSession.getMapper(IUserDao.class);
,經過一個空方法,我們進入Configuration
類中的函式如下:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
從mapperRegistry
中獲取mapper,他是Configuration
屬性如下: 可以看到這個mapperRegistry
甚至包含了Configuration
,甚至還多了個 knownMappers
那麼問題來了,這個knownMappers
是幹啥呢? 我直接說,這個map就是在上面解析xml配置檔案時,存放程式設計師在<mappers>
標籤下配置的<maper>
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
// 詳情:
public class MapperRegistry {
private final Configuration config;
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
繼續跟進這個getMapper()
如下: 並且我們在配置檔案中是這樣配置的
<mappers>
<mapper class="com.changwu.dao.IUserDao"/>
</mappers>
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
所以不難想象,我們肯定能獲取到結果,通過上面的程式碼我們能看到,獲取到的物件被強轉成了MapperProxyFactory
型別,它的主要成員如下: 說白了,這個 map代理工廠是個輔助物件,它是對程式設計師提供的mapper結果的描述,同時內建使用jdk動態代理的邏輯為mapper建立代理物件
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
...
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
說到了為mapper建立動態代理,就不得不去看看是哪個類充當了動態代理的需要的InvoketionHandler -- 這個類是mybatis中的MapperProxy, 沒錯它實現了InvocationHandler介面,重寫了invoke()
邏輯,原始碼如下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
小結: 至此當前模組的獲取mapper物件已經完結了,我們明明白白的看到了MyBatis為我們的mapper使用jdk的動態代理創建出來代理物件, 這也是為什麼我們免去了自己寫實現類的粗活
執行Map -- maperProxy
上一個模組我們知道了Mybatis為我們創建出來了mapper介面的代理物件,那當我們獲取到這個代理物件之後執行它的mapper.findAll();
實際上觸發的是代理物件的invoke()
方法
所以說,接著看上面的MapperProxy
的invoke()
方法:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
上面的方法我們關注兩個地方,第一個地方就是final MapperMethod mapperMethod = cachedMapperMethod(method);
,見名知意: 快取MapperMethod
第一個問題: 這個MapperMethod
是什麼? 它其實是Mybatis為sql命令+方法全限定名設計的封裝類
*/
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
說白了,就是想為MapperProxy儲存一份map格式的資訊,key=方法全名 value=MapperMethod(command,method),存放在MapperProxy的屬性methodCache中
comand -> "name=com.changwu.dao.IUserDao,fingAll"
method -> "result= public abstract java.util.List.com.changwu.dao.IUserDao.findAll()"
為什麼要給這個MapperProxy儲存這裡呢? 很簡單,它是mapper的代理物件啊,程式設計師使用的就是他,在這裡留一份method的副本,再用的話多方便?
完成快取後,繼續看它如何執行方法:,跟進mapperMethod.execute(sqlSession, args)
因為我使用的是@Select("select * from user")
,所以一定進入下面的result = executeForMany(sqlSession, args);
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
..
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
...
}
所以我們繼續關注這個 executeForMany(sqlSession, args);
方法,看他的第一個引數是sqlSession
,也就是我們的DefaultSqlSession
,他裡面存在兩大重要物件: 1是configuration 配置物件, 2是Executor 執行器物件
繼續跟進:
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(command.getName(), param);
}
我們在繼續跟進這個selectList
方法之前,先看看這個command.getName()是啥? 其實我們上面的程式碼追蹤中有記錄: 就是name=com.changwu.dao.IUserDao,fingAll
繼續跟進去到下面的方法:
這個方法就比較有趣了,首先來說,下面的入參都是什麼
statement = "com.changwu.dao.IUserDao.findAll"
parameter=null
第二個問題:MappedStatement
是什麼? 它是一個物件,維護了很多很多的配置資訊,但是我們關心它裡面的兩條資訊,這其實可以理解成一種方法與sql之間的對映,如下圖
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
前面閱讀時,我們知道Mybatis為我們建立的預設的執行器 Executor是CachingExecutor
,如下圖
繼續跟進,主要做了下面三件事, 獲取到繫結的sql,然後呼叫SimpleExecutor
快取key,然後繼續執行query()
方法
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
接著呼叫SimpleExecutor
的query()
方法,然後我們關注SimpleExecutor
的doQuery()
方法,原始碼如下
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
我們注意到.在SimpleExcutor中建立的了一個 XXXStatementHandler這樣一個處理器, 所以我們的只管感覺就是,sql真正執行者其實並不是Executor,而是Executor會為每一條sql的執行重新new 一個 StatementHandler ,由這個handler去具體的執行sql
關於這個StatementHandler到底是是何方神聖? 暫時瞭解它是Mybatis定義的一個規範介面,定義瞭如下功能即可
public interface StatementHandler {
// sql預編譯, 構建statement物件
Statement prepare(Connection connection, Integer transactionTimeout)
throws SQLException;
// 對prepare方法構建的預編譯的sql進行引數的設定
void parameterize(Statement statement)
throws SQLException;
// 批量處理器
void batch(Statement statement)
throws SQLException;
// create update delete
int update(Statement statement)
throws SQLException;
// select
<E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException;
<E> Cursor<E> queryCursor(Statement statement)
throws SQLException;
// 獲取sql的封裝物件
BoundSql getBoundSql();
// 獲取引數處理物件
ParameterHandler getParameterHandler();
}
瞭解了這個StatementHandler是什麼,下一個問題就是當前我們建立的預設的statement是誰呢? routingStatementHandler
如下圖
建立preparedStatement
馬上馬就發生了一件悄無聲息的大事!!!根據現有的sql等資訊,構建 PreparedStatement,我們關注這個prepareStatement(handler, ms.getStatementLog());
方法,通過除錯我們得知,prepareStatement()
是RoutingStatementHandler
的抽象方法,被PreparedStatementHandler
重寫了,所以我們去看它如何重寫的,如下:
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
statement = instantiateStatement(connection);
我們關注這個instantiateStatement()
方法, 並且進入它的connection.prepareStatement(sql);
方法,如下圖:
純潔的微笑... 見到了原生JDK, jdbc的親人...
建立完事這個 preparedStatement,下一步總該執行了吧...繞這麼多圈...
我們回到上面程式碼中的return handler.query(stmt, resultHandler);
準備執行,此時第一個引數就是我們的剛創建出來的PreparedStatement, 回想一下,上面建立的這個預設的statement中的代表是PreparedStatementHandler
,所以,我們進入到這個StatementHandler的實現類RountingStatementHandler
中,看他的query()
方法
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
return delegate.query(statement, resultHandler);
}
呼叫RountingStatementHandler
中維護的代表的StatementHandler也就是PreparedStatementHandler
的query()
方法,順勢跟進去,最終會通過反射執行jdbc操作,如圖, 我圈出來的物件就是我們上面創建出來的preparedStatement
提交事務
跟進conmit()
方法,分成兩步
- 清空快取
- 提交事務
清空快取是在CachingExecutor
中呼叫了SimpleExecutor
簡單執行器的方法commit(required)
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
接在SimpleExecutor
的父類BaseExecutor
中完成
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
提交事務的操作在tcm.commit();
中完成
本文到這裡也就行將結束了,如果您覺得挺好玩的,歡迎點贊支援,有錯誤的話,也歡迎批評指出