代理模式在MyBatis原始碼中的應用

2020-10-13 01:01:13
MyBatis 是一個應用非常廣泛的優秀持久層框架,它幾乎避免了所有 JDBC 程式碼、手動設定引數和獲取結果集等工作。MyBatis 可以使用簡單的 XML 組態檔或註解來對映類、介面和 POJO 與資料庫記錄的對應關係。

如果您使用過 MyBatis,則會發現 MyBatis 的使用非常簡單。首先需要定義一個 Dao 介面,然後編寫一個與 Dao 介面對應的 Mapper 組態檔。Java 物件與資料庫欄位的對映關係和 Dao 介面對應的 SQL 語句都寫在組態檔中,非常簡單清晰。

那麼 Dao 介面是怎麼和 Mapper 檔案對映的呢?只有一個 Dao 介面,又是怎麼以物件的形式來實現資料庫讀寫操作的呢?

在瞭解代理模式後,我們應該很容易猜到,可以通過動態代理來建立 Dao 介面的代理物件,並通過這個代理物件實現資料庫的操作。

在 MyBatis 中,MapperProxyFactory、MapperProxy、MapperMethod 是三個很重要的類。我們只需要瞭解這 3 個類,就能明白 Dao 介面與 SQL 的對映原理。

1. MapperProxyFactory

呼叫 addMapper() 方法時,跟蹤原始碼就會發現 MapperRegistry 類的 addMapper() 方法中有如下語句:

knownMappers.put(type, new MapperProxyFactory<T>(type));

type 就是 Dao 介面,下面來看 MapperProxyFactory 類。
public class MapperProxyFactory<T> {

    private final Class<T> mapperInterface;
    private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public Class<T> getMapperInterface() {
        return mapperInterface;
    }

    public Map<Method, MapperMethod> getMethodCache() {
        return methodCache;
    }

    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
    }

    public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }
}
MapperProxyFactory 看名字就知道這是一個工廠類,目的就是為了生成 MapperProxy。下面我們主要看它的構造方法和 newInstance() 方法。

構造方法中傳入了一個 Class,通過引數名 mapperInterface 可以很容易猜到這個類就是個 Dao 介面。

MapperProxyFactory 中有 2 個 newInstance() 方法,許可權修飾符不同,一個是 protected,一個是 public。
  1. protected 的 newInstance() 方法中首先建立了一個 MapperProxy 類的物件,然後通過 Proxy.newProxyInstance() 方法建立了一個物件並返回,也是通過同樣的方法返回了 mapperInterface 介面的代理物件。
  2. public 的 newInstance() 方法中有一個引數 SqlSession,SqlSession 處理的就是執行一次 SQL 的過程。

上面提到的 MapperProxy 類顯然是 InvocationHandler 介面的實現。因此,可以說 MapperProxyFactory 類是一個建立代理物件的工廠類,它通過建構函式傳入自定義的 Dao 介面,並通過 newInstance 方法返回 Dao 介面的代理物件

2. MapperProxy

看到這裡,是不是有了一種豁然開朗的感覺,但新的疑問又來了,我們定義的 Dao 介面的方法並沒有被實現,那這個代理物件又是如何實現增刪改查的呢?帶著這個疑問,我們來看一下 MapperProxy 類,原始碼如下:
public class MapperProxy<T> implements InvocationHandler, Serializable {

    private static final long serialVersionUID = -6424540398559729838L;
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;

    public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (Object.class.equals(method.getDeclaringClass())) {
            try {
                return method.invoke(this, args);
            } catch (Throwable t) {
                throw ExceptionUtil.unwrapThrowable(t);
            }
        }
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        return mapperMethod.execute(sqlSession, args);
    }

    private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = methodCache.get(method);
        if (mapperMethod == null) {
            mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
            methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
    }
}
很明顯因為 Java 動態代理,MapperProxy 實現了 InvocationHandler 介面。下面先看 invoke() 方法。

在 invoke() 方法中,首先檢查瞭如果是 Object 的方法就直接呼叫方法本身;如果不是就把方法 Method 包裝成 MapperMethod。我們前面已經提到了 MapperMethod 主要就是處理方法的註解,引數,返回值,以及引數與 SQL 語句中引數的對應關係。因為將 Method 處理為 MapperMethod 是一個較頻繁的操作,所以這裡做了快取處理。

MapperProxy中的成員變數

下面我們講解 MapperProxy 類的 3 個成員變數,分別如下:

1)SqlSession

通過名字就知道 SqlSession 變數是一個執行 SQL 的介面,簡單看一下它的介面定義。
public interface SqlSession extends Closeable {
    <T> T selectOne(String statement);

    <T> T selectOne(String statement, Object parameter);
   
    // 下面省略
}
這個介面方法的入參是 statement 和引數(parameter),返回值是資料物件。statement 可能會被誤解為 SQL 語句,但其實這裡的 statement 是指 Dao 介面方法的名稱,自定義的 SQL 語句都被快取在 Configuration 物件中。

在 SqlSession 中,可以通過 Dao 介面的方法名稱找到對應的 SQL 語句。因此,可以想到代理物件本質上就是要將執行的方法名稱和引數傳入 SqlSession 的對應方法中,根據方法名稱找到對應的 SQL 語句並替換引數,最後得到返回的結果。

2)mapperInterface

mapperInterface 的作用要結合第 3 個成員變數來說明。

3)methodCache

methodCache 其實就是一個 Map 鍵值對結構,鍵是 Method,值是 MapperMethod。

下面再回到 invoke() 方法的最後兩行,它首先通過 cachedMapperMethod() 方法找到與將要執行的 Dao 介面方法對應的 MapperMethod,然後呼叫 MapperMethod 的 execute() 方法來實現資料庫的操作。這裡顯然是將 SqlSession 傳入 MapperMethod 內部,並在 MapperMethod 內部將要執行的方法名和引數再傳入到 SqlSession 對應的方法中去執行。

3. MapperMethod

下面來看 MapperMethod 類的內部,看它如何完成 SQL 的執行。
public class MapperMethod{
    private final SqlCommand command;
    private final MethodSignature method;
}
MapperMethod 類中有兩個成員變數,分別是 SqlCommand 和 MethodSignature。雖然這兩個類的程式碼看起來很多,但實際上這兩個內部類非常簡單。
  • SqlCommand 主要解析了介面的方法名稱和方法型別,定義了諸如 INSERT、SELECT、DELETE 等資料庫操作的列舉型別。
  • MethodSignature 則解析了介面方法的簽名,即介面方法的引數名稱和引數值的對映關係,即通過 MethodSignature 類可以將入參的值轉換成引數名稱和引數值的對映。

最後來看 MapperMethod 類中最重要的 execute() 方法。
public Object execute(SqlSession sqlSession, Object[] args) {
    Object param;
    Object result;
    switch(this.command.getType()) {
    case INSERT:
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
        break;
    case UPDATE:
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
        break;
    case DELETE:
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
        break;
    case SELECT:
        if (this.method.returnsVoid() && this.method.hasResultHandler()) { // 返回型別為void
            this.executeWithResultHandler(sqlSession, args);
            result = null;
        } else if (this.method.returnsMany()) { // 返回型別為集合或陣列
            result = this.executeForMany(sqlSession, args);
        } else if (this.method.returnsMap()) {// 由@MapKey控制返回
            result = this.executeForMap(sqlSession, args);
        } else if (this.method.returnsCursor()) {// 返回型別為Cursor<T>,採用遊標
            result = this.executeForCursor(sqlSession, args);
        } else {
            // 其他型別
            param = this.method.convertArgsToSqlCommandParam(args);
            result = sqlSession.selectOne(this.command.getName(), param);
        }
        break;
    case FLUSH:
        result = sqlSession.flushStatements();
        break;
    default:
        throw new BindingException("Unknown execution method for: " + this.command.getName());
    }

    if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
        throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
    } else {
        return result;
    }
}
通過上面對 SqlCommand 和 MethodSignature 的簡單分析,我們很容易理解這段程式碼。
  1. 首先,根據 SqlCommand 中解析出來的方法型別選擇對應 SqlSession 中的方法,即如果是 INSERT 型別,則選擇 SqlSession.insert() 方法來執行資料庫操作。
  2. 其次,通過 MethodSignature 將引數值轉換為 Map<Key, Value> 的對映,Key 是方法的引數名稱,Value 是引數值。
  3. 最後,將方法名稱和引數傳入對應的 SqlSession 的方法中執行。至於在組態檔中定義的 SQL 語句,則被快取在 SqlSession 的成員變數 中。

Configuration 中有非常多引數,其中一個是 mappedStatements,它儲存了我們在組態檔中定義的所有方法。還有一個是 mappedStatement,它儲存了我們在組態檔中定義的各種引數,包括 SQL 語句。

到這裡,我們應該對 MyBatis 中如何通過將設定與 Dao 介面對映起來、如何通過代理模式生成代理物件來執行資料庫讀寫操作有了較為宏觀的認識,至於 SqlSession 中如果將引數與 SQL 語句結合組裝成完整的 SQL 語句,以及如何將資料庫欄位與 Java 物件對映,感興趣的小夥伴可以自行分析相關的原始碼。