?作者簡介:大家好,我是Leo,熱愛Java后端開發者,一個想要與大家共同進步的男人😉😉
🍎個人主頁:Leo的博客
💞當前專欄:每天一個知識點
?特色專欄: MySQL學習
🥭本文內容:第06天 靜態代理和動態代理
🖥?個人小站 :個人博客,歡迎大家訪問
📚個人知識庫: 知識庫,歡迎大家訪問
在了解靜態代理和動態代理之前,我們需要先了解一下什么是代理模式
1. 代理模式
代理模式是一種比較好理解的設計模式。簡單來說就是 我們使用代理對象來代替對真實對象(real object)的訪問,這樣就可以在不修改原目標對象的前提下,提供額外的功能操作,擴展目標對象的功能。
代理模式的主要作用是擴展目標對象的功能,比如說在目標對象的某個方法執行前后你可以增加一些自定義的操作。
代理模式:
為一個對象提供一個替身,以控制對這個對象的訪問。即通過代理對象訪問目標對象.這樣做的好處是:可以在目標對象實現的基礎上,增強額外的功能操作,即擴展目標對象的功能。- 被代理的對象可以是遠程對象、創建開銷大的對象或需要安全控制的對象
舉個例子:當我們工作之后需要出去租房子,房東張貼廣告帶我看房子,最后簽合同,但是房東只想坐著簽合同并不想到處跑著看房子,于是就找了一個中介專門來宣傳廣告并且帶租戶看房子,而房東只負責簽合同收錢!中介在這里就可以看作是代理你的代理對象
,代理的行為(方法)
是帶租戶看房子。
代理模式有靜態代理和動態代理兩種實現方式,我們 先來看一下靜態代理模式的實現。
2.靜態代理
靜態代理中,我們對目標對象的每個方法的增強都是手動完成的,非常不靈活(比如接口一旦新增加方法,目標對象和代理對象都要進行修改)且麻煩(需要對每個目標類都單獨寫一個代理類)。 實際應用場景非常非常少,日常開發幾乎看不到使用靜態代理的場景。
上面我們是從實現和應用角度來說的靜態代理,從 JVM 層面來說, 靜態代理在編譯時就將接口、實現類、代理類這些都變成了一個個實際的 class 文件。
靜態代理實現步驟:
- 定義一個接口及其實現類;
- 創建一個代理類同樣實現這個接口
- 將目標對象注入進代理類,然后在代理類的對應方法調用目標類中的對應方法。這樣的話,我們就可以通過代理類屏蔽對目標對象的訪問,并且可以在目標方法執行前后做一些自己想做的事情。
下面通過代碼來進一步了解靜態代理的應用
-
定義一個賣房的接口
public interface UserService {String sell(String message); }
-
實現賣房接口
public class UserServiceImpl implements UserService{public String sell(String message) {System.out.println("我是房東-->我要賣房,找我簽合同");return message;} }
-
創建代理類實現賣房接口
public class UserServiceProxy implements UserService{private UserService userService;public UserServiceProxy(UserService userService) {this.userService = userService;}@Overridepublic String sell(String message) {System.out.println("我是中介,我可以帶你去看房");userService.sell(message);//調用方法之后,我們同樣可以添加自己的操作System.out.println("after method send()");return null;} }
-
測試
@org.junit.Testpublic void name() {UserServiceImpl userService = new UserServiceImpl();UserServiceProxy userServiceProxy = new UserServiceProxy(userService);userServiceProxy.sell("賣房");} }
控制臺打印 :
靜態代理存在的問題
1. 靜態類文件數量過多,不利于項目管理UserServiceImpl UserServiceProxyOrderServiceImpl OrderServiceProxy
2. 額外功能維護性差代理類中 額外功能修改復雜(麻煩)
3. 動態代理
相比于靜態代理來說,動態代理更加靈活。我們不需要針對每個目標類都單獨創建一個代理類,并且也不需要我們必須實現接口,我們可以直接代理實現類( CGLIB 動態代理機制
)。
從 JVM 角度來說,動態代理是在運行時動態生成類字節碼,并加載到 JVM 中的。
說到動態代理,Spring AOP、RPC 框架應該是兩個不得不提的,它們的實現都依賴了動態代理。
動態代理在我們日常開發中使用的相對較少,但是在框架中的幾乎是必用的一門技術。學會了動態代理之后,對于我們理解和學習各種框架的原理也非常有幫助。
就 Java 來說,動態代理的實現方式有很多種,比如 JDK 動態代理、CGLIB 動態代理等等。
概念:通過代理類為原始類(目標類)增加額外功能,代理類由Spring動態生成。
好處:利于原始類(目標類)的維護
代理運行對象在程序運行的過程中動態的在內存進行構建,可以靈活的進行業務功能的切換。
3.1 JDK動態代理
JDK動態代理是基于 Java 的反射機制實現的。使用 JDK中接口和類實現代理對象的動態創建。JDK的動態代理要求目標對象必須實現接口,而代理對象不必實現業務接口,這是 java 設計上的要求。從 jdk1.3 以來,java 語言通過 java.lang.reflect 包提供三個類和接口支持代理模式,它們分別Proxy, Method和 InvocationHandler。
-
目標對象必須實現業務接口
-
JDK代理代理對象不需要實現業務接口
-
動態代理的對象在程序運行中不存在
-
動態代理靈活的進行業務功能的切換
1. Proxy類
? 通過JDK的
java.lang.reflect.Proxy
類實現動態代理,會使用其靜態方法newProxyInstance()
,依據目標對象、業務接口及調用處理器三者,自動生成一個動態代理對象。
public static newProxyInstance ( ClassLoader loader, Class<?>[] interfaces,InvocationHandler handler)loader:目標類的類加載器,通過目標對象的反射可獲取
interfaces:目標類實現的接口數組,通過目標對象的反射可獲取
handler:調用處理器。
2. Method類
invoke()
方法的第二個參數為Method
類對象,該類有一個方法也叫 invoke(),可以調用目標方法。這兩個 invoke()方法,雖然同名,但無關。
public Object invoke ( Object obj, Object... args)obj:表示目標對象
args:表示目標方法參數,就是其上一層 invoke 方法的第三個參數
該方法的作用是:調用執行 obj 對象所屬類的方法,這個方法由其調用者 Method 對象確定。在代碼中,一般的寫法為
method.invoke(target, args);
其中,method 為上一層 invoke 方法的第二個參數。這樣,即可調用了目標類的目標方法。
3. IocationHandler接口
InvocationHandler 接口叫做調用處理器,負責完成調用目標方法,并增強功能。通過代理對象執行目標接口中的方法 , 會把方法的調用分派給調用處理器(InvocationHandler)的實現類,執行實現類中的 invoke()方法,我們需要把功能代理寫在 invoke()方法中 。此接口中只有一個方法。
在
invoke
方法中可以截取對目標方法的調用。在這里進行功能增強。Java 的動態代理是建立在反射機制之上的。實現了InvocationHandler
接口的類用于加強目標類的主業務邏輯。這個接口中有一個方法invoke()
,具體加強的代碼邏輯就是定義在該方法中的。通過代理對象執行接口中的方法時,會自動調用 invoke()方法。
4. 實現步驟
-
代理對象不需要實現接口。
-
代理對象的生成是利用JDK中的proxy類,動態的在內存中構建代理對象。
-
代碼實現接口
-
ProxyFactory.java
代理實例生成工廠package com.leo.demo02.service.impl;import com.leo.demo02.service.UserService;import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy;/*** @author : Leo* @version 1.0* @date 2023/8/14 13:40* @description :*/ public class ProxyFactory {/**任何的代理對象,都要清楚目標對象,在此得設置一個目標對象*/private UserService userService;//傳入目標對象public ProxyFactory(UserService userService){this.userService = userService;}/** 給目標對象生成代理實例 */public Object getProxyInstance(){return Proxy.newProxyInstance(//指定當前目標對象,使用類加載器獲得userService.getClass().getClassLoader(),//獲得目標對象實現的所有接口userService.getClass().getInterfaces(),//處理代理實例上的方法并返回調用結果new InvocationHandler() {@Overridepublic Object invoke(//代理對象的實例Object proxy,//代理的目標對象的實現方法Method method,//代理的目標對象實現方法的參數Object[] args) throws Throwable {System.out.println("我是中介,我在張貼廣告......");System.out.println("我是中介,我在帶領租客看房子......");//目標對象執行自己的方法Object returnValue = method.invoke(userService, args);System.out.println("我是中介,我在帶領租客辦理結束流程");return returnValue;}});} }
-
測試類
@org.junit.Testpublic void test03() {// 創建代理工廠對象ProxyFactory factory = new ProxyFactory(new UserServiceImpl());UserService service = (UserService) factory.getProxyInstance();service.sell("賣房");}
控制臺打印 :
-
注意:JDK動態代理中,代理對象不需要實現接口,但是目標對象一定要實現接口,否則不能用JDK動態代理。
3.2 CGlib動態代理
Code Generation Library ,又稱為子類通過動態的的在內存中構建子類對象,重寫父類方法進行代理功能的增強。
想要功能擴展,但目標對象沒有實現接口,怎樣功能擴展?
解決方案:子類的方式
Class subclass extends UserDao{}
以子類的方式實現(cglib代理),在內存中構建一個子類對象從而實現對目標對象功能的擴展。
1. CGlib動態代理的特點
- JDK動態代理有一個限制,就是使用動態代理的目標對象必須實現一個或多個接口。如果想代理沒有實現類的接口,就可以使用CGLIB進行代理。
- CGLIB是一個強大的高性能的代碼生成包,它可以在運行期擴展Java類與實現Java接口。它廣泛的被許多AOP的框架使用,例如Spring AOP和dynaop,為他們提供方法的interception。
- CGLIB包的底層是通過使用一個小而快的字節碼處理框架ASM,來轉換字節碼并生成新的類。不鼓勵直接使用ASM,因為它要求你必須對JVM內部結構包括class文件的格式和指令集都很熟悉。
2. CGLIB的實現步驟
-
需要spring-core-5.2.5.jar依賴即可。
-
引入功能包后,就可以在內存中動態構建子類
-
被代理的類不能為final, 否則報錯。
-
目標對象的方法如果為final/static, 那么就不會被攔截,即不會執行目標對象額外的業務方法。
-
編寫 ProxyFactory.java
package com.leo.demo02.service.impl;import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy;import java.lang.reflect.Method;/*** @author : Leo* @version 1.0* @date 2023/8/14 14:00* @description :*/ public class ProxyFactory2 implements MethodInterceptor {/** 目標對象 */private Object target;/** 傳入目標對象 */public ProxyFactory2(Object target){this.target = target;}/** Cglib采用底層的字節碼技術,在子類中采用方法攔截的技術,攔截父類指定方法的調用,并順勢植入代理功能的代碼 */@Overridepublic Object intercept(Object obj, Method method, Object[] arg2, MethodProxy proxy) throws Throwable {//代理對象的功能System.out.println("我是中介,我在張貼廣告......");System.out.println("我是中介,我在帶領租客看房子......");//調用目標對象的方法Object returnValue=method.invoke(target, arg2);//代理對象的功能System.out.println("我是中介,我在帶領租客辦理結束流程");return returnValue;}/** 生成代理對象 */public Object getProxyInstance(){//1.使用工具類Enhancer en=new Enhancer();//2.設置父類en.setSuperclass(target.getClass());//3.設置回調函數en.setCallback(this);//4.創建子類(代理)對象return en.create();}}
-
測試類
@org.junit.Testpublic void test04() {UserService service = (UserService)new ProxyFactory2(new UserServiceImpl()).getProxyInstance();service.sell("賣房"); }
控制臺打印:
-
3.3 JDK 動態代理和 CGLIB 動態代理對比
JDK 動態代理只能代理實現了接口的類或者直接代理接口,而 CGLIB 可以代理未實現任何接口的類。 另外, CGLIB 動態代理是通過生成一個被代理類的子類來攔截被代理類的方法調用,因此不能代理聲明為 final 類型的類和方法。
就二者的效率來說,大部分情況都是 JDK 動態代理更優秀,隨著 JDK 版本的升級,這個優勢更加明顯。
4. 靜態代理和動態代理的對比
靈活性:動態代理更加靈活,不需要必須實現接口,可以直接代理實現類,并且可以不需要針對每個目標類都創建一個代理類。另外,靜態代理中,接口一旦新增加方法,目標對象和代理對象都要進行修改,這是非常麻煩的!
JVM 層面:靜態代理在編譯時就將接口、實現類、代理類這些都變成了一個個實際的 class 文件。而動態代理是在運行時動態生成類字節碼,并加載到 JVM 中的。