1. 程式人生 > >居然還有人這樣解說mybatis執行原理

居然還有人這樣解說mybatis執行原理

[TOC] > mybatis執行分為兩部分,第一部分讀取配置檔案快取到Configuration物件中。用以建立SqlSessionFactory,第二部分是SqlSession的執行過程。 # Mybatis基本認識 ## 動態代理 - 之前我們知道Mapper僅僅是一個介面,而不是一個邏輯實現類。但是在Java中介面是無法執行邏輯的。這裡Mybatis就是通過動態代理實現的。關於動態代理我們常用的有Jdk動態代理和cglib動態代理。兩種卻別這裡不做贅述。關於CGLIB代理在框架中使用的比較多。 - 關於動態代理就是所有的請求有一個入口,由這個入口進行分發。在開發領域的一個用途就是【負載均衡】 - 關於Mybatis的動態代理是使用了兩種的結合。 - 下面看看JDK和cglib兩種實現 ### JDK實現 - 首先我們需要提供一個介面 , 這個介面是對我們程式設計師的一個抽象。 擁有編碼和改BUG的本領 ```java public interface Developer { /** * 編碼 */ void code(); /** * 解決問題 */ void debug(); } ``` - 關於這兩種本領每個人處理方式不同。這裡我們需要一個具體的例項物件 ```java 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 : 對我們介面物件方法的呼叫。在呼叫節點我們可以進行我們的業務攔截 ```java 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動態代理優點在於他不需要我們提前準備介面。他代理的實際的物件。這對於我們開發來說就很方便了。 ```java 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的時候載入這個例項化物件就可以了 ```java 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 ```java 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對映器 # 對映器結構 ![](http://oytmxyuek.bkt.clouddn.com/20200603001.jpg) - 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`方法 ```java protected T newInstance(MapperProxy mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } ``` - 通過上滿程式碼及上述對jdk動態代理的表述。我們可以知道mapperProxy是我們代理的重點。 - MapperProxy是InvocationHandler的實現類。他重寫的invoke方法就是代理物件執行的方法入口。 ```java @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); } ``` ```java 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`原始碼 ![](http://oytmxyuek.bkt.clouddn.com/20200603002.jpg) - 根據uml我們不難看出mybatis中提供了三類執行器分別SimpleExecutor、ReuseExecutor、BatchExecutor ```java 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中配置也很簡單 ```xml
``` - 我們也可以通過java程式碼設定 ```java factory.openSession(ExecutorType.BATCH); ``` ## StatementHandler - 顧名思義,StatementHandler就是專門處理資料庫回話的。這個物件的建立還是在Configuration中管理的。 ```java 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 ![](http://oytmxyuek.bkt.clouddn.com/20200603003.jpg) - 關於StatementHandler和RoutingStatementHandler之間的關係我們通過原始碼可以看出這裡和Executor一樣都是介面卡模式。採用這種模式的好處是方便我們對這些物件進行代理。這裡讀者可以猜測一下是使用了哪種動態代理。給點提示 這裡使用了介面哦 ![](http://oytmxyuek.bkt.clouddn.com/20200603004.jpg) ![](http://oytmxyuek.bkt.clouddn.com/20200603005.jpg) - 在檢視BaseStatementHandler結構我們會發現和Executor一模一樣。同樣的Mybatis在構造RoutingStatementHandler的時候會根據setting中配置來載入不同的具體子類。這些子類都是繼承了BaseStatementHandler. - 前一節我們跟蹤了Executor。 我們知道Mybatis預設的是SimpleExecutor。 StatementHandler我們跟蹤了Mybaits預設的是PrePareStatementHandler。在SimpleExecutor執行查詢的原始碼如下 ![](http://oytmxyuek.bkt.clouddn.com/20200603006.jpg) ![](http://oytmxyuek.bkt.clouddn.com/20200603007.jpg) - 我們發現在executor查詢錢會先讓statementHandler構建一個Statement物件。最終就是StatementHandler中prepare方法。這個方法在抽象類BaseStatmentHandler中已經封裝好了。 ![](http://oytmxyuek.bkt.clouddn.com/20200603008.jpg) - 這個方法的邏輯是初始化statement和設定連線超時等一些輔助作用 - 然後就是設定一些引數等設定。最後就走到了執行器executor的doquery ![](http://oytmxyuek.bkt.clouddn.com/20200603009.jpg) - PrepareStatement在我們jdbc開發時是常見的一個類 。 這個方法執行execute前我們需要設定sql語句,設定引數進行編譯。這一系列步驟就是剛才我們說的流程也是PrepareStatementHandler.prepareStatement幫我們做的事情。那麼剩下的我們也很容易想到就是我們對資料結果的封裝。正如程式碼所示下馬就是resultSetHandler幫我們做事情了。 ## 結果處理器(ResultSetHandler) ```java @Override public List handleResultSets(Statement stmt) throws SQLException { ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); final List multipleResults = new ArrayList<>(); int resultSetCount = 0; ResultSetWrapper rsw = getFirstResultSet(stmt); List 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返回結果資料 ![](http://oytmxyuek.bkt.clouddn.com/20200603010.jpg) # 主題 ![](http://oytmxyuek.bkt.clouddn.com/20200603jd