Mybatis之介面程式設計--JAVA動態代理的最佳展現

atd681發表於2018-09-03

0. 前言

Mybatis是非常流行的ORM開發框架. 由最初的IBatis到今天的MyBatis, 功能越來越強大, 開發越來越便捷. 尤其是基於介面的開發, 使得開發者在編寫DAO時可以節省了很多時間. JAVA中介面不能被例項化, 因此不能直接呼叫介面. 那麼Mybatis是如何實現介面呼叫的呢?

本文將帶領大家瞭解Mybatis實現介面程式設計的關鍵 -- JAVA動態代理

1. Mybatis2.x

很早以前使用Mybatis2.x版本編寫DAO時, 我們需要這麼寫:

public interface UserDAO {
    Integer selectUserCount();
}
複製程式碼
public class IbatisUserDAO extends SqlMapClientDaoSupport implements UserDAO {

    public Integer selectUserCount() {
        return (Integer) this.getSqlMapClientTemplate().queryForObject("selectUserCount");
    }
    
}
複製程式碼
<sqlMap namespace="User">

    <select id="selectUserCount" resultClass="int">
        select count(*) from t_user
    </select>

</sqlMap> 
複製程式碼

每個DAO介面都需要實現類並手動呼叫Mybatis的元件, Mybatis通過傳入的ID找到SQL並執行.

2. Mybatis3.x

Mybatis3.x開始支援基於介面程式設計, 開發者只需要定義DAO中的資料介面, 在Mapper中編寫對應的SQL即可. 從此開發者不在需要手動呼叫Mybatis的元件, 這不僅節約了開發時間(無需提供DAO實現類), 更使得開發者不需要關心Mybatis的細節, 可以把更多精力放在業務資料介面上.

使用Mybatis3.x版本時編寫DAO時, 我們需要這麼寫:

public interface UserDAO {
    Integer selectUserCount();
}
複製程式碼
<sqlMap namespace="User">

    <select id="selectUserCount" resultClass="int">
        select count(*) from t_user
    </select>

</sqlMap> 
複製程式碼

3. 動態代理

Mybatis中的DAO定義的是介面, 沒有實現類. 但是在JAVA中是無法直接呼叫介面, 那Mybatis是如何實現的基於介面程式設計的呢? 首先我們先熟悉一種設計模式:

3.1 代理模式

設計模式中有一種模式叫做代理模式: 假如有人欠你錢, 那麼你會找一個律師幫你打官司. 需要談判時會由你的律師幫你和對方進行談判. 那麼律師就相當於代理類, 你相當於委託類, 代理類全權代理你. 雙方談判時由代理類出面, 談好了需要簽字畫押時才需要你出手. 這就叫做代理模式.

為什麼需要代理類呢? 很簡單: 你不會談判, 律師比你更專業.

3.2 靜態代理

JAVA中有我們下面會講到的動態代理, 相比之下才有靜態代理一說, 靜態代理指的就是代理模式. 我們以上述的律師代理談判為例, 編寫一段程式碼:

你和律師都能談判和簽名, 因此定義一個含有談判和簽名方法的介面, 定義你和律師兩個類實現介面:

// 介面: 人
public interface Person {

    // 談判
    void talk();

    // 簽名
    void sign();

}

複製程式碼
// 你
public class You implements Person{

    // 你的談判
    @Override
    public void talk() {
        System.out.println("你開始談判");
    }

    // 你的簽名
    @Override
    public void sign() {
        System.out.println("你開始簽名");
    }

}
複製程式碼
// 律師
public class Lawer implements Person {

    // 委託人(被代理人)
    private Person client;

    // 建立律師時必須指定委託人
    public Lawer(Person client) {
        this.client = client;
    }

    // 律師的談判
    @Override
    public void talk() {

        // 談判不需要委託人出面, 由律師完成
        // 因此不需要呼叫委託人的talk方法
        System.out.println("律師開始談判");

    }

    // 律師的簽名
    @Override
    public void sign() {

        // 律師不能代替委託人簽名
        // 需要呼叫委託人的sign方法
        client.sign();

    }

}
複製程式碼
  • 律師是代理別人去談判, 因此律師類中必須有委託人. 構造時傳入委託人.
  • 律師談判時不需要委託人出面, 因此不需要呼叫委託人的talk方法.
  • 簽名時必須由委託人簽名, 因此需要呼叫委託人的sign方法.

上述場景構建完成, 新建測試類:

public class Test {

    public static void main(String[] args) {

        // 需要談判
        // 你找個律師代理你
        Person lawer = new Lawer(new You());

        // 開始談判
        lawer.talk();

        // 需要簽名
        lawer.sign();

    }

}
複製程式碼

當你需要談判時, 建立的是代理類(律師)並將委託類(你)傳入代理類中, 所有的操作都是對代理類(代理你的律師)進行操作. 總結一下代理模式的實現方式:

  • 代理類與委託類實現同一介面.
  • 代理類中需要指定委託類(即代理類代理了誰).
  • 需要委託類時不直接返回委託類, 構建代理類返回.

使用代理模式有如下好處:

  • 你不需要學習談判的技巧, 這些都是律師去做的, 你只需要做自己做的事.
  • 你的朋友也需要談判時, 只需讓律師代理你的朋友即可, 他同樣不需要學習談判技巧.
  • 你的很多朋友都委託律師時, 如果有新的談判技巧, 只需要律師一人學習就可以.
  • 如果你覺得律師不好時, 只需要換一個律師. 你什麼都不需要改變.
  • 當有很多人找你談判, 都會先找到律師, 由律師和你對接, 你是很安全的.

上述代理模式的優勢可以歸納為以下幾點:

  • 許可權控制: 代理類負責控制委託類的訪問許可權. 即需要先和律師談判, 由律師決定找不找你.
  • 業務解耦: 降低委託類的業務複雜度. 即你不需要學習很多知識, 比如談判技巧.
  • 便於維護: 代理類實現通用業務, 委託類實現差異化. 通用業務變化時只修改代理類即可.

但如果你沒有使用代理模式, 你會發現你需要不停的學習各種談判技巧, 你的朋友需要談判時你需要教給他怎麼談判, 很多人直接和你談判時你會很累...

3.3 JAVA動態代理

代理模式雖然優勢很多, 但有這樣一個問題: 由於代理類與委託類都是實現同一介面, 當介面變化時, 所有代理類和委託類都需要相應的修改(隨介面增加或刪除方法). 比如上例介面增加了一個談判必備的喝酒功能, 你和律師都需要在各自類中增加相應方法.

Mybatis2.x時很多專案都定義了DAO介面和實現類, 當業務頻繁變化時需要同步修改介面與實現類.

代理模式核心就是返回委託類的代理類, 如果能夠做到當呼叫委託類的方法時自動執行代理類的方法, 代理模式就會完美很多. JAVA通過反射機制提供了這種方式, 叫做動態代理.

動態代理可以實現: 當呼叫Person介面中的任一方法時, 自動呼叫代理類中的方法.

3.3.1 建立動態代理類

動態代理類必須實現java.lang.reflect.InvocationHandler介面的invoke方法, 當呼叫Person介面中任一方法時都將執行動態代理類的invoke方法, 可以將代理類的業務邏輯寫在invoke方法中.

// 動態代理類
// 需要實現InvocationHandler介面的invoke方法
public class PersonProxy implements InvocationHandler {

    // 當呼叫被代理的介面的任何方法時都會執行到該方法
    // proxy: 被呼叫的委託者物件(Person)
    // method: 委託者被呼叫的方法物件(Person中被呼叫的方法)
    // args: 委託者被呼叫的的方法的引數
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(method.getName());
        return null;
    }

}

複製程式碼

3.3.2 獲取動態代理類

呼叫java.lang.reflect.Proxy類中的newProxyInstance方法獲取動態代理類, 此時獲取到的代理類是Person介面的代理類. 可以呼叫Person中的方法.

public static void main(String[] args) {

    // 動態代理類
    PersonProxy proxy = new PersonProxy();

    // 呼叫Proxy.newProxyInstance可獲取, newProxyInstance引數如下:
    // 引數1: 生成代理類的類載入器
    // 引數2: 代理的介面, JAVA根據介面在生成的代理類中新增介面的實現方法(執行引數3的invoke)
    // 引數3: 實現InvocationHandler的動態代理類,當呼叫引數2介面中的方法時會執行該類的invoke方法
    Person person = (Person) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), 
                                                    new Class[] { Person.class }, 
                                                    proxy);

    // 呼叫介面的方法, 會自動執行到動態代理類的invoke中
    person.talk();
    person.sign();

}
複製程式碼

獲取到的代理類為呼叫newProxyInstance時傳入的第二個引數的代理類, 本例中傳入Person介面, 獲取的到代理類為Person的代理類, Person為委託類.

3.3.3 動態代理原理

從程式的結構分析: 動態代理和靜態代理相比, 少定義了一個實現Person介面的代理類. 實際上JAVA動態代理會自動生成代理模式所需的實現Person介面的代理類. 過程簡述如下:

  1. 呼叫Proxy.newProxyInstance獲取代理類時第二個引數傳入介面Person.
  2. JAVA會自動生成一個代理類並實現Person介面中的所有方法.
  3. 每個方法的實現過程為通過反射呼叫第三個引數傳入的PersonProxy中的invoke方法
  4. 將生成的代理類返回. 代理類實現了Person介面, 因此代理類中有Person介面的所有方法.

JAVA動態代理自動生成的代理類, 示例如下:

// JAVA動態代理生成的代理類
public final class $Proxy0 extends Proxy implements Person {

    // 實現Person的方法
    @Override
    public void talk() {
        // 通過反射呼叫PersonProxy的invoke方法
        // this.h為建立代理類時傳入的PersonProxy物件(第三個引數)
        // m為PersonProxy的invoke方法
        this.h.invoke(this, m, null);

    }

    // 實現Person的方法
    @Override
    public void sign() {
        this.h.invoke(this, m, null);
    }

}
複製程式碼

當JAVA動態代理生成完代理類後, 是不是和靜態代理的實現方式完全一樣了呢.

4. Mybatis實現原理

熟悉了JAVA動態代理, 我們來看一下Mybatis介面程式設計的實現原理.

  1. 獲取DAO(Spring注入)時, 實際獲取到的是DAO的代理類.
  2. 程式碼中呼叫DAO中的selectUserCount方法, 實際上呼叫的是DAO代理類的方法.
  3. 代理類的selectUserCount方法中通過反射呼叫動態代理類(PersonProxy)的invoke方法.
  4. 動態代理類(PersonProxy)的invoke方法可以獲取DAO中被執行的方法名: selectUserCount.
  5. 通過selectUserCountMapper檔案中找到對應的SQL並執行

5. 總結

JAVA動態代理的優勢在是Mybatis介面程式設計中體現的淋漓盡致. 本文介紹了代理模式並結合動態代理簡述了Mybatis介面程式設計的實現原理, Mybatis真正的實現要複雜的多. 當明白了其原理後, 當檢視Mybatis原始碼時可以少走很多彎路.

下一篇文章會結合專案實際應用案例介紹Mybatis攔截器的使用方式, 歡迎關注.

相關文章