動態代理總結,面試你要知道的都在這裏,無廢話!
前言
面試題:講講 jdk 動態代理,cglib 區別,實現原理,優缺點,怎麼實現方法的調用的
這篇文章總結你需要回答的知識點,全程少廢話,懟乾貨,文章較長,可以點贊在看,喜歡這種文章的話,我之後也會一直分享的,硬核文章也會定期分享!
同時之前的個人網站:https://upheart.cn/,最近兩天想了想,決定繼續維護着,公衆號文章會定期(一般 2 天左右)同步更新到上去
至於之所以決定繼續維護,主要是爲了大家工作的時候也方便學習,畢竟大家工作的時候總不能玩手機看公衆號文章吧,哈哈!
代理模式
代理模式是一種設計模式,提供了對目標對象額外的訪問方式,即通過代理對象訪問目標對象,這樣可以在不修改原目標對象的前提下,提供額外的功能操作,擴展目標對象的功能
一個比方:在租房的時候,有的人會通過房東直租,有的人會通過中介租房。
這兩種情況哪種比較方便呢?當然是通過中介更加方便。
這裏的中介就相當於代理,用戶通過中介完成租房的一系列操作(看房、交押金、租房、清掃衛生)代理模式可以有效的將具體的實現與調用方進行解耦,通過面向接口進行編碼完全將具體的實現隱藏在內部。
分類:
靜態代理: 在編譯時就已經實現,編譯完成後代理類是一個實際的 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 動態代理
使用 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
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 下標來保持對方法的引用
Fastclass 機制
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 的方法。這樣就避免了反射調用,提高了效率
三種代理方式之間對比
問題
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
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Piyh4a0u_IWwwTAze7IMxQ