大型Java進階專題(六)設計模式之代理模式

漂泊在外的程式設計師老王發表於2020-07-02

代理模式

前言

又開始我的專題了,又停滯了一段時間了,加油繼續吧。都知道 SpringAOP 是用代理模式實現,到底是怎麼實現的?我們來一探究竟,並且自己模擬手寫還原部分細節。

代理模式的應用

在生活中,我們經常見到這樣的場景,如:租房中介、售票黃牛、婚介、經紀人、快遞、 事務代理、非侵入式日誌監聽等,這些都是代理模式的實際體現。代理模式(Proxy Pattern)的定義也非常簡單,是指為其他物件提供一種代理,以控制對這個物件的訪問。 代理物件在客服端和目標物件之間起到中介作用,代理模式屬於結構型設計模式。使用 代理模式主要有兩個目的:一保護目標物件,二增強目標象。下面我們來看一下代理 模式的類結構圖:

Subject 是頂層介面,RealSubject 是真實物件(被代理物件),Proxy 是代理物件,代 理物件持有被代理物件的引用,客戶端呼叫代理物件方法,同時也呼叫被代理物件的方 法,但是在代理物件前後增加一些處理。在程式碼中,我們想到代理,就會理解為是程式碼 增強,其實就是在原本邏輯前後增加一些邏輯,而呼叫者無感知。代理模式屬於結構型 模式,有靜態代理和動態代理。

靜態代理

舉個例子:人到了適婚年齡,父母總是迫不及待希望早點抱孫子。而現在社會的人在各 種壓力之下,都選擇晚婚晚育。於是著急的父母就開始到處為自己的子女相親,比子女 自己還著急。這個相親的過程,就是一種我們人人都有份的代理。來看程式碼實現: 

/**
 * 人很多行為,要談戀愛
 */
public interface Person {
    void findLove();
}


/**
 * 兒子需要找物件
 */
public class Son implements Person {

    @Override
    public void findLove() {
        System.out.println("工作沒時間!");
    }
}

/**
 * 父親代理兒子 先幫物色物件
 */
public class Father{

    private Son son;

    //代理物件持有 被代理物件的應用 但沒辦法擴充套件
    private Father(Son son) {
        this.son = son;
    }

    private void findLove() {
        //before
        System.out.println("父母幫物色物件");
        son.findLove();
        //after
        System.out.println("雙方同意交往!");
    }

    //測試程式碼
    public static void main(String[] args) {
        Son son = new Son();
        Father father = new Father(son);
        father.findLove();
    }
}

動態代理

動態代理和靜態對比基本思路是一致的,只不過動態代理功能更加強大,隨著業務的擴 展適應性更強。如果還以找物件為例,使用動態代理相當於是能夠適應複雜的業務場景。 不僅僅只是父親給兒子找物件,如果找物件這項業務發展成了一個產業,進而出現了媒 婆、婚介所等這樣的形式。那麼,此時用靜態代理成本就更大了,需要一個更加通用的 解決方案,要滿足任何單身人士找物件的需求。我們升級一下程式碼,先來看 JDK 實現方式:

JDK 實現方式

建立媒婆(婚介)JDKMeipo 類:

package com.study;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;


public class JdkMeipo implements InvocationHandler {
  	//持有被代理物件的引用
    Object target;

    public Object getInstance(Object target){
        this.target =target;
        Class<?> aClass = target.getClass();
        return Proxy.newProxyInstance(aClass.getClassLoader(),aClass.getInterfaces(),this);
    }
  
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object object = method.invoke(this.target,args);
        after();
        return object;
    }

    private void before() {
        System.out.println("我是婚介,幫你物色物件");
    }

    private void after() {
        System.out.println("已找到,如果合適就開始");
    }
}

建立單身客戶

package com.study;

public class Customer implements Person{

    @Override
    public void findLove() {
        System.out.println("我要找白富美");
    }
}

測試程式碼

package com.study;

public class DemoTest {
    public static void main(String[] args) {
        Person person = (Person) new JdkMeipo().getInstance(new Customer());
        person.findLove();
    }
}

執行效果

Jdk代理的原理

不僅知其然,還得知其所以然。既然 JDK Proxy 功能如此強大,那麼它是如何實現的呢? 我們現在來探究一下原理。 我們都知道 JDK Proxy 採用位元組重組,重新生的物件來替代原始的物件以達到動態代理 的目的。JDK Proxy 生成物件的步驟如下:

1.拿到代理物件的應用,並獲取它的所有介面,反射獲取。

2.通過JDK proxy 類重新生成一個新的類,同時新的類要實現被代理類所有實現的介面。

3.動態生成Java程式碼,把新加的業務邏輯方法由一定的邏輯程式碼去呼叫(在程式碼中體現)。

4.編譯重新生成Java程式碼.class

5.再重新載入的JVM中
以上這個過程就叫位元組重組。

CGLib實現方式

簡單看一下 CGLib 代理的使用,還是以媒婆為例,建立 CglibMeipo 類:

package com.study;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CGlibMeipo implements MethodInterceptor {
    Object target;
    public Object getInstance(Class<?> aClass){
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(aClass);
        enhancer.setCallback(this);
        return enhancer.create();
    }
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object invokeSuper = methodProxy.invokeSuper(o, objects);
        after();
        return invokeSuper;
    }

    private void after() {
        System.out.println("已找到,如果合適就開始");
    }

    private void before() {
        System.out.println("我是婚介,幫你物色物件");
    }
}

測試呼叫

package com.study;

public class DemoTest {
    public static void main(String[] args) {
        //Person person = (Person) new JdkMeipo().getInstance(new Customer());
        Person person = (Person) new CGlibMeipo().getInstance(Customer.class);
        person.findLove();
    }
}

執行結果:(CGLib代理的物件是不需要實現任何介面的,他是通過動態繼承目標物件實現的動態代理。)

CGLib 動態代理執行代理方法效率之所以比 JDK 的高是因為 Cglib 採用了 FastClass 機 制,它的原理簡單來說就是:為代理類和被代理類各生成一個 Class,這個 Class 會為代 理類或被代理類的方法分配一個 index(int 型別)。這個 index 當做一個入參,FastClass 就可以直接定位要呼叫的方法直接進行呼叫,這樣省去了反射呼叫,所以呼叫效率比 JDK 動態代理通過反射呼叫高。

CGLib 和 JDK 動態代理對比

1.JDK 動態代理是實現了被代理物件的介面,CGLib 是繼承了被代理物件。

2.JDK 和 CGLib 都是在執行期生成位元組碼,JDK 是直接寫 Class 位元組碼,CGLib 使用 ASM 框架寫 Class 位元組碼,Cglib 代理實現更復雜,生成代理類比 JDK 效率低。

3.JDK 呼叫代理方法,是通過反射機制呼叫,CGLib 是通過 FastClass 機制直接呼叫方法, CGLib 執行效率更高。

相關文章