您好,登錄后才能下訂單哦!
這篇文章主要講解了“動態代理知識總結”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“動態代理知識總結”吧!
代理模式是一種設計模式,提供了對目標對象額外的訪問方式,即通過代理對象訪問目標對象,這樣可以在不修改原目標對象的前提下,提供額外的功能操作,擴展目標對象的功能
一個比方:在租房的時候,有的人會通過房東直租,有的人會通過中介租房。
這兩種情況哪種比較方便呢?當然是通過中介更加方便。
這里的中介就相當于代理,用戶通過中介完成租房的一系列操作(看房、交押金、租房、清掃衛生)代理模式可以有效的將具體的實現與調用方進行解耦,通過面向接口進行編碼完全將具體的實現隱藏在內部。
分類:
靜態代理: 在編譯時就已經實現,編譯完成后代理類是一個實際的class文件
動態代理: 在運行時動態生成的,即編譯完成后沒有實際的class文件,而是在運行時動態生成類字節碼,并加載到JVM中
使用方式
創建一個接口,然后創建被代理的類實現該接口并且實現該接口中的抽象方法。之后再創建一個代理類,同時使其也實現這個接口。在代理類中持有一個被代理對象的引用,而后在代理類方法中調用該對象的方法。
public interface UserDao { void save(); }
public class UserDaoImpl implements UserDao { @Override public void save() { System.out.println("正在保存用戶..."); } }
public class TransactionHandler implements UserDao { //目標代理對象 private UserDao target; //構造代理對象時傳入目標對象 public TransactionHandler(UserDao target) { this.target = target; } @Override public void save() { //調用目標方法前的處理 System.out.println("開啟事務控制..."); //調用目標對象的方法 target.save(); //調用目標方法后的處理 System.out.println("關閉事務控制..."); } }
public class Main { public static void main(String[] args) { //新建目標對象 UserDaoImpl target = new UserDaoImpl(); //創建代理對象, 并使用接口對其進行引用 UserDao userDao = new TransactionHandler(target); //針對接口進行調用 userDao.save(); } }
使用JDK靜態代理很容易就完成了對一個類的代理操作。但是JDK靜態代理的缺點也暴露了出來:由于代理只能為一個類服務,如果需要代理的類很多,那么就需要編寫大量的代理類,比較繁瑣
使用JDK動態代理的五大步驟:
通過實現InvocationHandler接口來自定義自己的InvocationHandler;
通過Proxy.getProxyClass獲得動態代理類;
通過反射機制獲得代理類的構造方法,方法簽名為getConstructor(InvocationHandler.class);
通過構造函數獲得代理對象并將自定義的InvocationHandler實例對象傳為參數傳入;
通過代理對象調用目標方法;
public interface IHello { void sayHello(); }
public class HelloImpl implements IHello { @Override public void sayHello() { System.out.println("Hello world!"); }
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class MyInvocationHandler implements InvocationHandler { /** 目標對象 */ private Object target; public MyInvocationHandler(Object target){ this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("------插入前置通知代碼-------------"); // 執行相應的目標方法 Object rs = method.invoke(target,args); System.out.println("------插入后置處理代碼-------------"); return rs; } }
import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; public class MyProxyTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException { // =========================第一種========================== // 1、生成$Proxy0的class文件 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); // 2、獲取動態代理類 Class proxyClazz = Proxy.getProxyClass(IHello.class.getClassLoader(),IHello.class); // 3、獲得代理類的構造函數,并傳入參數類型InvocationHandler.class Constructor constructor = proxyClazz.getConstructor(InvocationHandler.class); // 4、通過構造函數來創建動態代理對象,將自定義的InvocationHandler實例傳入 IHello iHello1 = (IHello) constructor.newInstance(new MyInvocationHandler(new HelloImpl())); // 5、通過代理對象調用目標方法 iHello1.sayHello(); // ==========================第二種============================= /** * Proxy類中還有個將2~4步驟封裝好的簡便方法來創建動態代理對象, *其方法簽名為:newProxyInstance(ClassLoader loader,Class<?>[] instance, InvocationHandler h) */ IHello iHello2 = (IHello) Proxy.newProxyInstance(IHello.class.getClassLoader(), // 加載接口的類加載器 new Class[]{IHello.class}, // 一組接口 new MyInvocationHandler(new HelloImpl())); // 自定義的InvocationHandler iHello2.sayHello(); } }
JDK靜態代理與JDK動態代理之間有些許相似,比如說都要創建代理類,以及代理類都要實現接口等。
不同之處: 在靜態代理中我們需要對哪個接口和哪個被代理類創建代理類,所以我們在編譯前就需要代理類實現與被代理類相同的接口,并且直接在實現的方法中調用被代理類相應的方法;但是動態代理則不同,我們不知道要針對哪個接口、哪個被代理類創建代理類,因為它是在運行時被創建的。
一句話來總結一下JDK靜態代理和JDK動態代理的區別:
JDK靜態代理是通過直接編碼創建的,而JDK動態代理是利用反射機制在運行時創建代理類的。
其實在動態代理中,核心是InvocationHandler。每一個代理的實例都會有一個關聯的調用處理程序(InvocationHandler)。對待代理實例進行調用時,將對方法的調用進行編碼并指派到它的調用處理器(InvocationHandler)的invoke方法
對代理對象實例方法的調用都是通過InvocationHandler中的invoke方法來完成的,而invoke方法會根據傳入的代理對象、方法名稱以及參數決定調用代理的哪個方法。
CGLIB包的底層是通過使用一個小而快的字節碼處理框架ASM,來轉換字節碼并生成新的類
CGLIB代理實現如下:
首先實現一個MethodInterceptor,方法調用會被轉發到該類的intercept()方法。
然后在需要使用的時候,通過CGLIB動態代理獲取代理對象。
使用案例
public class HelloService { public HelloService() { System.out.println("HelloService構造"); } /** * 該方法不能被子類覆蓋,Cglib是無法代理final修飾的方法的 */ final public String sayOthers(String name) { System.out.println("HelloService:sayOthers>>"+name); return null; } public void sayHello() { System.out.println("HelloService:sayHello"); } }
import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * 自定義MethodInterceptor */ public class MyMethodInterceptor implements MethodInterceptor{ /** * sub:cglib生成的代理對象 * method:被代理對象方法 * objects:方法入參 * methodProxy: 代理方法 */ @Override public Object intercept(Object sub, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("======插入前置通知======"); Object object = methodProxy.invokeSuper(sub, objects); System.out.println("======插入后者通知======"); return object; } }
import net.sf.cglib.core.DebuggingClassWriter; import net.sf.cglib.proxy.Enhancer; public class Client { public static void main(String[] args) { // 代理類class文件存入本地磁盤方便我們反編譯查看源碼 System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\code"); // 通過CGLIB動態代理獲取代理對象的過程 Enhancer enhancer = new Enhancer(); // 設置enhancer對象的父類 enhancer.setSuperclass(HelloService.class); // 設置enhancer的回調對象 enhancer.setCallback(new MyMethodInterceptor()); // 創建代理對象 HelloService proxy= (HelloService)enhancer.create(); // 通過代理對象調用目標方法 proxy.sayHello(); } }
JDK代理要求被代理的類必須實現接口,有很強的局限性。
而CGLIB動態代理則沒有此類強制性要求。簡單的說,CGLIB會讓生成的代理類繼承被代理類,并在代理類中對代理方法進行強化處理(前置處理、后置處理等)。
總結一下CGLIB在進行代理的時候都進行了哪些工作
生成的代理類繼承被代理類。在這里我們需要注意一點:如果委托類被final修飾,那么它不可被繼承,即不可被代理;同樣,如果委托類中存在final修飾的方法,那么該方法也不可被代理
代理類會為委托方法生成兩個方法,一個是與委托方法簽名相同的方法,它在方法中會通過super調用委托方法;另一個是代理類獨有的方法
當執行代理對象的方法時,會首先判斷一下是否存在實現了MethodInterceptor接口的CGLIB$CALLBACK_0;,如果存在,則將調用MethodInterceptor中的intercept方法
在intercept方法中,我們除了會調用委托方法,還會進行一些增強操作。在Spring AOP中,典型的應用場景就是在某些敏感方法執行前后進行操作日志記錄
在CGLIB中,方法的調用并不是通過反射來完成的,而是直接對方法進行調用:通過FastClass機制對Class對象進行特別的處理,比如將會用數組保存method的引用,每次調用方法的時候都是通過一個index下標來保持對方法的引用
CGLIB采用了FastClass的機制來實現對被攔截方法的調用。
FastClass機制就是對一個類的方法建立索引,通過索引來直接調用相應的方法
public class test10 { //這里,tt可以看作目標對象,fc可以看作是代理對象;首先根據代理對象的getIndex方法獲取目標方法的索引, //然后再調用代理對象的invoke方法就可以直接調用目標類的方法,避免了反射 public static void main(String[] args){ Test tt = new Test(); Test2 fc = new Test2(); int index = fc.getIndex("f()V"); fc.invoke(index, tt, null); } } class Test{ public void f(){ System.out.println("f method"); } public void g(){ System.out.println("g method"); } } class Test2{ public Object invoke(int index, Object o, Object[] ol){ Test t = (Test) o; switch(index){ case 1: t.f(); return null; case 2: t.g(); return null; } return null; } //這個方法對Test類中的方法建立索引 public int getIndex(String signature){ switch(signature.hashCode()){ case 3078479: return 1; case 3108270: return 2; } return -1; } }
上例中,Test2是Test的Fastclass,在Test2中有兩個方法getIndex和invoke。
在getIndex方法中對Test的每個方法建立索引,并根據入參(方法名+方法的描述符)來返回相應的索引。
Invoke根據指定的索引,以ol為入參調用對象O的方法。這樣就避免了反射調用,提高了效率
三種代理方式之間對比
代理方式 | 實現 | 優點 | 缺點 | 特點 |
---|---|---|---|---|
JDK靜態代理 | 代理類與委托類實現同一接口,并且在代理類中需要硬編碼接口 | 實現簡單,容易理解 | 代理類需要硬編碼接口,在實際應用中可能會導致重復編碼,浪費存儲空間并且效率很低 | 好像沒啥特點 |
JDK動態代理 | 代理類與委托類實現同一接口,主要是通過代理類實現InvocationHandler并重寫invoke 方法來進行動態代理的,在invoke方法中將對方法進行增強處理 | 不需要硬編碼接口,代碼復用率高 | 只能夠代理實現了接口的委托類 | 底層使用反射機制進行方法的調用 |
CGLIB動態代理 | 代理類將委托類作為自己的父類并為其中的非final委托方法創建兩個方法,一個是與委托方法簽名相同的方法,它在方法中會通過super 調用委托方法;另一個是代理類獨有的方法。在代理方法中,它會判斷是否存在實現了MethodInterceptor 接口的對象,若存在則將調用intercept方法對委托方法進行代理 | 可以在運行時對類或者是接口進行增強操作,且委托類無需實現接口 | 不能對final 類以及final方法進行代理 | 底層將方法全部存入一個數組中,通過數組索引直接進行方法調用 |
CGlib比JDK快?
使用CGLiB實現動態代理,CGLib底層采用ASM字節碼生成框架,使用字節碼技術生成代理類, 在jdk6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能對聲明為final的方法進行代理, 因為CGLib原理是動態生成被代理類的子類。
在jdk6、jdk7、jdk8逐步對JDK動態代理優化之后,在調用次數較少的情況下,JDK代理效率高于CGLIB代理效率。只有當進行大量調用的時候,jdk6和jdk7比CGLIB代理效率低一點,但是到jdk8的時候,jdk代理效率高于CGLIB代理,總之,每一次jdk版本升級,jdk代理效率都得到提升,而CGLIB代理消息確有點跟不上步伐。
Spring如何選擇用JDK還是CGLIB?
當Bean實現接口時,Spring就會用JDK的動態代理。
當Bean沒有實現接口時,Spring使用CGlib實現。
可以強制使用CGlib
感謝各位的閱讀,以上就是“動態代理知識總結”的內容了,經過本文的學習后,相信大家對動態代理知識總結這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。