本文探討在需要了解一個開源專案時,如何快速的理清開源專案的程式碼邏輯!
以下是個人認為行之有效的方法:
- 先「跑起來」
- 自頂向下拆解
- 深入細節
- 延伸改進
本文以Mybatis為例來進行演示!
先“跑起來”
程式界有個老傳統,學習新技術時都是從「Hello World」開始的!無論是學習新語言時,列印「Hello World」;還是學習新框架時編寫個demo!那為什麼這裡的「跑起來」要打個引號呢?
實際上,當你想要閱讀一個開源專案的原始碼時,絕大部分情況下,你已經能夠使用這個開源專案了!所以這裡的“跑起來”就不是寫個「Hello World」,也不是能跑起來的程式了!而是能__在你的腦子裡「跑起來」__!什麼意思?
Mybatis你會用了吧?那麼請問Mybatis是如何執行的呢?仔細想想,你能否用完整的語句把它描述出來?
這裡是Mybatis的官方入門文章!你是如何看這篇文章的?讀一遍就行了嗎?還是跟著文章跑一遍就夠了嗎?從這篇文章裡你能獲得多少資訊?
我們來理一下:
- 安裝
- 如何在專案中引入Mybatis?
- Mybatis的groupId是什麼?artifactId又是什麼?目前最新版本是多少?
- 從 XML 中構建 SqlSessionFactory
- SqlSessionFactoryBuilder可以通過xml或者Configuration來構建SqlSessionFactory,那是如何構建的呢?
- xml配置了哪些資訊?既然使用了xml,那肯定有xml解析,用什麼方式解析的?
- xml裡的標籤都是什麼意思:configuration,environments,transactionManager,dataSource,mappers。以及這些標籤的屬性分別是什麼意思?
- SqlSessionFactory的作用是什麼?
- 不使用 XML 構建 SqlSessionFactory
- BlogDataSourceFactory,DataSource,TransactionFactory,Environment,Configuration這些類的作用是什麼?
- *Mapper的作用是什麼?
- 為什麼提供基於XML和Java的兩種配置方式?這兩種配置方式的優缺點是什麼?
- 從 SqlSessionFactory 中獲取 SqlSession
- SqlSession的作用是什麼?
- selectOne和getMapper的執行方式有什麼區別?
- 探究已對映的 SQL 語句
- *Mapper.xml的配置是什麼?
- 名稱空間,id的作用是什麼?
- *Mapper.xml是如何和*Mapper.java進行匹配的?
- 匹配規則是什麼?
- 基於註解的對映配置如何使用?
- 為什麼提供基於XML和基於註解的兩種對映配置?有什麼優劣?
- 作用域(Scope)和生命週期
- SqlSessionFactoryBuilder應該在哪個作用域使用?為什麼?
- SqlSessionFactory應該在哪個作用域使用?為什麼?
- SqlSession應該在哪個作用域使用?為什麼?
- Mapper例項應該在哪個作用域使用?為什麼?
回答出了上面這些問題!你也就基本能在腦子裡把Mybatis「跑起來」了!之後,你才能正真的開始閱讀原始碼!
當你能把一個開源專案「跑起來」後,實際上你就有了對開源專案最初步的瞭解了!就像「書的索引」一樣!基於這個索引,我們一步步的進行拆解,來細化出下一層的結構和流程,期間可能需要深入技術細節,考量實現,考慮是否有更好的實現方案!也就是說後面的三步並不是線性的,而是__不斷交替執行__的一個過程!最終就形成一個完整的原始碼執行流程!
自頂向下拆解
繼續通過Mybatis來演示(限於篇幅,我只演示一個大概流程)!我們現在已經有了一個大概的流程了:
- SqlSessionFactoryBuilder通過xml或者Configuration構建出SqlSessionFactory
- 可以從SqlSessionFactory中獲取SqlSession
- SqlSession則是真正執行sql的類
雖說每個點都可以往下細化,但是也分個輕重緩急!
- 我們是先了解怎麼構建SqlSessionFactory呢?
- 還是瞭解如何獲取SqlSession呢?
- 還是瞭解SqlSession如何執行sql的呢?
很明顯,SqlSession去執行 sql才是Mybatis的核心!我們先從這個點入手!
首先,你當然得先下載Mybatis的原始碼了(請自行下載)!
我們直接去看SqlSession!它是個介面,裡面有一堆執行sql的方法!
這裡只列出了一部分方法:
public interface SqlSession extends Closeable {
<T> T selectOne(String statement);
<E> List<E> selectList(String statement);
<K, V> Map<K, V> selectMap(String statement, String mapKey);
<T> Cursor<T> selectCursor(String statement);
void select(String statement, Object parameter, ResultHandler handler);
int insert(String statement);
int update(String statement);
int delete(String statement);
void commit();
void rollback();
List<BatchResult> flushStatements();
<T> T getMapper(Class<T> type);
...
}
複製程式碼
SqlSession就是通過這些方法來執行sql的!我們直接看我們常用的,也是Mybatis推薦的用法,就是基於Mapper的執行!也就是說「SqlSession通過Mapper來執行具體的sql」!上面的流程也就細化成了:
- SqlSessionFactoryBuilder通過xml或者Configuration構建出SqlSessionFactory
- 可以從SqlSessionFactory中獲取SqlSession
- SqlSession則是真正執行sql的類
- SqlSession獲取對應的Mapper例項
- Mapper例項來執行相應的sql
那SqlSession是如何獲取Mapper的呢?Mapper又是如何執行sql的呢?
深入細節
我們來看SqlSession的實現!SqlSession有兩個實現類SqlSessionManager和DefaultSqlSession!通過IDE的引用功能可以檢視兩個類的使用情況。你會發現SqlSessionManager實際並沒有使用!而DefaultSqlSession是通過DefaultSqlSessionFactory構建的!所以我們來看DefaultSqlSession是如何構建Mapper的!
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
複製程式碼
它直接委託給了Configuration的getMapper方法!
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
複製程式碼
Configuration又委託給了MapperRegistry類的getMapper方法!
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);
}
}
複製程式碼
在MapperRegistry類的getMapper中:
- 通過type從knownMappers中獲取對應的MapperProxyFactory例項
- 如果不存在則丟擲異常
- 如果存在則呼叫mapperProxyFactory.newInstance(sqlSession)建立對應的Mapper
在這裡knowMappers是什麼?MapperProxyFactory又是什麼?mapperProxyFactory.newInstance(sqlSession)具體做了什麼?
其實很簡單,knowMappers是個Map,裡面包含了class與對應的MapperProxyFactory的對應關係!MapperProxyFactory通過newInstance來構建對應的Mapper(實際上是Mapper的代理)!
快接近真相了,看mapperProxyFactory.newInstance(sqlSession)裡的程式碼:
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession,
mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(),
new Class[] { mapperInterface },
mapperProxy);
}
複製程式碼
這裡幹了什麼?
- 通過sqlSession,mapperInterface和methodCache構建了一個MapperProxy物件
- 然後通過Java的動態代理,來生成了Mapper的代理類
- 將Mapper方法的執行都委託給了MapperProxy去執行
@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);
}
複製程式碼
- 如果是Object裡的方法則直接執行
- 否則執行MapperMethod的execute方法
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 UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional() &&
(result == null || !method.getReturnType().equals(result.getClass()))) {
result = OptionalUtil.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
return result;
}
複製程式碼
最終實際還是委託給了sqlSession去執行具體的sql!後面具體怎麼實現的就自行檢視吧!
延伸改進
現在我們的流程大概是這樣的一個過程:
- SqlSessionFactoryBuilder通過xml或者Configuration構建出SqlSessionFactory
- 可以從SqlSessionFactory中獲取SqlSession
- SqlSession則是真正執行sql的類
- SqlSession獲取對應的Mapper例項
- DefaultSqlSession.getMapper
- Configuration.getMapper
- MapperRegistry.getMapper
- mapperProxyFactory.newInstance(sqlSession)
- 通過sqlSession,mapperInterface和methodCache構建了一個MapperProxy物件
- 然後通過Java的動態代理,來生成了Mapper的代理類
- Mapper例項來執行相應的sql
- 將Mapper方法的執行都委託給了MapperProxy去執行
- 如果是Object裡的方法則直接執行
- 否則執行MapperMethod的execute方法
- 最終還是委託給sqlSession去執行sql
- SqlSession獲取對應的Mapper例項
現在我們大概知道了:
- 為什麼Mapper是個介面了
- Mybatis基於這個介面做了什麼
那麼,
- 什麼是動態代理(基礎哦)?
- 為什麼使用動態代理來處理?
- 基於動態代理有什麼優點?又有什麼缺點?
- 除了動態代理,還有其它什麼實現方式嗎?比如說cglib?
- 如果是其它語言的話,有沒有什麼好的實現方式呢?
- ......
這個問題列表可以很長,可以按個人需要去思考並嘗試回答!可能最終這些問題已經和開源專案本身沒有什麼關係了!但是你思考後的收穫要比看原始碼本身要多得多!
再迴圈
一輪結束後,可以再次進行:
- 自頂向下拆解
- 深入細節
- 延伸改進
不斷的拆解->深入->改進,最終你能__通過一個開源專案,學習到遠比開源專案本身多得多的知識__!
最重要的是,你的流程是完整的。無論是最初的大致流程:
- SqlSessionFactoryBuilder通過xml或者Configuration構建出SqlSessionFactory
- 可以從SqlSessionFactory中獲取SqlSession
- SqlSession則是真正執行sql的類
還是到最終深入的細枝末節,都是個完整的流程!
這樣的好處是,你的時間能自由控制:
- 你是要花個半天時間,瞭解大致流程
- 還是花個幾天理解細節流程
- 還是花個幾周,幾個月來深入思考,不斷延伸 你都可以從之前的流程中快速進行下去!
而不像debug那樣的方式,需要一下子花費很長的時間去一步步的理流程,費時費力、收效很小,而且如果中斷了就很難繼續了!
總結
本文通過梳理Mybatis原始碼的一個簡單流程,來講述一個個人認為比較好的閱讀原始碼的方式,並闡述此方法與傳統debug方式相比的優勢。
公眾號:ivaneye