居然還有人這樣解說mybatis運行原理
- 2020 年 6 月 3 日
- 筆記
mybatis運行分為兩部分,第一部分讀取配置文件快取到Configuration對象中。用以創建SqlSessionFactory,第二部分是SqlSession的執行過程。
Mybatis基本認識
動態代理
-
之前我們知道Mapper僅僅是一個介面,而不是一個邏輯實現類。但是在Java中介面是無法執行邏輯的。這裡Mybatis就是通過動態代理實現的。關於動態代理我們常用的有Jdk動態代理和cglib動態代理。兩種卻別這裡不做贅述。關於CGLIB代理在框架中使用的比較多。
-
關於動態代理就是所有的請求有一個入口,由這個入口進行分發。在開發領域的一個用途就是【負載均衡】
-
關於Mybatis的動態代理是使用了兩種的結合。
-
下面看看JDK和cglib兩種實現
JDK實現
- 首先我們需要提供一個介面 , 這個介面是對我們程式設計師的一個抽象。 擁有編碼和改BUG的本領
public interface Developer {
/**
* 編碼
*/
void code();
/**
* 解決問題
*/
void debug();
}
- 關於這兩種本領每個人處理方式不同。這裡我們需要一個具體的實例對象
public class JavaDeveloper implements Developer {
@Override
public void code() {
System.out.println("java code");
}
@Override
public void debug() {
System.out.println("java debug");
}
}
-
我們傳統的調用方式是通過java提供的new 機制創造一個JavaDeveloper對象出來。而通過動態代理是通過
java.lang.reflect.Proxy
對象創建對象調用實際方法的。 -
通過
newProxyInstance
方法獲取介面對象的。而這個方法需要三個參數
ClassLoader loader : 通過實際介面實例對象獲取ClassLoader
Class<?>[] interfaces : 我們抽象的介面
InvocationHandler h : 對我們介面對象方法的調用。在調用節點我們可以進行我們的業務攔截
JavaDeveloper jDeveloper = new JavaDeveloper();
Developer developer = (Developer) Proxy.newProxyInstance(jDeveloper.getClass().getClassLoader(), jDeveloper.getClass().getInterfaces(), (proxy, method, params) -> {
if (method.getName().equals("code")) {
System.out.println("我是一個特殊的人,code之前先分析問題");
return method.invoke(jDeveloper, params);
}
if (method.getName().equals("debug")) {
System.out.println("我沒有bug");
}
return null;
});
developer.code();
developer.debug();
CGLIB動態代理
- cglib動態代理優點在於他不需要我們提前準備介面。他代理的實際的對象。這對於我們開發來說就很方便了。
public class HelloService {
public HelloService() {
System.out.println("HelloService構造");
}
final public String sayHello(String name) {
System.out.println("HelloService:sayOthers>>"+name);
return null;
}
public void sayHello() {
System.out.println("HelloService:sayHello");
}
}
- 下面我們只需要實現cglib提供的MethodInterceptor介面,在初始化設置cglib的時候載入這個實例化對象就可以了
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("======插入前置通知======");
Object object = methodProxy.invokeSuper(o, objects);
System.out.println("======插入後者通知======");
return object;
}
}
- 下面我們就來初始化設置cglib
public static void main(String[] args) {
//代理類class文件存入本地磁碟方便我們反編譯查看源程式碼
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/root/code");
//通過CGLIB動態代理獲取代理對象過程
Enhancer enhancer = new Enhancer();
//設置enhancer對象的父類
enhancer.setSuperclass(HelloService.class);
// 設置enhancer的回調對象
enhancer.setCallback(new MyMethodInterceptor());
//創建代理對象
HelloService helloService = (HelloService) enhancer.create();
//通過代理對象調用目標方法
helloService.sayHello();
}
- 仔細看看cglib和spring的aop特別像。針對切點進行切面攔截控制。
總結
- 通過對比兩種動態代理我們很容易發現,mybatis就是通過JDK代理實現Mapper調用的。我們Mapper介面實現通過代理到xml中對應的sql執行邏輯
反射
- 相信有一定經驗的Java工程師都對反射或多或少有一定了解。其實從思想上看不慣哪種語言都是有反射的機制的。
- 通過反射我們就擺脫了對象的限制我們調用方法不再需要通過對象調用了。可以通過Class對象獲取方法對象。從而通過invoke方法進行方法的調用了。
Configuration對象作用
- Configuration對象存儲了所有Mybatis的配置。主要初始化一下參數
- properties
- settings
- typeAliases
- typeHandler
- ObjectFactory
- plugins
- environment
- DatabaseIdProvider
- Mapper映射器
映射器結構
-
BoundSql提供三個主要的屬性 parameterMappings 、parameterObject、sql
-
parameterObject參數本身。我們可以傳遞java基本類型、POJO、Map或者@Param標註的參數。
-
當我們傳遞的是java基本類型mybatis會轉換成對應的包裝對象 int -> Integer
-
如果我們傳遞POJO、Map。就是對象本身
-
我們傳遞多個參數且沒有@Param指定變數名則parameterObject 類似
{“1″:p1,”2″:p2,”param1″:p1,”param2”:p2} -
我們傳遞多個參數且@Param指定變數名 則parameterObject類似
{“key1″:p1,”key2″:p2,”param1″:p1,”param2”:p2} -
parameterMapping 是記錄屬性、名稱、表達式、javaType,jdbcType、typeHandler這些資訊
-
sql 屬性就是我們映射器中的一條sql. 正常我們在常見中對sql進行校驗。正常不需要修改sql。
sqlsession執行流程(源碼跟蹤)
- 首先我們看看我們平時開發的Mapper介面是如何動態代理的。這就需要提到
MapperProxyFactory
這個類了。該類中的newInstance
方法
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
- 通過上滿程式碼及上述對jdk動態代理的表述。我們可以知道mapperProxy是我們代理的重點。
- 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);
}
private boolean isDefaultMethod(Method method) {
return (method.getModifiers()
& (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC
&& method.getDeclaringClass().isInterface();
}
- 通過源碼發現。invoke內部首先判斷對象是否是類 。 通過打斷點發現最終會走到cacheMapperMethod這個方法去創建MapperMethod對象。
- 繼續查看MapperMethod中execute方法我們可以了解到內部實現其實是一個命令行模式開發。通過判斷命令從而執行不同的語句。判斷到具體執行語句然後將參數傳遞給sqlsession進行sql調用並獲取結果。到了sqlsession就和正常jdbc開發sql進行關聯了。sqlsession中
Executor
、StatementHandler
、ParameterHandler
、Resulthandler
四大天王
Executor
-
顧名思義他就是一個執行器。將java提供的sql提交到資料庫。Mybatis提供了三種執行器。
-
Configuration.class
中newExecutor
源碼
- 根據uml我們不難看出mybatis中提供了三類執行器分別SimpleExecutor、ReuseExecutor、BatchExecutor
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 得到configuration 中的environment
final Environment environment = configuration.getEnvironment();
// 得到configuration 中的事務工廠
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 獲取執行器
final Executor executor = configuration.newExecutor(tx, execType);
// 返回默認的SqlSession
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獲取一個資料庫session對象時我們或根據我們的settings配置載入一個Executor對象。在settings中配置也很簡單
<settings>
<!--取值範圍 SIMPLE, REUSE, BATCH -->
<setting name="defaultExecutorType" value="SIMPLE"/>
</settings>
- 我們也可以通過java程式碼設置
factory.openSession(ExecutorType.BATCH);
StatementHandler
- 顧名思義,StatementHandler就是專門處理資料庫回話的。這個對象的創建還是在Configuration中管理的。
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
- 很明顯Mybatis中StatementHandler使用的是RoutingStatementHandler這個class
- 關於StatementHandler和RoutingStatementHandler之間的關係我們通過源碼可以看出這裡和Executor一樣都是適配器模式。採用這種模式的好處是方便我們對這些對象進行代理。這裡讀者可以猜測一下是使用了哪種動態代理。給點提示 這裡使用了介面哦
-
在查看BaseStatementHandler結構我們會發現和Executor一模一樣。同樣的Mybatis在構造RoutingStatementHandler的時候會根據setting中配置來載入不同的具體子類。這些子類都是繼承了BaseStatementHandler.
-
前一節我們跟蹤了Executor。 我們知道Mybatis默認的是SimpleExecutor。 StatementHandler我們跟蹤了Mybaits默認的是PrePareStatementHandler。在SimpleExecutor執行查詢的源碼如下
- 我們發現在executor查詢錢會先讓statementHandler構建一個Statement對象。最終就是StatementHandler中prepare方法。這個方法在抽象類BaseStatmentHandler中已經封裝好了。
- 這個方法的邏輯是初始化statement和設置連接超時等一些輔助作用
- 然後就是設置一些參數等設置。最後就走到了執行器executor的doquery
- PrepareStatement在我們jdbc開發時是常見的一個類 。 這個方法執行execute前我們需要設置sql語句,設置參數進行編譯。這一系列步驟就是剛才我們說的流程也是PrepareStatementHandler.prepareStatement幫我們做的事情。那麼剩下的我們也很容易想到就是我們對數據結果的封裝。正如程式碼所示下馬就是resultSetHandler幫我們做事情了。
結果處理器(ResultSetHandler)
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);
}
- 這個方法我們可以導出來是結果xml中標籤配置對結果的一個封裝。
總結
- SqlSession在一個查詢開啟的時候會先通過CacheExecutor查詢快取。擊穿快取後會通過BaseExector子類的SimpleExecutor創建StatementHandler。PrepareStatementHandler會基於PrepareStament執行資料庫操作。並針對返回結果通過ResultSetHandler返回結果數據