背景:據說Java設計模式中的代理模式是進入BAT的必經之路。java
一、代理模式:nginx
給某一對象提供一個代理對象,並由代理對象控制對原對象的引用;簡而言之,就是在不改變源代碼的狀況下,實現對目標功能的擴展;好比,你計劃下個月結婚,固然你能夠本身籌劃婚禮的過程,那樣太鬧騰了,因而你就把籌備婚禮的過程交由婚慶公司佈置,而且只須要在婚禮當天使用婚慶公司籌劃婚禮的結果便可。算法
二、代理模式的結構:spring
a、抽象對象角色編程
聲明瞭目標對象和代理對象的共同接口,那麼在任何可使用目標對象的地方也都可使用代理對象;設計模式
b、目標對象角色服務器
定義了代理對象所表明的目標對象;架構
c、代理對象角色app
代理對象內部包含目標對象的引用,So能夠自由地操做目標對象,代理對象提供一個與目標對象相同的接口,So能夠在任什麼時候候替換目標對象;負載均衡
三、靜態代理
從JVM加載類的角度看,靜態代理和動態代理本質上是同樣的,兩者都是在原有類的基礎上,加入一些多出的行爲,甚至徹底替換原有的行爲;而靜態代理採用的方式是咱們手動的將某些行爲進行替換,產生一個新的與原有類接口相同卻行爲不一樣的類型;
以淘寶網爲例,模擬訪問網站的場景;現在你們都常常訪問淘寶網,幾乎全部的Web項目尤爲是像淘寶這樣的大型網站,是不可能採用集中式的架構,使用的必定是分佈式架構,而分佈式架構對於用戶來講,當咱們發起連接的時候,連接指向的並非最終的應用服務器,而是代理服務器如Nginx,用做負載均衡[用戶訪問淘寶網 --> 代理服務器 --> 最終服務器]。
定義一個服務器Server,以及用於獲取頁面標題的方法getPageTitle:
1 /**
2 * 用於獲取網站數據的服務器接口 3 */
4 public interface Server { 5
6 //根據url獲取頁面標題
7 public String getPageTitle(String url); 8
9 }
訪問淘寶網的TaobaoServer,傳入url,獲取頁面標題:
1 /**
2 * 淘寶服務器 3 */
4 public class TaobaoServer implements Server{ 5
6 @Override 7 public String getPageTitle(String url) { 8 //簡單的if..else判斷
9 if ("http://www.taobao.com".equals(url)) { 10 return "淘寶首頁"; 11 }else if ("http://www.tianmao.taobao.com".equals(url)) { 12 return "淘寶-天貓商城"; 13 } 14 return "空空如也"; 15 } 16 }
a、如果不使用代理,那麼用戶訪問就至關於直接 new TaobaoServer()而且調用getPageTitle()方法便可;
b、而因爲分佈式架構的存在,所以須要一個NginxProxy類做爲代理,到時候用戶直接訪問的就是NginxProxy而不直接與TaobaoServer打交道,由NginxProxy負責與最終的TaobaoServer打交道;
NginxProxy代理類:
1 import java.util.List; 2 import java.util.UUID; 3 import com.google.common.collect.Lists; 4
5 /**
6 * Nginx代理 7 */
8 public class NginxProxy implements Server{ 9
10 //淘寶服務器列表
11 private static final List<String> TAOBAO_SERVER_ADDRESSES = Lists.newArrayList("127.0.0.1","127.0.0.2","127.0.0.3"); 12
13 private Server server; 14
15 public NginxProxy(Server server) { 16 this.server = server; 17 } 18
19 @Override 20 public String getPageTitle(String url) { 21 //UUID模擬請求原始Ip,正常狀況下是傳入Request的
22 String remoteIp = UUID.randomUUID().toString(); 23 //路由選擇的算法,這裏簡單的定義爲對remoteIp的Hash之的絕對值取模
24 int index = Math.abs(remoteIp.hashCode()) % TAOBAO_SERVER_ADDRESSES.size(); 25 //選擇淘寶服務器IP
26 String realTaobaoIp = TAOBAO_SERVER_ADDRESSES.get(index); 27 return "【頁面標題: " + server.getPageTitle(url) + "】, 【來源IP: " + realTaobaoIp + "】"; 28
29 } 30 }
用於測試,簡單設計服務器列表幾個IP,同時因爲只是傳入一個url而不是具體的Request,每次隨機一個UUID而且對其hashCode的絕對值取模,模擬該請求被路由到哪臺服務器上:
1 /**
2 * 靜態代理測試類 3 */
4 public class StaticProxyTest { 5 public static void main(String[] args) { 6 //訪問淘寶服務器
7 TaobaoServer taobaoServer = new TaobaoServer(); 8 //訪問Nginx代理
9 NginxProxy nginxProxy = new NginxProxy(taobaoServer); 10 System.out.println(nginxProxy.getPageTitle("http://www.taobao.com")); 11 } 12 }
因爲淘寶服務器和代理服務器實際上都是服務器,So它們可使用相同的接口Server;用戶不和最終目標對象角色TaobaoServer打交道,而是與代理對象角色NginxProxy打交道,由代理對象角色NginxProxy控制用戶的訪問;屢次運行來源IP會改變,其結果以下:
四、靜態代理的缺點
靜態代理的的特色是靜態代理的代理類是由程序猿建立的,在程序運行以前靜態代理的.class文件就已經存在了;靜態代理在代理量較小時還OK,可是代理量增長就會存在兩個比較明顯的缺點:
a、靜態代理的內容,即NginxProxy路由的選擇這幾段代碼,只能服務一路Server該接口,而不能服務於其餘接口,如果其餘接口想要使用這幾行代碼如新增一個代理類,而不斷的新增,又因爲靜態代理的內容沒法複用,則必然會形成靜態代理類過於龐大;
b、Server接口中如果新增了一個方法,如getPageInfo(String url)方法,而實際對象實現了這個方法,代理對象也必須新增getPageInfo(String url)方法,爲其增長代理內容;
五、動態代理
因爲靜態代理的侷限性,孕育而生了動態代理;靜態代理是死的,在咱們編譯期間即按下CTRL+S的那一刻就給被代理對象生成了一個不可動態改變的代理類,而動態代理就是在運行期間動態生成代理類的一種更爲靈活的代理模式;動態代理是JDK自帶的功能,它須要實現一個InvocationHandler接口,並調用Proxy的靜態方法產生代理類。
Nginx InvocationHandler:
1 import java.lang.reflect.InvocationHandler; 2 import java.lang.reflect.Method; 3 import java.util.List; 4 import java.util.UUID; 5 import com.google.common.collect.Lists; 6 /**
7 * Nginx InvocationHandler 8 */
9 public class NginxInvovationHandler implements InvocationHandler { 10
11 //淘寶服務器列表
12 private static final List<String> TAOBAO_SERVER_ADDRESSES = Lists.newArrayList("127.0.0.1","127.0.0.2","127.0.0.3"); 13
14 private Object object; 15
16 public NginxInvovationHandler(Object object) { 17 this.object = object; 18 } 19
20 @Override 21 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 22 String remoteIp = UUID.randomUUID().toString(); 23 int index = Math.abs(remoteIp.hashCode()) % TAOBAO_SERVER_ADDRESSES.size(); 24 String realTaobaoIp = TAOBAO_SERVER_ADDRESSES.get(index); 25 //執行速度:StringBuilder > StringBuffer > String
26 StringBuilder sb = new StringBuilder(); 27 sb.append(" 【頁面標題: "); 28 sb.append(method.invoke(object, args)); 29 sb.append("】,【來源Ip: "); 30 sb.append(realTaobaoIp); 31 sb.append("】"); 32 return sb.toString(); 33
34 } 35 }
上面的動態代理NginxInvovationHandler 將選擇服務器的邏輯抽象成公共的代碼,由於調用的是Object中的method,而Object又是全部類的超類/父類,So並不僅限定於接口Server,任意的其餘接口均可以使用該服務,所以該NginxInvovationHandler能夠靈活的各處複用。
動態代理測試:
1 import java.lang.reflect.InvocationHandler; 2 import java.lang.reflect.Proxy; 3
4 /**
5 * 動態代理測試類 6 */
7 public class DynamicProxyTest { 8 public void testDynamicProxy(String url) { 9 TaobaoServer taobaoServer = new TaobaoServer(); 10 InvocationHandler invocationHandler = new NginxInvovationHandler(taobaoServer); 11 //使用Proxy的newProxyInstance方法能夠產生對目標接口對一個代理,代理內容則由InvocationHandler實現
12 Server proxy = (Server)Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[] {Server.class}, invocationHandler); 13 System.out.println(proxy.getPageTitle(url)); 14 } 15 public static void main(String[] args) { 16 String url = "http://www.taobao.com"; 17 DynamicProxyTest dynamicProxyTest = new DynamicProxyTest(); 18 dynamicProxyTest.testDynamicProxy(url); 19 } 20
21 }
動態代理程序運行結果:
六、動態代理的優/缺點
優勢:
a、減小了類的數量,看起來比較直觀;
b、代理內容能夠複用;
c、最重要的是動態代理能夠在不修改源代碼的基礎上在源代碼上進行操做,如在吐司上抹果醬的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(一種字節碼加強技術)來爲某一個類實現代理。
七、GCLIB動態代理
HelloService業務類:
1 /**
2 * 沒有實現任何接口的業務類 3 */
4 public class HelloService { 5
6 public HelloService() { 7 System.out.println("HelloService構造"); 8 } 9
10 //該方法不被覆蓋,cglib沒法代理由final修飾的方法
11 final public String sayOthers(String name) { 12 System.out.println("HelloService : sayOthers >> " + name); 13 return null; 14 } 15
16 public void sayHello() { 17 System.out.println("HelloService : sayHello"); 18 } 19
20 }
自定義MethodInterceptor類:
1 import java.lang.reflect.Method; 2 import org.springframework.cglib.proxy.MethodInterceptor; 3 import org.springframework.cglib.proxy.MethodProxy; 4
5 /**
6 * 自定義MethodInterceptor 7 */
8 public class MyMethodInterceptor implements MethodInterceptor{ 9
10 /**
11 * sub: cglib生成的代理對象 12 * method: 被代理對象方法 13 * objects: 方法入參 14 * methodProxy: 代理方法 15 */
16 @Override 17 public Object intercept(Object sub, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { 18 System.out.println("<<<<<<<<<<<<插入前置通知>>>>>>>>>>>>"); 19 Object object = methodProxy.invokeSuper(sub, objects); 20 System.out.println("<<<<<<<<<<<<插入後置通知>>>>>>>>>>>>"); 21 return object; 22 } 23
24 }
CGLIB代理對象調用目標方法:
1 import org.assertj.core.internal.cglib.core.DebuggingClassWriter; 2 import org.springframework.cglib.proxy.Enhancer; 3
4 /**
5 * CGLIB代理對象調用目標方法 6 */
7 public class Client { 8 public static void main(String[] args) { 9 //代理類class文件存入本地磁盤,方便反編譯查看源碼
10 System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/TJT/Code"); 11 //經過cglib動態代理 獲取代理對象的過程
12 Enhancer enhancer = new Enhancer(); 13 //設置enhance 對象的父類
14 enhancer.setSuperclass(HelloService.class); 15 //設置enhance 的回調對象
16 enhancer.setCallback(new MyMethodInterceptor()); 17 //建立代理對象
18 HelloService proxy = (HelloService)enhancer.create(); 19 //經過代理對象調用目標方法
20 proxy.sayHello(); 21 System.out.println(); 22 //sayOthers() 被fianl修飾不能被cglib代理
23 proxy.sayOthers("濤姐濤哥"); 24 System.out.println(); 25 proxy.sayHello(); 26 } 27
28 }
CGLIB動態代理程序運行結果以下: