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

靜態代理、動態代理與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介面的實現類。

Tags: