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