動態(tài)代理總結(jié),面試你要知道的都在這里,無廢話!
本文轉(zhuǎn)載自微信公眾號「月伴飛魚」,作者日常加油站。轉(zhuǎn)載本文請聯(lián)系月伴飛魚公眾號。
代理模式
代理模式是一種設(shè)計模式,提供了對目標(biāo)對象額外的訪問方式,即通過代理對象訪問目標(biāo)對象,這樣可以在不修改原目標(biāo)對象的前提下,提供額外的功能操作,擴展目標(biāo)對象的功能
一個比方:在租房的時候,有的人會通過房東直租,有的人會通過中介租房。
這兩種情況哪種比較方便呢?當(dāng)然是通過中介更加方便。
這里的中介就相當(dāng)于代理,用戶通過中介完成租房的一系列操作(看房、交押金、租房、清掃衛(wèi)生)代理模式可以有效的將具體的實現(xiàn)與調(diào)用方進(jìn)行解耦,通過面向接口進(jìn)行編碼完全將具體的實現(xiàn)隱藏在內(nèi)部。
分類:
靜態(tài)代理: 在編譯時就已經(jīng)實現(xiàn),編譯完成后代理類是一個實際的class文件
動態(tài)代理: 在運行時動態(tài)生成的,即編譯完成后沒有實際的class文件,而是在運行時動態(tài)生成類字節(jié)碼,并加載到JVM中
靜態(tài)代理
使用方式
創(chuàng)建一個接口,然后創(chuàng)建被代理的類實現(xiàn)該接口并且實現(xiàn)該接口中的抽象方法。之后再創(chuàng)建一個代理類,同時使其也實現(xiàn)這個接口。在代理類中持有一個被代理對象的引用,而后在代理類方法中調(diào)用該對象的方法。
- public interface UserDao {
- void save();
- }
- public class UserDaoImpl implements UserDao {
- @Override
- public void save() {
- System.out.println("正在保存用戶...");
- }
- }
- public class TransactionHandler implements UserDao {
- //目標(biāo)代理對象
- private UserDao target;
- //構(gòu)造代理對象時傳入目標(biāo)對象
- public TransactionHandler(UserDao target) {
- this.target = target;
- }
- @Override
- public void save() {
- //調(diào)用目標(biāo)方法前的處理
- System.out.println("開啟事務(wù)控制...");
- //調(diào)用目標(biāo)對象的方法
- target.save();
- //調(diào)用目標(biāo)方法后的處理
- System.out.println("關(guān)閉事務(wù)控制...");
- }
- }
- public class Main {
- public static void main(String[] args) {
- //新建目標(biāo)對象
- UserDaoImpl target = new UserDaoImpl();
- //創(chuàng)建代理對象, 并使用接口對其進(jìn)行引用
- UserDao userDao = new TransactionHandler(target);
- //針對接口進(jìn)行調(diào)用
- userDao.save();
- }
- }
使用JDK靜態(tài)代理很容易就完成了對一個類的代理操作。但是JDK靜態(tài)代理的缺點也暴露了出來:由于代理只能為一個類服務(wù),如果需要代理的類很多,那么就需要編寫大量的代理類,比較繁瑣
JDK動態(tài)代理
使用JDK動態(tài)代理的五大步驟:
- 通過實現(xiàn)InvocationHandler接口來自定義自己的InvocationHandler;
- 通過Proxy.getProxyClass獲得動態(tài)代理類;
- 通過反射機制獲得代理類的構(gòu)造方法,方法簽名為getConstructor(InvocationHandler.class);
- 通過構(gòu)造函數(shù)獲得代理對象并將自定義的InvocationHandler實例對象傳為參數(shù)傳入;
- 通過代理對象調(diào)用目標(biāo)方法;
- 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 {
- /** 目標(biāo)對象 */
- 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("------插入前置通知代碼-------------");
- // 執(zhí)行相應(yīng)的目標(biāo)方法
- 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、獲取動態(tài)代理類
- Class proxyClazz = Proxy.getProxyClass(IHello.class.getClassLoader(),IHello.class);
- // 3、獲得代理類的構(gòu)造函數(shù),并傳入?yún)?shù)類型InvocationHandler.class
- Constructor constructor = proxyClazz.getConstructor(InvocationHandler.class);
- // 4、通過構(gòu)造函數(shù)來創(chuàng)建動態(tài)代理對象,將自定義的InvocationHandler實例傳入
- IHello iHello1 = (IHello) constructor.newInstance(new MyInvocationHandler(new HelloImpl()));
- // 5、通過代理對象調(diào)用目標(biāo)方法
- iHello1.sayHello();
- // ==========================第二種=============================
- /**
- * Proxy類中還有個將2~4步驟封裝好的簡便方法來創(chuàng)建動態(tài)代理對象,
- *其方法簽名為: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靜態(tài)代理與JDK動態(tài)代理之間有些許相似,比如說都要創(chuàng)建代理類,以及代理類都要實現(xiàn)接口等。
不同之處: 在靜態(tài)代理中我們需要對哪個接口和哪個被代理類創(chuàng)建代理類,所以我們在編譯前就需要代理類實現(xiàn)與被代理類相同的接口,并且直接在實現(xiàn)的方法中調(diào)用被代理類相應(yīng)的方法;但是動態(tài)代理則不同,我們不知道要針對哪個接口、哪個被代理類創(chuàng)建代理類,因為它是在運行時被創(chuàng)建的。
一句話來總結(jié)一下JDK靜態(tài)代理和JDK動態(tài)代理的區(qū)別:
JDK靜態(tài)代理是通過直接編碼創(chuàng)建的,而JDK動態(tài)代理是利用反射機制在運行時創(chuàng)建代理類的。
其實在動態(tài)代理中,核心是InvocationHandler。每一個代理的實例都會有一個關(guān)聯(lián)的調(diào)用處理程序(InvocationHandler)。對待代理實例進(jìn)行調(diào)用時,將對方法的調(diào)用進(jìn)行編碼并指派到它的調(diào)用處理器(InvocationHandler)的invoke方法
對代理對象實例方法的調(diào)用都是通過InvocationHandler中的invoke方法來完成的,而invoke方法會根據(jù)傳入的代理對象、方法名稱以及參數(shù)決定調(diào)用代理的哪個方法。
CGLIB
CGLIB包的底層是通過使用一個小而快的字節(jié)碼處理框架ASM,來轉(zhuǎn)換字節(jié)碼并生成新的類
CGLIB代理實現(xiàn)如下:
首先實現(xiàn)一個MethodInterceptor,方法調(diào)用會被轉(zhuǎn)發(fā)到該類的intercept()方法。
然后在需要使用的時候,通過CGLIB動態(tài)代理獲取代理對象。
使用案例
- public class HelloService {
- public HelloService() {
- System.out.println("HelloService構(gòu)造");
- }
- /**
- * 該方法不能被子類覆蓋,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:方法入?yún)?nbsp;
- * 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動態(tài)代理獲取代理對象的過程
- Enhancer enhancer = new Enhancer();
- // 設(shè)置enhancer對象的父類
- enhancer.setSuperclass(HelloService.class);
- // 設(shè)置enhancer的回調(diào)對象
- enhancer.setCallback(new MyMethodInterceptor());
- // 創(chuàng)建代理對象
- HelloService proxy= (HelloService)enhancer.create();
- // 通過代理對象調(diào)用目標(biāo)方法
- proxy.sayHello();
- }
- }
JDK代理要求被代理的類必須實現(xiàn)接口,有很強的局限性。
而CGLIB動態(tài)代理則沒有此類強制性要求。簡單的說,CGLIB會讓生成的代理類繼承被代理類,并在代理類中對代理方法進(jìn)行強化處理(前置處理、后置處理等)。
總結(jié)一下CGLIB在進(jìn)行代理的時候都進(jìn)行了哪些工作
- 生成的代理類繼承被代理類。在這里我們需要注意一點:如果委托類被final修飾,那么它不可被繼承,即不可被代理;同樣,如果委托類中存在final修飾的方法,那么該方法也不可被代理
- 代理類會為委托方法生成兩個方法,一個是與委托方法簽名相同的方法,它在方法中會通過super調(diào)用委托方法;另一個是代理類獨有的方法
- 當(dāng)執(zhí)行代理對象的方法時,會首先判斷一下是否存在實現(xiàn)了MethodInterceptor接口的CGLIB$CALLBACK_0;,如果存在,則將調(diào)用MethodInterceptor中的intercept方法
在intercept方法中,我們除了會調(diào)用委托方法,還會進(jìn)行一些增強操作。在Spring AOP中,典型的應(yīng)用場景就是在某些敏感方法執(zhí)行前后進(jìn)行操作日志記錄
在CGLIB中,方法的調(diào)用并不是通過反射來完成的,而是直接對方法進(jìn)行調(diào)用:通過FastClass機制對Class對象進(jìn)行特別的處理,比如將會用數(shù)組保存method的引用,每次調(diào)用方法的時候都是通過一個index下標(biāo)來保持對方法的引用
Fastclass機制
CGLIB采用了FastClass的機制來實現(xiàn)對被攔截方法的調(diào)用。
FastClass機制就是對一個類的方法建立索引,通過索引來直接調(diào)用相應(yīng)的方法
- public class test10 {
- //這里,tt可以看作目標(biāo)對象,fc可以看作是代理對象;首先根據(jù)代理對象的getIndex方法獲取目標(biāo)方法的索引,
- //然后再調(diào)用代理對象的invoke方法就可以直接調(diào)用目標(biāo)類的方法,避免了反射
- 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的每個方法建立索引,并根據(jù)入?yún)?方法名+方法的描述符)來返回相應(yīng)的索引。
Invoke根據(jù)指定的索引,以ol為入?yún)⒄{(diào)用對象O的方法。這樣就避免了反射調(diào)用,提高了效率
三種代理方式之間對比
代理方式
實現(xiàn)
優(yōu)點
缺點
特點
JDK靜態(tài)代理
代理類與委托類實現(xiàn)同一接口,并且在代理類中需要硬編碼接口
實現(xiàn)簡單,容易理解
代理類需要硬編碼接口,在實際應(yīng)用中可能會導(dǎo)致重復(fù)編碼,浪費存儲空間并且效率很低
好像沒啥特點
JDK動態(tài)代理
代理類與委托類實現(xiàn)同一接口,主要是通過代理類實現(xiàn)InvocationHandler并重寫
invoke
方法來進(jìn)行動態(tài)代理的,在invoke方法中將對方法進(jìn)行增強處理不需要硬編碼接口,代碼復(fù)用率高
只能夠代理實現(xiàn)了接口的委托類
底層使用反射機制進(jìn)行方法的調(diào)用
CGLIB動態(tài)代理
代理類將委托類作為自己的父類并為其中的非final委托方法創(chuàng)建兩個方法,一個是與委托方法簽名相同的方法,它在方法中會通過
super
調(diào)用委托方法;另一個是代理類獨有的方法。在代理方法中,它會判斷是否存在實現(xiàn)了MethodInterceptor
接口的對象,若存在則將調(diào)用intercept方法對委托方法進(jìn)行代理可以在運行時對類或者是接口進(jìn)行增強操作,且委托類無需實現(xiàn)接口
不能對
final
類以及final方法進(jìn)行代理底層將方法全部存入一個數(shù)組中,通過數(shù)組索引直接進(jìn)行方法調(diào)用
問題
CGlib比JDK快?
- 使用CGLiB實現(xiàn)動態(tài)代理,CGLib底層采用ASM字節(jié)碼生成框架,使用字節(jié)碼技術(shù)生成代理類, 在jdk6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能對聲明為final的方法進(jìn)行代理, 因為CGLib原理是動態(tài)生成被代理類的子類。
- 在jdk6、jdk7、jdk8逐步對JDK動態(tài)代理優(yōu)化之后,在調(diào)用次數(shù)較少的情況下,JDK代理效率高于CGLIB代理效率。只有當(dāng)進(jìn)行大量調(diào)用的時候,jdk6和jdk7比CGLIB代理效率低一點,但是到j(luò)dk8的時候,jdk代理效率高于CGLIB代理,總之,每一次jdk版本升級,jdk代理效率都得到提升,而CGLIB代理消息確有點跟不上步伐。
Spring如何選擇用JDK還是CGLIB?
- 當(dāng)Bean實現(xiàn)接口時,Spring就會用JDK的動態(tài)代理。
- 當(dāng)Bean沒有實現(xiàn)接口時,Spring使用CGlib實現(xiàn)。
- 可以強制使用CGlib