1. 程式人生 > >深入剖析 mybatis 原理(二)

深入剖析 mybatis 原理(二)

# 前言

在上篇文章中我們分析了 sqlSession.selectOne(“org.apache.ibatis.mybatis.UserInfoMapper.selectById”, parameter) 程式碼的執行過程,我們說,這種方式雖然更接近 mybaits 的底層,但不夠面向物件,也不利於 IDEA 工具的編譯期排錯。

而mybatis 還有另一種寫法,我們在測試程式碼也寫過,如下:

   UserInfoMapper userInfoMapper = sqlSession.getMapper(UserInfoMapper.class);
   UserInfo userInfo2 = userInfoMapper.selectById(1
);

這段程式碼非常的面向物件,也非常的利於IDE工具在編譯期間排錯。實際上這是mybait 為我們做的工作,是對第一種的方式的一種更抽象的封裝。同時,這種方式也是我們現在開發常用的一種方式,所以,我們必須剖析的原理,看看他到底是如何實現的。想必有經驗的大佬都能猜測到,肯定用動態代理的技術。不過,我們還是從原始碼看個究竟吧!

1. 從 getMapper 方法進入原始碼

實際上呼叫了 configuration 的 getMapper方法:

configuration 實際上呼叫了 mapperRegistry.getMapper:

注意,要停下來,看看 mapperRegistry 是什麼,從名字上看出來,該物件是Mapper 對映器註冊容器,我們看看該物件中有什麼?

這是該類的屬性,有一個 Configuration 物件,有一個 Map,Map 存放什麼資料呢,key 是 class 型別, value是 MapperProxyFactory 型別,MapperProxyFactory 又是什麼呢,看名字是對映器代理物件工廠,我們看看該類:

這是該類的結構圖,有一個Class 物件,表示 對映器的介面,有一個Map 表示對映方法的快取。並且由2 個newInstance 方法,看名字肯定是建立代理物件啦。我們看看這2個方法:

呼叫了動態的技術,根據給定的介面和SqlSession 和方法快取,建立一個代理物件 MapperProxy ,該類實現了 InvocationHandler 介面,因此我們需要看看他的 invoke 方法:

這是 MapperProxy 的 invoke 方法,該方法首先判斷方法的class 是否繼承 Object ,如果是,就不使用代理,直接執行,如果該方法是預設的,那麼就執行預設方法(該方法是針對Java7以上版本對動態型別語言的支援,不進行詳述)。我們這裡肯定不是,執行下面的 cachedMapperMethod 方法 並呼叫返回物件 MapperMethod 的execute 方法。

我們看看 cachedMapperMethod 方法,該方法應該是跟快取相關,我們看看實現:

該方法首先從快取中取出,如果沒有,便建立一個,並放入快取並返回。我們關注一下 MapperMethod 的構造方法:

該構造方法拿著這三個引數建立了兩個物件,一個是SQL命令物件,一個方法簽名物件,這兩個類都是MapperMethod 的靜態內部類。我們來看看這兩個類。

SqlCommand 類:

該類由2個屬性,一個是name,表示sql語句的名稱,一個是type 自動,表示sql語句的型別,sql語句的名稱是怎麼來的呢?我們從程式碼中看到是從 resolveMappedStatement 方法返回的 MappedStatement 物件中得到的。而 SqlCommandType 也是從該方法中得到的。那麼我重點關注該方法。

我們剛剛說,name 是怎麼來的,是呼叫了 MappedStatement 物件的getId 方法來的,而 id 是怎麼來的呢?是mapperInterface.getName() + “.” + methodName 拼起來的,也就是介面名稱和方法名稱成為了一個id,素以注意,該方法不能過載。因為他不關注引數。根據id從 configuration 獲取解析好的MappedStatement(存放在hashmap中)。如果沒有這個id的話,注意,該方法最後還會遞迴呼叫該介面的所有父介面的 resolveMappedStatement ,確保找到給定 id 的 MappedStatement。

那麼 SqlCommandType 是什麼呢?是個列舉,我們看看該列舉:

該列舉定義我們在xml中的標籤。是不是很親切?

那麼 MethodSignature 是什麼呢?我們看看該類由哪些屬性:

看該類名字知道是方法的簽名,因此包含方法的很多資訊,是否返回多值,是否返回map,是否返回遊標,返回值型別等等,這些屬性都是在構造方法中注入的:

看完了內部類,回到 MapperProxy 的invoke 方法,現在有了 MapperMethod 物件,就要執行該物件的execute 方法,該方法是如何執行的呢?

java
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);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}

該方法主要是判斷方法的型別,也就是我們的insert select 標籤,根據不同的標籤執行不同的方法,如果是 SELECT 標籤就還要再判斷他的返回值,根據不同的返回值型別執行不同的方法。預設執行 sqlSession 的 selectOne 方法,看到這裡,是不是一目瞭然了呢?也就是說getMapper 最終還是呼叫 SqlSession 的 selectOne 方法,只不過通過動態代理封裝了一遍,讓mybatis 來管理這些字串樣式的key,而不是讓使用者來手動管理。

我們回到 MapperRegistry 的 getMapper 方法:

首先根據介面型別從快取中取出,如果沒有,則丟擲異常,因為這些快取都是在解析配置檔案的時候放入的。根據返回的對映代理工廠,呼叫該工廠的方法,傳入 SqlSession 返回一個介面代理:

這段程式碼其實我們已經看過了,建立一個實現類 InvocationHandler 介面的物件,然後使用JDK動態代理建立例項返回,而 InvocationHandler 的實現類 MapperProxy 的程式碼我們剛剛也看過了。主要邏輯在invoke中,在該方法中呼叫 SqlSession 的 selectOne 方法。後面的我們就不說了,和上一篇文章的邏輯一樣,就不贅述了。

2. 總結

大家可能注意到了,這篇文章不長,因為主要邏輯在上篇文章中,這裡只不過將 Mybatis 如何封裝代理的過程解析了一遍。主要實現這個功能是的是 mybatis 的binding 包下的幾個類:

這幾個類完成了對代理的封裝和對目標方法的呼叫。當然還有 SqlSession,可以看出,mybatis 的模組化做的非常好。

我們也來看看這幾個類的UML圖:

可以看到,所有的類都關聯著SqlSession,由此可以看出SqlSession的重要性。而我們今天解析的程式碼只不過在SqlSession外面封裝了一層,便於開發者使用,否則,配置這些字串,就太難以維護了。

好了,今天的Mybatis 分析就到這裡了。我們通過一個demo知道了mybatis 的執行原理,由此,在以後的開發中,遇到錯誤時,再也不是黑盒操作了。可以深入原始碼去找真正的原因。當然,閱讀原始碼帶來的好處肯定不止這些。

good luck !!!!