靜態代理、動態代理與Mybatis的理解

2022-06-21 06:00:18

靜態代理、動態代理與Mybatis的理解

這裡的代理與設計模式中的代理模式密切相關,代理模式的主要作用是為其他物件提供一種控制對這個物件的存取方法,即在一個物件不適合或者不能直接參照另一個物件時,代理物件充當中介的作用。

現實生活中比較貼切的例子比如租房,被代理物件就是房東,代理物件就是中介,使用者就是租客,租客通過中介向房東租賃房屋,即使用者通過代理物件存取被代理物件。

一、直接呼叫

  • 一般我們通過new關鍵字初始化物件來呼叫類中的方法

  • 如下程式碼,建立Human介面,Student類實現了Human介面,在main函數中,通過new關鍵字來初始化Student物件來實現對Student類中say()方法的呼叫

interface Human{
    public void say();
}

class Student implements Human{
    @Override
    public void say() {
        System.out.println("I'm a Student");
    }
}

public class ProxyTest {
    public static void main(String[] args) {
        Human human = new Student();
        human.say();
    }
}

//輸出
//I'm a Student

二、靜態代理

實現靜態代理有以下三個步驟:

  • 建立介面,通過介面來實現物件的代理

  • 建立該介面的實現類

  • 建立Proxy代理類來呼叫我們需要的方法

interface Human{
    public void say();
}

class Student implements Human{

    @Override
    public void say() {
        System.out.println("I'm a Student");
    }
}

class StudentProxy implements Human{
    private Student student;

    public StudentProxy(){}

    public StudentProxy(Student student){
        this.student = student;
    }
    
    private void begin(){
        System.out.println("Begin");
    }
    
    private void end(){
        System.out.println("End");
    }
    
    @Override
    public void say() {
        begin();
        student.say();
        end();
    }
}

public class ProxyTest {
    public static void main(String[] args) {
        Student student = new Student();
        StudentProxy studentProxy = new StudentProxy(student);
        studentProxy.say();
    }
}

//輸出
//Begin
//I'm a Student
//End

在上述程式碼中,我們在沒有修改Student類中say()方法的情況下,實現了在原來的say()方法前後分別執行sayHello()sayBye()方法。由此引出代理模式的主要作用:

  • 在不修改被代理物件的情況下,實現對被代理物件功能的增強

同時,靜態代理也存在一些比較致命的缺點。想象這樣一個場景:若新增一個Worker類實現了Human介面,我們應該如何去代理這個Worker類?比較容易想到的方法是擴大StudentProxy的代理範圍,然後將Worker當作引數傳入StudentProxy,然後繼續使用StudentProxy類代理Worker物件。這樣實現功能是沒有問題的,但會存在如下問題:

  • 當Human介面的範例中方法增加時,代理類中程式碼會變得非常冗長
  • 當有其他不屬於Human類的子類需要被代理時,需要新增一個新的代理類

由此引出動態代理

三、動態代理

使用動態代理時,我們不需要編寫實現類,而是通過JDK提供的Proxy.newProxyInstance()建立一個Human介面的物件。

生成動態代理有以下幾個步驟:

  • 定義一個InvocationHandler範例,它負責實現介面的方法呼叫;
  • 通過Proxy.newProxyInstance()建立interface範例,它需要3個引數:
    • 使用的ClassLoader,通常是介面類的ClassLoader
    • 需要實現的介面陣列,至少需要傳入一個介面進去;
    • 用來處理介面方法呼叫的InvocationHandler範例。
  • 將返回的Object強制轉型為介面。
interface Human{
    public void say();
}

class Student implements Human{

    @Override
    public void say() {
        System.out.println("I'm a Student");
    }
    
    @Override
    public void eat() {
        System.out.println("I eat something");
    }
}

class MyInvocationHandler implements InvocationHandler {
    private Object object;

    public MyInvocationHandler(){}

    public MyInvocationHandler(Object object){
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Begin");
        Object invoke = method.invoke(object, args);
        System.out.println("End");
        return invoke;
    }
}

public class ProxyTest {
    public static void main(String[] args) {
        MyInvocationHandler handler = new MyInvocationHandler(new Student());
        Human human = (Human) Proxy.newProxyInstance(
                Human.class.getClassLoader(),
                new Class[] {Human.class},
                handler);
        human.say();
        human.eat();
    }
}

當Human介面的範例中方法增加時,如新增eat()方法時,只需要在Student類中直接範例化該方法即可。

當有其他不屬於Human類的子類需要被代理時,只需要將傳入MyInvocationHandler()中的new Student()替換為需要被代理的子類即可。

綜上所述,通過動態代理基本可以解決靜態代理的痛點。

四、Mybatis中的動態代理

在Springboot專案中設定Mybatis時,我們僅編寫了Mapper介面,並未編寫Mapper介面的實現類,那麼當我們呼叫Mapper介面中方法時,是如何生成方法體的呢?

首先,專案在啟動時生成MapperFactoryBean物件,通過factory.getObject()方法獲取mapper的代理物件





將上述過程與動態代理的步驟進行對比,我們最終獲取的是一個類似於動態代理例子中Human的代理物件,這裡是MapperProxy的代理物件。至此,一個Mapper代理物件就生成完畢。

然後,當我們完成專案中Mybatis的相關設定後,使用我們Mapper介面中的資料庫相關方法時,將呼叫之前生成的MapperProxy代理物件中invoke()方法。類比動態代理的例子,即呼叫MyInvocationHandler類中的invoke()方法。

//83行程式碼含義:如果method為Object中定義的方法(toString()、hash()...)則直接執行,這裡我們要執行的是Mapper介面中定義的方法,顯然返回為false
Object.class.equals(method.getDeclaringClass())

於是執行cachedInvoker(method)invoke()方法

進入execute()方法,我們看到之前我們設定的mapper.xml在MapperMethod初始化時,被解析成了59行的command。在此處通過sqlSession物件實現了對資料庫的操作。

至此,我們對Mybatis的資料庫操作流程已經有了大致瞭解。回到開頭的問題:為什麼僅編寫了Mapper介面,並未編寫Mapper介面的實現類,仍然可以實現我們的功能?這與我們之前的動態代理例子有什麼區別呢?

研究程式碼我們發現,我們並沒有直接使用method.invoke()方法來呼叫實現類中的方法,而是呼叫了cachedInvoker(method)invoke()方法解析我們設定的Mapper.xml,並通過sqlSession實現了資料庫操作,這個invoke()方法相當於Mybatis自定義的方法。因此,這裡的invoke()方法具體執行的邏輯是根據Mapper.xml設定來生成的,這個Mapper.xml設定可以理解為Mapper介面的實現類。