java動態代理

自在现实發表於2024-06-24

代理模式
代理模式的英文叫做Proxy或Surrogate,中文都可譯為“代理”,所謂代理,就是一個人或者一個機構代表另一個人或者另一個機構採取行動。在一些情況下,一個客戶不想或者不能夠直接引用一個物件,而代理物件可以在客戶端和目標物件之間起到中介的作用 。代理就是為其他物件提供一個代理以控制對某個物件的訪問。比如火車票代售點就是一個代理,它控制要買火車票的人(其他物件)對火車站售票點(某個物件)的訪問,由它向火車票售票點買票。

   代理模式簡要圖示如下:


   代理模式的優點

優點一:可以隱藏真實目標類的實現;
優點二:可以實現客戶與真實目標類間的解耦,在不修改真實目標類程式碼的情況下能夠做一些額外的處理。
代理模式:代理類和被代理類實現共同的介面(或繼承),代理類中存有指向被代理類的索引,實際執行時透過呼叫代理類的方法、實際執行的是被代理類的方法。

   如上圖所示,Client類就是客戶端,Proxy類就是代理類,RealSubject類是真實目標類,Proxy和RealSubject類為客戶提供的服務能力都體現在DoAction()方法中,Proxy的DoAction()實際上是在呼叫RealSubject的DoAction方法,當然Proxy類的DoAction()在呼叫RealSubject的DoAction方法前後也可以有一些自定義的操作,比如在列印日誌等,這樣一個流程就是體現了簡單的代理思想。

   代理又可以分為靜態代理和動態代理。

靜態代理:由程式設計師建立或特定工具自動生成原始碼,再對其編譯。在程式執行前,代理類的.class檔案就已經存在了。
動態代理:在程式執行時,運用反射機制動態建立而成,動態代理類的位元組碼在程式執行時由Java反射機制動態生成,無需程式設計師手工編寫它的原始碼。
舉一個實際的例子,考慮一個字型提供功能,字型庫可能源自本地磁碟、網路或者系統。 先考慮從本地磁碟中獲取字型,和上面的例子一樣,採用代理的方式實現,定義一個提供字型的介面FontProvider:

public interface FontProvider {
Font getFont(String name);
}
1
2
3
真正提供獲取磁碟字型庫的類:

class FontProviderFromDisk implements FontProvider {
@Override
Font getFont(String name){
System.out.println("磁碟上的字型庫");
return null;
}
}
1
2
3
4
5
6
7
代理類ProxyForFont:

class ProxyForFont implements FontProvider {
private FontProvider fontProvider;
ProxyForFont(FontProvider fontProvider) {
this.fontProvider=fontProvider;
}
@Override
Font getFont(String name) {
System.out.println("呼叫代理方法前可以做點事情");
Font font = fontProvider.getFont(name);
System.out.println("呼叫代理方法後可以再做點事情");
return font;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
當我們需要從磁碟獲取字型庫時,直接呼叫ProxyForFont就可以了:

public class MyFontProvider {
public static void main(String[] args) {
FontProvider fp = new ProxyForFont(new FontProviderFromDisk());
fp.getFont("字型庫名");
}
}
1
2
3
4
5
6
這樣實現的好處在哪兒呢?比如,每次從磁碟獲取字型庫的時候,磁碟的I/O比較耗時,想透過快取將讀到的字型庫暫存一份。此時,我們直接修改ProxyForFont類而不用去修改真正的目標類FontProviderFromDisk:

class ProxyForFont implements FontProvider {
private FontProvider fontProvider;
ProxyForFont(FontProvider fontProvider) {
this.fontProvider = fontProvider;
}
@Override
Font getFont(String name){
System.out.println("檢查磁碟快取中是否存在字型庫");
if (exist) {
Font font = fontProvider.getFont(name);
System.out.println("將磁碟讀到的字型庫儲存到快取");
return font;
} else {
System.out.println("如果存在直接從快取中獲取");
return null;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
直接修改FontProviderFromDisk類的getFont方法,也能達到相同的效果,但是這樣會有一個問題,上文中我們提到,字型庫的獲取源除了磁碟還有系統和網路等,所以還存在FontProviderFromSystem和FontProviderFromNet兩個類,如果這兩個類也需要快取功能的時候,還得再繼續動這兩個類的getFont實現,而如果使用了代理模式,不僅能實現客戶與目標的解耦,還可以在不修改真實目標類程式碼的情況下能夠做一些額外的處理,即只需要在代理類ProxyForFont中修改即可。

從靜態代理到動態代理

   以上都是靜態代理的實現方式,是不是感覺靜態代理已經無所不能了呢?我們再來看一個需求。

   以上都是獲取字型庫,如果想獲取圖片、音樂等其他資源呢?這個時候一個FontProvider介面就不夠用了,還得提供ImageProvider和MusicProvider介面,實現對應的兩個功能類以及兩個代理類ProxyForImage和ProxyForMusic。當要給獲取圖片和獲取音樂等都加上快取功能的時候,兩個代理類ProxyForImage和ProxyForMusic都需要改動,而快取的邏輯三個類又是相同的,如此寫程式碼就會出現重複和代理類爆炸。當你要代理的方法越多時,你需要重複的邏輯就越多,假設你的目標類有100個方法,那麼你的代理類就需要對這100個方法進行委託,但是又可能他們前後需要執行的邏輯時一樣的,這樣就會產生很多冗餘。 這樣,就有個更好的動態代理的方法出現了。

   動態代理也分為兩類:基於介面的代理和基於繼承的代理;兩類實現的代表分別是:JDK代理與CGlib代理。

JDK代理
JDK動態代理主要涉及java.lang.reflect包下的Proxy類和InvocationHandler介面。 JDK代理實現的三個要點:

透過java.lang.reflect.Proxy類來動態生成代理類;
代理類要實現InvocationHandler介面;
JDK代理只能基於介面進行動態代理;
每一個動態代理類都必須要實現InvocationHandler這個介面,並且每個代理類例項都關聯到了一個handler,當我們透過代理物件呼叫一個方法的時候,這個方法的呼叫就會被轉發為由InvocationHandler這個介面的 invoke 方法來進行呼叫。我們來看看InvocationHandler這個介面的唯一一個方法 invoke 方法:

/**

  • proxy:   指代我們所代理的那個真實物件
  • method:  指代的是我們所要呼叫真實物件的某個方法的Method物件
  • args:   指代的是呼叫真實物件某個方法時使用的引數
    */
    Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

1
2
3
4
5
6
7
Proxy這個類的作用就是用來動態建立一個代理物件的類,它提供了許多的方法,但是我們用的最多的就是 newProxyInstance 這個方法,這個方法的作用就是得到一個動態的代理物件,其接收三個引數,我們來看看這三個引數所代表的含義。

/**

  • loader:
  • 一個ClassLoader物件,定義了由哪個ClassLoader物件來對生成的代理物件
  • 進行載入
  • interfaces:
  • 一個Interface物件的陣列,表示的是我將要給我需要代理的物件提供一組什麼
  • 介面,如果我提供了一組介面給它,這樣我就能呼叫這組介面中的方法了
  • h:
  • 一個InvocationHandler物件,表示的是當我這個動態代理物件在呼叫方法的
  • 時候,會關聯到哪一個InvocationHandler物件上
  • /
    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
JDK動態代理例項。

//subject介面,這個是jdk動態代理必須的前提。
public interface Subject {
void request();
void hello();
}
1
2
3
4
5
定義業務類,實現該介面

//目標物件RealSubject
public class RealSubject implements Subject {
@Override
public void request() {
System.out.println("real subject execute request");
}
@Override
public void hello() {
System.out.println("hello");
}
}
1
2
3
4
5
6
7
8
9
10
11
定義代理類

//代理類JdkProxySubject
public class JdkProxySubject implements InvocationHandler {

//這個就是我們要代理的真實物件
private Object subject;
//構造方法,給我們要代理的真實物件賦初值
public JdkProxySubject(Object subject) {
    this.subject = subject;
}

/*
*invoke方法方法引數解析
*Object proxy:指被代理的物件。 
*Method method:要呼叫的方法 
*Object[] args:方法呼叫時所需要的引數 
*InvocationHandler介面的子類可以看成代理的最終操作類。
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("before");
    Object result = null;
    try {
        //利用反射動態的來反射方法,這就是動態代理和靜態代理的區別
        result = method.invoke(subject,args);
    } catch (Exception e) {
        System.out.println("ex:"+e.getMessage());
        throw e;
    } finally {
        System.out.println("after");
    }
    return result;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
定義客戶端類,執行業務邏輯時使用代理類

//客戶端Client
public class Client {
/*
*newProxyInstance方法引數解析
*ClassLoader loader:類載入器
*Class<?>[] interfaces:得到全部的介面
*InvocationHandler h:得到InvocationHandler介面的子類例項
*/
public static void main(String[] args) {
//我們要代理的真實物件
Subject realSubject = new RealSubject();
//我們要代理哪個真實物件,就將該物件傳進去
InvocationHandler handler = new JdkProxySubject(realSubject);
Subject subject = (Subject) Proxy.newProxyInstance(Client.class.getClassLoader(),new Class[]{Subject.class}, handler);
subject.hello();
subject.request();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
輸出:

before
hello
after
before
real subject execute request
after
1
2
3
4
5
6
因為利用JdkProxySubject生成的代理類實現了介面,所以目標類中所有的方法在代理類中都有。現在,如果我們在subject介面中新增加了一個goodBye()方法,然後再RealSubject中對goodBye()方法進行實現,但是在代理類中,我們不需要再去為goodBye()方法再去寫一個代理方法,而是透過反射呼叫目標物件的方法,來動態的生成代理類。

代理的本質其實就是一種對行為的監聽,對代理物件($proxy InvocationHandler)的一種監聽行為。

CGlib代理模式
CGLib底層採用ASM位元組碼生成框架,使用位元組碼技術生成代理類,為一個類建立子類,並在子類中採用方法攔截的技術攔截所有對父類方法的呼叫,並順勢加入橫切邏輯。CGlib是針對類來實現代理的,原理是對指定的業務類生成一個子類,並覆蓋其中業務方法實現代理,因為採用的是繼承,所以不能對final修飾的類進行代理。CGlib和JDK的原理類似,也是透過方法去反射呼叫目標物件的方法。

//目標物件RealSubject,cglib不需要為目標類定義介面,當然目標類實現了介面也不影響
public class RealSubject {
public void request() {
System.out.println("real subject execute request");
}

public void hello() {
    System.out.println("hello");
}

}
1
2
3
4
5
6
7
8
9
10
//實現MethodInterceptor方法代理介面,建立代理類
public class DemoCglibProxy implements MethodInterceptor {

private Object target;//業務類物件,供代理方法中進行真正的業務方法呼叫

//相當於JDK動態代理中的繫結
public Object getInstance(Object target) { 
	//給業務物件賦值 
    this.target = target;  
    //建立加強器,用來建立動態代理類
    Enhancer enhancer = new Enhancer(); 
    //為加強器指定要代理的業務類(即:為下面生成的代理類指定父類)
    enhancer.setSuperclass(this.target.getClass());  
    //設定回撥:對於代理類上所有方法的呼叫,都會呼叫CallBack,而Callback則需要實現intercept()方法進行攔
    enhancer.setCallback(this); 
   // 建立動態代理類物件並返回  
   return enhancer.create(); 
}

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    System.out.println("before in cglib");
    Object result = null;
    try{
        result = proxy.invokeSuper(obj, args);
    }catch (Exception e){
        System.out.println("get ex:"+e.getMessage());
        throw e;
    }finally {
        System.out.println("after in cglib");
    }
    return result;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
建立業務類和代理類物件,然後透過代理類物件的getInstance(業務類物件) 返回一個動態代理類物件(它是業務類的子類,可以用業務類引用指向它),最後透過動態代理類物件進行方法呼叫。

//客戶端
public class Client {
public static void main(String[] args) {
// 此刻,realSubject不是單純的目標類,而是增強過的目標類
RealSubject realSubject = ((RealSubject))new DemoCglibProxy().getInstance(new RealSubject());
realSubject.hello();
realSubject.request()
}
}
1
2
3
4
5
6
7
8
9
輸出:

before in cglib
hello
after in cglib
before in cglib
real subject execute request
after in cglib
1
2
3
4
5
6
Cglib是無需透過介面來實現,它是透過實現子類的方式來完成呼叫的。Enhancer物件把代理物件設定為被代理類的子類來實現動態代理的。因為是採用繼承方式,所以代理類不能加final修飾,否則會報錯。被final修飾的類不能被繼承,內部的方法和變數都變成final型別。

Spring如何選擇用JDK還是CGLib
當Bean實現介面時,Spring就會用JDK的動態代理;
當Bean沒有實現介面時,Spring使用CGlib的代理實現;
可以透過修改配置檔案強制使用CGlib;
CGLib底層採用ASM位元組碼生成框架,使用位元組碼技術生成代理類,比使用Java反射效率要高。唯一需要注意的是,CGLib不能對宣告為final的類和方法進行代理,因為CGLib原理是動態生成被代理類的子類。代理模式也是我們必須要理解的一種模式,因為學習好代理模式有助於我們去讀一些原始碼,排查一些更深層次的問題,或者面對一些業務場景問題,也能有一個很大的提升,設計模式本身也就是為了解決問題而建立出來的。
————————————————

                        版權宣告:本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連結和本宣告。

原文連結:https://blog.csdn.net/fuzhongmin05/article/details/81260715

相關文章