代理模式
代理模式的定義很簡單:給某一物件提供一個代理物件,並由代理物件控制對原物件的引用。
代理模式的結構
有些情況下,一個客戶不想或者不能夠直接引用一個物件,可以通過代理物件在客戶端和目標物件之間起到中介作用。代理模式中的角色有:
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(一種位元組碼增強技術)來為某一個類實現代理了。