Java的代理就是客戶端再也不直接和委託類打交道,而是經過一箇中間層來訪問,這個中間層就是代理。使用代理有兩個好處,一是能夠隱藏委託類的實現;二是能夠實現客戶與委託類之間的解耦,在不修改委託類代碼的狀況下可以作一些額外的處理。java
咱們舉個很常見的例子:工廠會生產不少的玩具,可是咱們買玩具都是到商店買的,而不是到工廠去買的,工廠怎麼生產咱們並不關心,咱們只知道到商店能夠買到本身想要的玩具,而且,若是咱們須要送人的話商店能夠把這些玩具使用禮品盒包裝。這個工廠就是委託類,商店就是代理類,咱們就是客戶類。面試
在Java中有不少場景須要使用代理類,好比遠程RPC調用的時候就是經過代理類去實現的,還有Spring的AOP切面中,咱們也是爲切面生成了一個代理類等等。代理類主要分爲靜態代理、JDK動態代理和CGLIB動態代理,它們各有優缺點,沒有最好的,存在就是有意義的,在不一樣的場景下它們會有不一樣的用途。數據庫
Java的靜態代理安全
靜態代理首先是定義接口和接口的實現類,而後定義接口的代理對象,並將接口的實例注入到代理對象中,而後經過代理對象去調用真正的實現類,實現過程很是簡單也比較容易理解。靜態代理的代理關係在編譯期間就已經肯定了的,適合於代理類較少且肯定的狀況,能夠實如今不修改委託類的狀況下作一些額外的處理,好比包裝禮盒,實現客戶類與委託類的解耦等。可是缺點是隻適用於委託方法少的狀況下,試想一下,若是委託類有幾百上千個方法,豈不是要在代理類中寫一堆的代理方法,因而就有了動態代理來解決這個問題,動態代理在後面說。網絡
定義接口和接口的實現類數據結構
// 委託接口 public interface SayHelloService { void sayHello(String userName); }
// 委託實現類 public class SayHelloServiceImpl implements SayHelloService { public void sayHello(String userName) { System.out.println("hello, " + userName); } }
接口的代理類框架
// 代理類 public class SayHelloProxy implements SayHelloService { private SayHelloService sayHelloService = new SayHelloServiceImpl(); public void sayHello(String userName) { // 代理事前作一些事情 System.out.println("do something before proxy..."); // 調用委託類的方法 sayHelloService.sayHello(userName); // 代理過後作一些事情 System.out.println("do something after proxy..."); } }
經過代理對象訪問委託類ide
// 測試靜態代理類 public class SayHelloTest { public static void main(String[] args) { SayHelloProxy sayHelloProxy = new SayHelloProxy(); sayHelloProxy.sayHello("yanggb"); } }
Java的動態代理技術函數
代理類在程序運行時建立的代理方式,就叫作動態代理。在瞭解動態代理以前,先要回顧JVM的類加載機制中的加載階段要作的三件事情:性能
1.經過一個類的全名或其餘途徑來獲取這個類的二進制字節流。
2.將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
3.在內存中生成一個表明這個類的Class對象,做爲方法區中對這個類訪問的入口。
而動態代理主要就發生在第一個階段,這個階段類的二進制字節流的來源能夠有不少,好比ZIP包、網絡、運行時計算生成、其餘文件生成(JSP)和數據庫獲取等。其中的運行時計算生成就是Java的動態代理技術,在Proxy類中,就是運用了ProxyGenerator.generateProxyClass來爲特定接口生成形式爲*$Proxy的代理類的二進制字節流。所謂的動態代理就是想辦法根據接口或者目標對象計算出代理類的字節碼而後加載進JVM中。實際計算的狀況會很複雜,所以通常是藉助一些諸如JDK動態代理實現、CGLIB第三方庫來完成的。所以爲了讓生成的代理類與目標對象(就是委託類)保持一致,一般有兩種作法:經過接口的JDK動態代理和經過繼承類的CGLIB動態代理。
由於前面說的兩種動態代理都是基於反射來實現的,在運行時查找對象屬性、方法、修改做用域、經過方法名稱調用方法等。而在線的應用不會頻繁使用反射,由於反射的性能開銷較大,所以另外還有使用ASM框架的JAVASSIST,相對開銷很小,下次用另外的篇幅去說。
JDK動態代理
在Java的JDK動態代理中,主要涉及兩個類,一個是java.lang.reflect.Proxy,一個是java.lang.reflectInvocationHandler。要使用JDK的動態代理須要一個實現InvocationHandler接口的中間類,這個接口只有一個方法invoke()方法,對處理類的全部方法的調用都會變成對invoke()方法的調用,這樣就能夠在invoke()方法中添加統一的處理邏輯(也能夠根據method參數判斷是哪一個方法)。中間類(實現了InvocationHandler的類)有一個委託類對象引用,在invoke()方法中調用了委託類對象的相應方法,經過這種聚合的方式持有委託類對象引用,把外部對invoke()方法的調用最終都轉爲對委託類對象的調用。
實際上,中間類與委託類構成了靜態代理關係,在這個關係中,中間類是代理類,委託類是委託類。而後代理類與中間類也構成一個靜態代理關係,在這個關係中,中間類是委託類,代理類是代理類。也就是說,動態代理關係是由兩組靜態代理關係組成的,這就是JDK動態代理的原理。
InvocationHandler接口的定義源碼與參數說明(在註釋中)
public interface InvocationHandler { /** * 調用處理 * @param proxy 代理類對象 * @param methon 標識具體調用的是代理類的哪一個方法 * @param args 代理類方法的參數 */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }
定義接口和接口的實現類
// 委託類接口 public interface HelloService { void sayHello(String userName); void sayByeBye(String userName); }
// 委託類 public class HelloServiceImpl implements HelloService { public void sayHello(String userName) { System.out.println("hello, " + userName); } public void sayByeBye(String userName) { System.out.println("byebye, "+ userName); } }
定義一個實現InvocationHandler接口的中間類
// 中間類 public class HelloInvocationHandler implements InvocationHandler { /** * 中間類持有委託類對象的引用,這裏會構成一種靜態代理關係 */ private Object obj; /** * 有參構造器,傳入委託類的對象 * * @param obj 委託類的對象 */ public HelloInvocationHandler(Object obj) { this.obj = obj; } /** * 動態生成代理類對象,Proxy.newProxyInstance * * @return 返回代理類的實例 */ public Object newProxyInstance() { return Proxy.newProxyInstance( // 指定代理對象的類加載器 obj.getClass().getClassLoader(), // 代理對象須要實現的接口,能夠同時指定多個接口 obj.getClass().getInterfaces(), // 方法調用的實際處理者,代理對象的方法調用都會轉發到這裏 this); } /** * @param proxy 代理對象 * @param method 代理方法 * @param args 方法的參數 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 在invoke前作一些事情 System.out.println("do something before invoke..."); // 執行invoke Object result = method.invoke(obj, args); // 在invoke以後作一些事情 System.out.println("do something after invoke..."); return result; } }
經過中間類訪問委託類
// 測試動態代理類 public class HelloTest { public static void main(String[] args) { HelloInvocationHandler helloInvocationHandler = new HelloInvocationHandler(new HelloServiceImpl()); HelloService helloService = (HelloService) helloInvocationHandler.newProxyInstance(); helloService.sayHello("yanggb"); helloService.sayByeBye("yanggb"); } }
在上面的測試動態代理類中,咱們調用了Proxy類的newProxyInstance()方法來獲取一個代理類示例。這個代理類實現了咱們指定的接口,而且會把方法調用分發到指定的調用處理器。首先經過newProxyInstance()方法獲取代理類的示例,以後就能夠經過這個代理類的實例調用代理類的方法,對代理類的方法調用都會調用中間類(實現了InvocationHandler接口的類)的invoke()方法,在invoke()方法中咱們調用委託類的對應方法,而後加上了本身的處理邏輯。
JDK動態代理最大的特色就是動態生成的代理類和委託類實現同一個接口。JDK動態代理其實內部是經過反射機制(Proxy.newProxyInstance)實現的,也就是已知的一個對象,在運行的時候動態調用它的方法,而且調用的時候還能夠加一些本身的邏輯。
CGLIB動態代理
JDK動態代理依賴接口實現,而當咱們只有類沒有接口的時候,就須要使用另外一種動態代理技術:CGLIB動態代理。首先CGLIB動態代理是第三方框架實現的,須要在Maven工程中引入cglib的包:
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.2</version> </dependency>
CGLIB代理是針對類來實現代理的,原理是對指定的委託類生成一個子類並重寫其中的業務方法來實現代理。代理類對象是由Enhancer類建立的。CGLIB建立動態代理類的模式是:
1.查找目標類上的全部非final的public方法(final的方法不能被重寫)。
2.將這些方法的定義轉爲字節碼。
3.將字節碼轉換成相應的代理的Class對象,而後經過反射得到代理類的實例對象。
4.實現MethodInterceptor接口,用來處理對代理類上全部方法的請求。
定義一個簡單類
// 委託類,是一個簡單類 public class HelloClass { public void sayHello(String userName){ System.out.println("hello, " + userName); } public void sayByeBye(String userName){ System.out.println("byebye, " + userName); } }
定義一個實現MethodInterceptor類的攔截類
// HelloInterceptor 用於對方法調用攔截以及回調 public class HelloInterceptor implements MethodInterceptor { /** * CGLIB 加強類對象,代理類對象是由 Enhancer 類建立的, * Enhancer 是 CGLIB 的字節碼加強器,能夠很方便的對類進行拓展 */ private Enhancer enhancer = new Enhancer(); /** * * @param obj 被代理的對象 * @param method 代理的方法 * @param args 方法的參數 * @param proxy CGLIB方法代理對象 * @return cglib生成用來代替Method對象的一個對象,使用MethodProxy比調用JDK自身的Method直接執行方法效率會有提高 */ public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("方法調用以前"); Object o = proxy.invokeSuper(obj, args); System.out.println("方法調用以後"); return o; } /** * 使用動態代理建立一個代理對象 * @param c 類名 */ public Object newProxyInstance(Class<?> c) { // 設置產生的代理對象的父類,加強類型 enhancer.setSuperclass(c); // 定義代理邏輯對象爲當前對象,要求當前對象實現 MethodInterceptor 接口 enhancer.setCallback(this); // 使用默認無參數的構造函數建立目標對象,這是一個前提,被代理的類要提供無參構造方法 return enhancer.create(); } }
經過攔截類訪問委託類
// 測試類 public class HelloTest { public static void main(String[] args) { HelloInterceptor helloInterceptor = new HelloInterceptor(); HelloClass sayHelloClass = (HelloClass) helloInterceptor.newProxyInstance(HelloClass.class); sayHelloClass.sayHello("yanggb"); sayHelloClass.sayByeBye("yanggb"); } }
對於須要被代理的類,它只是動態生成一個子類以覆蓋非final的方法,同時綁定鉤子回調自定義的攔截器。它比JDK動態代理在性能上要快。值得注意的是,咱們傳入目標類做爲代理類的父類。不一樣於JDK動態代理,咱們不能使用目標對象來建立代理。目標對象只能被CGLIB建立。在例子中,默認的無參構造方法被使用來建立目標對象。
總結
1.靜態代理比較容易理解,須要被代理的類和代理類實現自同一個接口,而後在代理類中調用真正的實現類,而且靜態代理的關係在編譯期間就已經肯定了。而動態代理的關係是在運行期間肯定的。靜態代理實現簡單,適合於代理類較少且肯定的狀況,而動態代理則給咱們提供了更大的靈活性。
2.JDK動態代理所用到的代理類在程序調用到代理類對象時才由JVM真正建立,JVM根據傳進來的業務實現類對象以及方法名,動態地建立了一個代理類的class文件並被字節碼引擎執行,而後經過該代理類對象進行方法調用。咱們須要作的只是指定代理類的預處理、調用後操做便可。
3.靜態代理和動態代理都是基於接口實現的,而對於那些沒有提供接口只是提供了實現類的而言,就只能選擇CGLIB動態代理了。
面試題1:JDK動態代理和CGLIB動態代理的區別
1.JDK動態代理是基於Java反射實現的,必需要實現了接口的業務類才能用這種方法生成代理對象。
2.CGLIB動態代理是基於ASM框架,經過生成業務類的子類來實現的。
3.JDK動態代理的優點是最小化依賴關係,減小依賴意味着簡化開發和維護,而且有JDK自身的支持。還能夠平滑地進行JDK版本升級,代碼實現簡單。
4.基於CGLIB框架的優點是無須實現接口,達到代理類無侵入,咱們只需操做咱們關心的類,沒必要爲其餘相關類增長工做量,性能比較高。
面試題2:描述代理的幾種實現方式,並說出優缺點
代理能夠分爲靜態代理和動態代理,而動態代理又分爲JDK動態代理和CGLIB動態代理。
靜態代理:代理對象和實際對象都繼承了同一個接口,在代理對象中指向的是實際對象的實例,這樣對外暴露的是代理對象而真正調用的是Real Object。優勢是能夠很好地保護實際對象的業務邏輯對外暴露,從而提升安全性。缺點是不一樣的接口要有不一樣的代理類實現,會冗餘。
JDK動態代理:JDK動態代理只須要實現InvocationHandler接口,重寫invoke()方法即可以完成代理的實現,原理是利用反射生成代理類Proxyxx.class代理類字節碼並生成對象。JDK動態代理之因此只能代理接口是由於代理類自己已經繼承了Proxy類,而Java是不容許多重繼承的,只是容許實現多個接口。優勢是解決了靜態代理中冗餘的代理實現類問題。缺點是JDK動態代理是基於接口的設計實現的,若是沒有接口就會拋異常。
CGLIB動態代理:因爲JDK動態代理限制了只能基於接口設計,而對於沒有接口的狀況,JDK方式解決不了。CGLIB採用了很是底層的字節碼技術,其原理是經過字節碼技術爲一個類建立子類,並在子類中採用方法攔截的技術攔截全部父類方法的調用,順勢織入橫切邏輯來完成動態代理的實現。實現的具體方式是實現MethodInterceptor接口並重寫intercept()方法,經過Enhancer類的回調方法來實現。可是CGLIB在建立代理對象時所花費的時間卻比JDK多得多,因此對於單例的對象由於無需頻繁建立對象用CGLIB更合適。反之則是使用JDK動態代理的方式更合適一些。同時,因爲CGLIB是採用的動態建立子類的方法,對於final方法是沒法進行代理的。優勢是沒有接口也可以實現動態代理,並且採用的是字節碼加強技術,性能也不錯。缺點則是技術實現上要相對難理解些。
"室內的燈沒有開,陰沉的陽光透過灰濛濛的窗戶玻璃折射進來,個人世界只剩下了寧靜。"