如何理解mybatis運行原理?試試理解他爲咱們到底作了啥---前人種樹後人乘涼

這是我參與更文挑戰的第21天,活動詳情查看: 更文挑戰java

[TOC]程序員

mybatis運行分爲兩部分,第一部分讀取配置文件緩存到Configuration對象中。用以建立SqlSessionFactory,第二部分是SqlSession的執行過程。spring

Mybatis基本認識

動態代理

  • 以前咱們知道Mapper僅僅是一個接口,而不是一個邏輯實現類。可是在Java中接口是沒法執行邏輯的。這裏Mybatis就是經過動態代理實現的。關於動態代理咱們經常使用的有Jdk動態代理和cglib動態代理。兩種卻別這裏不作贅述。關於CGLIB代理在框架中使用的比較多。sql

  • 關於動態代理就是全部的請求有一個入口,由這個入口進行分發。在開發領域的一個用途就是【負載均衡】數據庫

  • 關於Mybatis的動態代理是使用了兩種的結合。緩存

  • 下面看看JDK和cglib兩種實現markdown

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對象建立對象調用實際方法的。session

  • 經過newProxyInstance方法獲取接口對象的。而這個方法須要三個參數mybatis

ClassLoader loader : 經過實際接口實例對象獲取ClassLoader Class<?>[] interfaces : 咱們抽象的接口 InvocationHandler h : 對咱們接口對象方法的調用。在調用節點咱們能夠進行咱們的業務攔截app

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中ExecutorStatementHandlerParameterHandlerResulthandler四大天王

Executor

  • 顧名思義他就是一個執行器。將java提供的sql提交到數據庫。Mybatis提供了三種執行器。

  • Configuration.classnewExecutor源碼

  • 根據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返回結果數據

主題

相關文章
相關標籤/搜索