一文看懂mybatis底層運行原理解析

友情提示 : 本文的重點是解析mybatis執行一次sql 的流程 ,過程中思維比較跳躍,覺得比較難得可以往下查看總結中源碼流程


 

這篇文章通過一個insert語句來解析mybatis執行一次sql的運行流程。背景介紹,源碼來自於 mybatis-3.0.5.jar,

mybatis-config.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC 
	"-//mybatis.org//DTD Config 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

	<properties resource="jdbc.properties" />
	<!-- 配置別名 -->
	<typeAliases>
		<typeAlias type="dao.DataDao" alias="DataDao" />
	</typeAliases>
	 
	<!-- 配置環境變量 -->
	<environments default="development">
		<environment id="development">
			<transactionManager type="JDBC" />
			<dataSource type="POOLED">
				<property name="driver" value="${driverClass}" />
				<property name="url" value="${jdbcUrl}" />
				<property name="username" value="${DBusername}" />
				<property name="password" value="${DBpassword}" />
			</dataSource>
		</environment>
	</environments>
	
	<!-- 配置mappers -->
	<mappers>

		<mapper resource="dao/DataDao.xml" />
	</mappers>
	
</configuration>   

mapper對應的接口:

public interface DataDao {

    public  int mysqlInsert(Map<String, Object> map) throws Exception;
}

接口對應的 DataDao.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="dao.DataDao">
   <insert id="mysqlInsert" parameterType="map" >
		insert into ${tableName}
		(${columns})
		values
		(${colValues})
	</insert>
</mapper>

首先我們會從mybatis的入口加載的地方來進行解讀,先看一段mybatis的插入代碼:

/**
     * 傳入map參數,執行插入
     * @param map
     * @return
     */
    public static int mysqlInsert(Map<String, Object> map) throws Exception {
        SqlSession session = null;
        int index=0;
        try {
            session = SessionFactory.getSession();
            DataDao dataDao = session.getMapper(DataDao.class);
            index=dataDao.mysqlInsert(map);
            if(index>0){
                logger.info("insert success: "+index);
            }
             session.commit(true);
        } catch (Exception e) {
            logger.error("插入異常:", e);
            session.rollback(true);
            throw e;
        } finally {
            session.close();
        }
        return index;
    }
public class SessionFactory {

    private static Logger logger =  LoggerFactory.getLogger(SessionFactory.class);

    private static SqlSessionFactoryBuilder sqlSessionFactoryBuilder;
    private static SqlSessionFactory sqlSessionFactory;

        //初始化mybatis
        static {
            String resource = "mybatis-config.xml";
            Reader reader = null;
            try {
                reader = Resources.getResourceAsReader(resource);
                sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
                sqlSessionFactory = sqlSessionFactoryBuilder.build(reader);
            } catch (IOException e) {
                logger.error("加載配置文件錯誤:",e);
                e.printStackTrace();
            }catch (Exception e){
                logger.error("加載配置文件錯誤:",e);
            }

            logger.info("init mybatis");
        }

    public  static synchronized SqlSession getSession() throws IOException {
        SqlSession sqlSession =null;
        if(null == sqlSession) {
            sqlSession = (null != sqlSessionFactory)?sqlSessionFactory.openSession(): null;
        }
        return sqlSession;
    }

}

代碼中可以看到首先會通過  SessionFactory 去獲取 SqlSession,裏面會有一段static代碼,在SessionFactory類加載時會執行mybatis-config.xml 的初始化過程。現在我們直接看重點: 

通過SqlSession 的 getMapper獲取DataDao數據接口的時候發現有2個實現,追溯到SessionFactory的初始化中會看到下面一段:

所以我們直接進入 DefaultSqlSession getMapper方法中,可以找到 MapperRegistry 中:

看到這裏熟悉的動態代理的朋友知道這是一個獲取java的動態代理類的寫法(ps: 不熟悉動態代理的,可以看這裏我的上一篇博客 動態代理白話解析),我們繼續往下看:

繼續走到下一步:dataDao.mysqlInsert(map); 執行這個方法時會調用 MapperProxy 的 invoke方法:

開源看到除了Object 中的方法不能代理,其他的都會走到 mapperMethod.execute(args):

type 字段會在mapperMethod初始化時賦值,這裏我們執行的insert,繼續往下走到DefaultSqlSession 的update方法 :

到這裏後,接下來我們開始看 Configuration的初始化,它在SqlSessionFactory初始化時構造的

上面2段代碼可以看到 Configuration 是通過加載mybatis-config.xml時進行初始化的。

現在我們回到 this.configuration.getMappedStatement(statement); 這段代碼:

進去看下我們發現他其實是通過 id 去獲取對應的MappedStatement ,通過上文我們可以知道id是MapperMethod的execute方法傳進來的commandName,它在MapperMethod 的構造函數中的 setupFields 方法中初始化:

在本文中的值爲: DataDao.mysqlInsert。類似這種,也就是接口類.方法名。

現在我們回到主流程中,查看 mappedStatements 的初始化過程,是在上文中初始化Configuration 的this.mapperElement(root.evalNode("mappers")); 中  : 

這裏會解析mapper.xml中的sql節點,本文中的示例是 這一段:

我們繼續查看mappedStatements的初始化 :

這裏的id我們可以看到就是節點中的namespace.id的值拼接,也就是 DataDao.mysqlInsert

 

現在我們繼續回到主流程,看這段代碼:

代碼中通過executor執行update方法,現在我們看下 executor 的初始化過程,是在SqlSession初始化的過程中:

在Configuration的構造函數中可以看到這裏的 executorType 默認爲SIMPLE,cacheEnabled 默認爲true.

現在我們繼續回到主流程,進到 CachingExecutor 的 update 方法中:

這裏會進入到 BaseExecutor 的 update方法中

這裏會進入到 SimpleExecutor 的doUpdate中:

這裏首先初始化了一個 RoutingStatementHandler ,由上文可知 statementType 默認爲 PREPARED,所以 其 delegate 屬性爲 PreparedStatementHandler。我們回到主流程,繼續看  this.prepareStatement(handler) :

會走到 BaseStatementHandler 的 prepare 方法 : 

這裏會進入到 PreparedStatementHandler 的 instantiateStatement 方法:

這裏的 boundSql 是在 上文中的 mappedStatement 初始化的時候賦值的:

 

首先是在 parseStatementNode 中初始化 DynamicSqlSource,然後獲取 BoundSql,接下來我們看下XMLStatementBuilder的parseDynamicTags方法:

代碼中我們可以看到若節點類型既不是文本節點也不是CDATA節點就會調用不同的處理類去處理,我們比較常用的就是查詢時使用的 <if> 和 <where> 節點 ,這裏我們執行的是 insert , 所以直接是一個文本節點,下面就是初始化了一個MixedSqlNode 和 configuration 構造了 DynamicSqlSource,然後通過getBoundSql 方法來構造BoundSql:

 由上文知:rootSqlNode是MixedSqlNode,這裏會執行 MixedSqlNode 的 apply 方法,在本例中會執行 TextSqlNode 的 apply

方法:

這一段呢主要是用來處理sql中的${}佔位符參數,本文中的  insert into ${tableName} (${columns}) values (${colValues}) 經過parse 轉換後  insert into table (id,name) values (1,'test') , 這一段的代碼就不分析了,比較繁雜,主要是通過將對應的佔位符替換爲對應的值。

我們回到  getBoundSql 方法 ,繼續往下走,到  sqlSourceParser.parse(context.getSql(), parameterType) 這一行代碼: 

又看到了熟悉的代碼,這一段是將#{} 佔位符替換爲對應的value.

我們繼續回到上文主流程中的  PreparedStatementHandler 的 instantiateStatement 方法,往下走:

這裏的 Jdbc3KeyGenerator 是如果你設置了useGeneratedKeys=true 用來 自動生成主鍵的,resultSetType 本例中也沒有涉及爲空,所以這裏會返回一個 PreparedStatement, 到這裏我們的  SimpleExecutor 的 doUpdate 的  this.prepareStatement(handler)

這段代碼就分析完了。

我們回到主流程繼續往下走:

這裏進入到 jdbc 的 PreparedStatement 的執行過程,執行insert插入的操作。後面2段代碼我就不分析了,主要是用來獲取主鍵的。

到這裏,mybatis執行一次insert sql的運行機制就解析完成了。

 

總結:

 在源碼分析的過程中,可能會比較跳躍,因爲在源碼分析過程中會有一些配置類或執行類的初始化過程。這裏分享一下快速閱讀源碼的方式,在查看源碼的過程中,可能會有許多擴展,校驗,初始化的一些功能代碼,比如:

   1. Configuration 類的初始化

   2. Interceptor 攔截器的執行(實際採用的是責任鏈設計模式和動態代理設計模式來實現的攔截)

   3. cache緩存的實現方式

可以暫時先跳過,先把主要流程的代碼過一遍,比如mybatis 執行一次sql的流程源碼可以按照如下方式查看:

 

當我們將整體流程實現查看清楚後,如果想查看更多的細節,可以使用其他時間回頭來查看擴展功能的實現方式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章