靜態代理和動態代理

2020-10-13 01:01:16
靜態代理就是按照代理模式書寫的程式碼,如《代理模式》一節中的範例,其特點是代理類和目標類在程式碼中是確定的,因此稱為靜態。靜態代理可以在不修改目標物件功能的前提下,對目標功能進行擴充套件。

但是靜態代理顯然不夠靈活,這時就需要動態代理。

動態代理也叫 JDK 代理或介面代理,有以下特點:
  • 代理物件不需要實現介面
  • 代理物件的生成是利用 JDK 的 API 動態的在記憶體中構建代理物件
  • 能在程式碼執行時動態地改變某個物件的代理,並且能為代理物件動態地增加方法、增加行為

一般情況下,動態代理的底層不用我們親自去實現,可以使用執行緒提供的 API 。例如,在 Java 生態中,目前普遍使用的是 JDK 自帶的代理和 GGLib 提供的類庫。

JDK 實現代理只需要使用 newProxyInstance 方法,該方法需要接收三個引數,語法格式如下:

static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h )

注意該方法在 Proxy 類中是靜態方法,且接收的三個引數說明依次為:
  • ClassLoader loader:指定當前目標物件使用類載入器,獲取載入器的方法是固定的
  • Class<?>[] interfaces:目標物件實現的介面的型別,使用泛型方式確認型別
  • InvocationHandler h:事件處理,執行目標物件的方法時,會觸發事件處理器的方法,把當前執行目標物件的方法作為引數傳入

下面根據範例介紹靜態代理和動態代理。

眾所周知,程式設計非常培養孩子的邏輯思維能力。很多父母為了不讓孩子輸在起跑線上,就開始到處為孩子找輔導老師。下面來看程式碼實現。

建立頂層介面 IPerson,程式碼如下:
public interface IPerson {
    void findTeacher(); //找老師
}
兒子張三要找老師,實現 IPerson 介面,ZhangSan 類程式碼如下:
public class ZhangSan implements IPerson {

    @Override
    public void findTeacher() {
        System.out.println("兒子張三提出要求");
    }
}
父親張老三要幫兒子張三找老師,實現 IPerson 介面,ZhangLaoSan 類程式碼如下:
public class ZhangLaoSan implements IPerson {
    private ZhangSan zhangsan;

    public ZhangLaoSan(ZhangSan zhangsan) {
        this.zhangsan = zhangsan;
    }

    @Override
    public void findTeacher() {
        System.out.println("張老三開始找老師");
        zhangsan.findTeacher();
        System.out.println("開始學習");
    }
}
新建 Test 類測試程式碼。
public class Test {
    public static void main(String[] args) {
        ZhangLaoSan zhanglaosan = new ZhangLaoSan(new ZhangSan());
        zhanglaosan.findTeacher();
    }
}
執行結果如下所示:

張老三開始找老師
兒子張三提出要求
開始學習

上面的場景有個弊端,就是自己的父親只會幫自己的子女去挑選輔導老師,別人家的孩子是不會管的。於是社會上這樣業務發展成了一個產業,出現了答疑、輔導班、培訓機構等,還有各種各樣的客製化套餐。

這樣如果還是用靜態代理成本就太高了,需要一個更加通用的解決方案,滿足任何想學習程式設計找老師的需求。這就由靜態代理升級到了動態代理。採用動態代理基本上只要是人(IPerson)就可以提供找老師服務。

下面基於 JDK 動態代理支援來升級一下程式碼。

首先建立輔導班類 JdkFuDao。
public class JdkFuDao implements InvocationHandler {
    private IPerson target;

    public IPerson getInstance(IPerson target) {
        this.target = target;
        Class<?> clazz = target.getClass();
        return (IPerson) Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(this.target, args);
        after();
        return result;
    }

    private void after() {
        System.out.println("雙方同意,開始輔導");
    }

    private void before() {
        System.out.println("這裡是C語言中文網輔導班,已經收集到您的需求,開始挑選老師");
    }
}
然後建立一個類 ZhaoLiu。
public class ZhaoLiu implements IPerson {
    @Override
    public void findTeacher() {
        System.out.println("符合趙六的要求");
    }

    public void buyInsure() {

    }
}
需要注意的是,代理物件不需要實現介面,但是目標物件一定要實現介面,否則不能用動態代理。

最後使用者端測試程式碼如下:
public class Test {
    public static void main(String[] args) {
        JdkFuDao jdkFuDao = new JdkFuDao();

        IPerson zhaoliu = jdkFuDao.getInstance(new ZhaoLiu());
        zhaoliu.findTeacher();
    }
}
執行結果如下所示:

這裡是C語言中文網輔導班,已經收集到您的需求,開始挑選老師
符合趙六的要求
雙方同意,開始輔導

靜態代理和動態代理的區別

靜態代理和動態代理主要有以下幾點區別:
  • 靜態代理只能通過手動完成代理操作,如果被代理類增加了新的方法,則代理類需要同步增加,違背開閉原則。
  • 動態代理採用在執行時動態生成程式碼的方式,取消了對被代理類的擴充套件限制,遵循開閉原則。
  • 若動態代理要對目標類的增強邏輯進行擴充套件,結合策略模式,只需要新增策略類便可完成,無需修改代理類的程式碼。