Java設計模式9:代理模式

五月的倉頡發表於2015-10-25

代理模式

代理模式的定義很簡單:給某一物件提供一個代理物件,並由代理物件控制對原物件的引用

 

代理模式的結構

有些情況下,一個客戶不想或者不能夠直接引用一個物件,可以通過代理物件在客戶端和目標物件之間起到中介作用。代理模式中的角色有:

1、抽象物件角色

宣告瞭目標物件和代理物件的共同介面,這樣一來在任何可以使用目標物件的地方都可以使用代理物件

2、目標物件角色

定義了代理物件所代表的目標物件

3、代理物件角色

代理物件內部含有目標物件的引用,從而可以在任何時候操作目標物件;代理物件提供一個與目標物件相同的介面,以便可以在任何時候替代目標物件

 

靜態代理示例

這裡模擬的是作為訪問網站的場景,以新浪網舉例。我們通常訪問新浪網,幾乎所有的Web專案尤其是新浪這種大型網站,是不可能採用集中式的架構的,使用的一定是分散式的架構,分散式架構對於使用者來說,我們發起連結的時候,連結指向的並不是最終的應用伺服器,而是代理伺服器比如Nginx,用以做負載均衡。

所以,我們的例子,簡化來說就是使用者訪問新浪網-->代理伺服器-->最終伺服器。先定義一個伺服器介面Server,簡單定義一個方法,用於獲取頁面標題:

 1 /**
 2  * 伺服器介面,用於獲取網站資料
 3  */
 4 public interface Server {
 5 
 6     /**
 7      * 根據url獲取頁面標題
 8      */
 9     public String getPageTitle(String url);
10     
11 }

我們訪問的是新浪網,所以寫一個SinaServer,傳入url,獲取頁面標題:

 1 /**
 2  * 新浪伺服器
 3  */
 4 public class SinaServer implements Server {
 5 
 6     @Override
 7     public String getPageTitle(String url) {
 8         if ("http://www.sina.com.cn/".equals(url)) {
 9             return "新浪首頁";
10         } else if ("http://http://sports.sina.com.cn/".equals(url)) {
11             return "新浪體育_新浪網";
12         }
13         
14         return "無頁面標題";
15     }
16     
17 }

這裡寫得比較簡單,就做了一個if..else if判斷,大家理解意思就好。寫到這裡,我們說明兩點:

  • 如果不使用代理,那麼使用者訪問相當於就是直接new SinaServer()出來並且呼叫getPageTitle(String url)方法即可
  • 由於分散式架構的存在,因此我們這裡要寫一個NginxProxy,作為一個代理,到時候使用者直接訪問的是NginxProxy而不是和SinaServer打交道,由NginxProxy負責和最終的SinaServer打交道

因此,我們寫一個NginxProxy:

 1 /**
 2  * Nginx代理
 3  */
 4 public class NginxProxy implements Server {
 5     
 6     /**
 7      * 新浪伺服器列表
 8      */
 9     private static final List<String> SINA_SERVER_ADDRESSES = Lists.newArrayList("192.168.1.1", "192.168.1.2", "192.168.1.3"); 
10 
11     private Server server;
12     
13     public NginxProxy(Server server) {
14         this.server = server;
15     }
16     
17     @Override
18     public String getPageTitle(String url) {
19         // 這裡就簡單傳了一個url,正常請求傳入的是Request,使用UUID模擬請求原始Ip
20         String remoteIp = UUID.randomUUID().toString();
21         // 路由選擇演算法這裡簡單定義為對remoteIp的Hash值的絕對值取模
22         int index = Math.abs(remoteIp.hashCode()) % SINA_SERVER_ADDRESSES.size();
23         // 選擇新浪伺服器Ip
24         String realSinaIp = SINA_SERVER_ADDRESSES.get(index);
25         
26         return "【頁面標題:" + server.getPageTitle(url) + "】,【來源Ip:" + realSinaIp + "】";
27     }
28     
29 }

這裡同樣為了簡單起見,伺服器列表寫死幾個ip,同時由於只傳一個url而不是具體的Request,每次隨機一個UUID,對UUID的HashCode絕對值取模,模擬這次請求被路由到哪臺伺服器上。

呼叫方這麼寫:

 1 /**
 2  * 靜態代理測試
 3  */
 4 public class StaticProxyTest {
 5 
 6     @Test
 7     public void testStaticProxy() {
 8         Server sinaServer = new SinaServer();
 9         Server nginxProxy = new NginxProxy(sinaServer);
10         System.out.println(nginxProxy.getPageTitle("http://www.sina.com.cn/"));
11     }
12     
13 }

第8行表示的是要訪問的是新浪伺服器,第9行表示的是使用者實際訪問的是Nginx代理而不是真實的新浪伺服器,由於新浪伺服器和代理伺服器實際上都是伺服器,因此他們可以使用相同的介面Server。

程式最終執行的結果為:

【頁面標題:新浪首頁】,【來源Ip:192.168.1.2

當然,多執行幾次,來源Ip一定是會變的,這就是一個靜態代理的例子,即使用者不和最終目標物件角色(SinaServer)打交道,而是和代理物件角色(NginxProxy)打交道,由代理物件角色(NginxProxy)控制使用者的訪問

 

靜態代理的缺點

靜態代理的特點是靜態代理的代理類是程式設計師建立的,在程式執行之前靜態代理的.class檔案已經存在了

從靜態代理模式的程式碼來看,靜態代理模式確實有一個代理物件來控制實際物件的引用,並通過代理物件來使用實際物件。這種模式在代理量較小的時候還可以,但是代理量一大起來,就存在著兩個比較大的缺點:

1、靜態代理的內容,即NginxProxy的路由選擇這幾行程式碼,只能服務於Server介面而不能服務於其他介面,如果其它介面想用這幾行程式碼,比如新增一個靜態代理類。久而久之,由於靜態代理的內容無法複用,必然造成靜態代理類的不斷龐大

2、Server介面裡面如果新增了一個方法,比如getPageData(String url)方法,實際物件實現了這個方法,代理物件也必須新增方法getPageData(String url),去給getPageData(String url)增加代理內容(假如需要的話)

 

利用JDK中的代理類Proxy實現動態代理的示例

由於靜態代理的侷限性,所以產生了動態代理的概念。

上面的例子我們採用動態代理的方式,動態代理的核心就是將公共的邏輯抽象到InvocationHandler中。關於動態代理,JDK本身提供了支援,因此實現一下InvocationHandler介面:

 1 /**
 2  * Nginx InvocationHandler
 3  */
 4 public class NginxInvocationHandler implements InvocationHandler {
 5 
 6     /**
 7      * 新浪伺服器列表
 8      */
 9     private static final List<String> SINA_SERVER_ADDRESSES = Lists.newArrayList("192.168.1.1", "192.168.1.2", "192.168.1.3"); 
10     
11     private Object object;
12     
13     public NginxInvocationHandler(Object object) {
14         this.object = object;
15     }
16     
17     @Override
18     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
19         String remoteIp = UUID.randomUUID().toString();
20         int index = Math.abs(remoteIp.hashCode()) % SINA_SERVER_ADDRESSES.size();
21         String realSinaIp = SINA_SERVER_ADDRESSES.get(index);
22         
23         StringBuilder sb = new StringBuilder();
24         sb.append("【頁面標題:");
25         sb.append(method.invoke(object, args));
26         sb.append("】,【來源Ip:");
27         sb.append(realSinaIp);
28         sb.append("】");
29         return sb.toString();
30     }
31     
32 }

這裡就將選擇伺服器的邏輯抽象成為了公共的程式碼了,因為呼叫的是Object裡面的method,Object是所有類的超類,因此並不限定非要是Sever,A、B、C都是可以的,因此這個NginxInvocationHandler可以靈活地被各個地方給複用。

呼叫的時候這麼寫:

 1 /**
 2  * 動態代理測試
 3  */
 4 public class DynamicProxyTest {
 5 
 6     @Test
 7     public void testDynamicProxy() {
 8         Server sinaServer = new SinaServer();
 9         InvocationHandler invocationHandler = new NginxInvocationHandler(sinaServer);
10         Server proxy = (Server)Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{Server.class}, invocationHandler);
11         
12         System.out.println(proxy.getPageTitle("http://www.sina.com.cn/"));
13     }
14     
15 }

Proxy本身也是JDK提供給開發者的,使用Proxy的newProxyInstance方法可以產生對目標介面的一個代理,至於代理的內容,即InvocatoinHandler的實現。

看一下執行結構,和靜態代理是一樣的:

【頁面標題:新浪首頁】,【來源Ip:192.168.1.2

動態代理寫法本身有點不好理解,需要開發者多實踐,多思考,才能真正明白動態代理的含義及其實際應用。

 

動態代理的優點

1、最直觀的,類少了很多

2、代理內容也就是InvocationHandler介面的實現類可以複用,可以給A介面用、也可以給B介面用,A介面用了InvocationHandler介面實現類A的代理,不想用了,可以方便地換成InvocationHandler介面實現B的代理

3、最重要的,用了動態代理,就可以在不修改原來程式碼的基礎上,就在原來程式碼的基礎上做操作,這就是AOP即面向切面程式設計

 

動態代理的缺點

動態代理有一個最大的缺點,就是它只能針對介面生成代理,不能只針對某一個類生成代理,比方說我們在呼叫Proxy的newProxyInstance方法的時候,第二個引數傳某個具體類的getClass(),那麼會報錯:

Exception in thread "main" java.lang.IllegalArgumentException: proxy.DynamicHelloWorldImpl is not an interface

這是因為java.lang.reflect.Proxy的newProxyInstance方法會判斷傳入的Class是不是一個介面:

...
/*
  * Verify that the Class object actually represents an
  * interface.
  */
 if (!interfaceClass.isInterface()) {
 throw new IllegalArgumentException(
    interfaceClass.getName() + " is not an interface");
}
...

而實際使用中,我們為某一個單獨的類實現一個代理也很正常,這種情況下,我們就可以考慮使用CGLIB(一種位元組碼增強技術)來為某一個類實現代理了。

相關文章