最近在學習MyBatis源碼,了解到MyBatis里之所以只需要開發者編寫Mapper接口即可執行SQL,就是因為JDK的動態代理在背后默默為我們做了很多事情。但是我自己對動態代理還只是一知半解,於是手機整理資料學習,整理了這篇筆記。
說到動態代理,首先要講的就是設計模式中的代理模式,而對於代理,根據創建代理類的時間點,又可以分為靜態代理和動態代理。
1. 代理模式
代理模式(Proxy),為其他對象提供一種代理以控制對這個對象的訪問。他的特征是代理類與委托類實現相同的接口,代理類主要負責為委托類預處理消息、過濾消息、把消息轉發給委托類以及事后處理消息等。代理類與委托類質檢通常會存在關聯關系,一個代理類的對象與一個委托類的對象關聯,代理類的對象本身並不真正實現服務,而是通過調用委托類的對象的相關方法,來提供特定的服務。簡單來說就是,我們訪問實際對象時,是通過代理對象來訪問的,代理模式就是在訪問實際對象時引入一定程度的間接性,因為這種間接性,使我們可以附加多種用途。
Tips:
- 委托類:指的是代理模式中的被代理對象
- 代理類:指的是生成的代表委托類的一個角色
Java代理模式實現方式,主要有以下五種方法
- 靜態代理,由開發者編輯代理類代碼,實現代理模式。在編譯器就生成了代理類。
- 基於JDK實現動態代理,通過JDK提供的工具方法Proxy.newProxyInstance()動態構建全新的代理類字節碼文件並實例化對象返回,這個代理類繼承Proxy類,並持有InvocationHandler接口引用。JDK動態代理是由Java內部的反射機制來實例化代理對象,並代理的調用委托類方法。
- 基於CGLib動態代理模式,原理是繼承被代理類生成字代理類,不用實現接口,只需要被代理類是非final類即可。CGLib動態代理底層是借助asm字節碼技術。
- 基於AspectJ實現動態代理。修改目標類的字節,織入代理的字節,在程序編譯的時候插入動態代理的字節碼,不會生成全新的Class文件。
- 基於instrumentation實現動態代理。修改目標類的字節碼、類加載的時候動態攔截去修改,基於javaagent實現
-javaagent:spring-instrument-4.3.8.RELEASE.jar
,類加載的時候插入動態代理的字節碼,不會生成全新的Class文件。
2. 靜態代理
靜態代理是代理類在編譯器就創建好了,不是編譯器生成的代理類,而是我們手動創建的類。在編譯時就已經將接口、本代理類和代理類確定下來。軟件設計模式中所指的代理一般就是說的靜態代理。
Subject類,定義了RealSubject和Proxy的共用接口,這樣就在任何使用RealSubject的地方都可以使用Proxy。
public interface Subject {
/**
* doSomething()
*/
void doSomething();
}
RealSubject類,定義Proxy所代表的真實實體。
public class RealSubject implements Subject {
@Override
public void doSomething() {
// 委托類執行操作
System.out.println("RealSubject.doSomething()");
}
}
ProxySubject類,保存一個引用使得代理可以訪問實體,並提供一個與Subject的接口相同的接口,這樣代理就可以用來替代實體。
public class ProxySubject implements Subject {
private RealSubject realSubject;
/**
* 向代理類中注入委托類對象
*
* @param realSubject 委托類對象
*/
public ProxySubject(RealSubject realSubject){
this.realSubject = realSubject;
}
/**
* 代理類執行操作
*/
@Override
public void doSomething() {
System.out.println("代理類調用委托類方法之前");
realSubject.doSomething();
System.out.println("代理類調用委托類方法之后");
}
}
測試第一種方式,不使用代理類,直接使用簡單委托類執行。
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
realSubject.doSomething();
}
輸出:
RealSubject.doSomething()
測試第二種方式,使用代理類,執行增強邏輯。
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
ProxySubject proxySubject = new ProxySubject(realSubject);
proxySubject.doSomething();
}
輸出:
代理類調用委托類方法之前
RealSubject.doSomething()
代理類調用委托類方法之后
我們在創建代理對象時,通過構造器塞入一個目標對象,然后在代理對象的方法內部調用目標對象同名方法,並在調用前后做增強邏輯。也就是說,代理對象 = 增強代碼 + 目標對象。有了代理對象后,就不用原對象了。
靜態代理的缺陷
開發者需要手動為目標類編寫對應的代理類,而且要對類中的每個方法都編寫增強邏輯的代碼,如果當前系統中已經存在成百上千個類,工作量太大了,且重復代碼過多。所以,有沒有什么方法能讓我們少寫或者不寫代理類,卻能完成代理功能?
3. 動態代理
靜態代理是代理類在代碼運行前已經創建好,並生成class文件;動態代理類是代理類在程序運行時創建的代理模式。動態代理類的代理類並不是在Java代碼中定義的,而是在運行時根據我們在Java代碼中的“指示”動態生成的。相比於靜態代理,動態代理的優勢在於可以很方便的對代理類的函數進行統一的處理,而不用修改每個代理類中的方法。
3.1 JDK動態代理
基於接口實現。Java的java.lang.reflect
包下提供了Proxy類和一個 InvocationHandler 接口,這個類Proxy定義了生成JDK動態代理類的方法getProxyClass(ClassLoader loader,Class<?>... interfaces)
生成動態代理類,返回class實例代表一個class文件。可以保存該 class 文件查看jdk生成的代理類文件長什么樣。該生成的動態代理類繼承Proxy類,(重要特性) ,並實現公共接口。InvocationHandler這個接口,是被動態代理類回調的接口,我們所有需要增加的針對委托類的統一增強邏輯都增加到invoke()方法里面,在調用委托類接口方法之前或之后。
例子。任然使用上面靜態代理里的類,只不過這次我們不會再用到代理類ProxySubject,而是讓JDK去幫我們生成代理類。方法如下:
public static void main(String[] args) {
// 實例化目標對象
Subject subject = new RealSubject();
// 獲取代理對象
Subject proxyInstance = (Subject) Proxy.newProxyInstance(subject.getClass().getClassLoader(), subject.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 增強邏輯
System.out.println("動態代理調用委托類方法之前");
Object invoke = method.invoke(subject, args);
System.out.println("動態代理調用委托類方法之后");
return invoke;
}
});
// 執行目標方法
proxyInstance.doSomething();
}
輸出:
動態代理調用委托類方法之前
RealSubject.doSomething()
動態代理調用委托類方法之后
Jdk為我們的生成了一個叫$Proxy0(這個名字后面的0是編號,有多個代理類會一次遞增)的代理類,這個類文件時默認不會保存在文件,放在內存中的,我們在創建代理對象時,就是通過反射獲得這個類的構造方法,然后創建代理對象實例。通過對這個生成的代理類源碼的查看,我們很容易能看出,動態代理實現的具體過程。
我們可以對 InvocationHandler看做一個中介類,中介類持有一個被代理對象,被Proxy類回調。在invoke方法中調用了被代理對象的相應方法。通過聚合方式持有被代理對象的引用,把客戶端對invoke的調用最終都轉為對被代理對象的調用。
客戶端代碼通過代理類引用調用接口方法時,通過代理類關聯的中介類對象引用來調用中介類對象的invoke方法,從而達到代理執行被代理對象的方法。也就是說,動態代理Proxy類提供了模板實現,對外提供擴展點,外部通過實現InvocationHandler接口將被代理類納入JDK代理類Proxy。
JDK動態代理特點總結
- 生成的代理類:$Proxy0 extends Proxy implements Subject,我們看到代理類繼承了Proxy類,Java的繼承機制決定了JDK動態代理類們無法實現對 類 的動態代理。所以也就決定了JDK動態代理只能對接口進行代理。
- 每個生成的動態代理實例都會關聯一個調用處理器對象,可以通過 Proxy 提供的靜態方法 getInvocationHandler去獲得代理類實例的調用處理器對象。在代理類實例上調用其代理的接口中所聲明的方法時,這些方法最終都會由調用處理器的 invoke 方法執行。
- 代理類的根類 java.lang.Object 中有三個方法也同樣會被分派到調用處理器的 invoke 方法執行,它們是 hashCode,equals 和 toString,可能的原因有:一是因為這些方法為 public 且非 final 類型,能夠被代理類覆蓋;二是因為這些方法往往呈現出一個類的某種特征屬性,具有一定的區分度,所以為了保證代理類與委托類對外的一致性,這三個方法也應該被調用處理器分派到委托類執行。
JDK動態代理的不足
JDK動態代理的代理類字節碼在創建時,需要實現業務實現類所實現的接口作為參數。如果業務實現類是沒有實現接口而是直接定義業務方法的話,就無法使用JDK動態代理了。(JDK動態代理重要特點是代理接口)並且,如果業務實現類中新增了接口中沒有的方法,這些方法是無法被代理的(因為無法被調用)。動態代理只能對接口產生代理,不能對類產生代理。
3.2 CGLib動態代理
基於繼承。CGlib是針對類來實現代理的,他的原理是對代理的目標類生成一個子類,並覆蓋其中方法實現增強,因為底層是基於創建被代理類的一個子類,所以它避免了JDK動態代理類的缺陷。但因為采用的是繼承,所以不能對final修飾的類進行代理。final修飾的類不可繼承。
例子。
public class CGlibSubject implements MethodInterceptor {
private Object target;
public Object getInstance(Object target) {
this.target = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
// 設置回調方法
enhancer.setCallback(this);
// 創建代理對象
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("CGlib動態代理調用委托類方法之前");
Object result = methodProxy.invokeSuper(o, objects);
System.out.println("CGlib動態代理調用委托類方法之后");
return result;
}
}
測試:
public static void main (String[] args) {
CGlibSubject cglibSubject = new CGlibSubject();
RealSubject instance = (RealSubject) cglibSubject.getInstance(new RealSubject());
instance.doSomething();
}
輸出:
CGlib動態代理調用委托類方法之前
RealSubject.doSomething()
CGlib動態代理調用委托類方法之后
CGlib動態代理特點總結
- CGlib可以傳入接口也可以傳入普通的類,接口使用實現的方式,普通類使用會使用繼承的方式生成代理類;
- 由於是繼承方式,如果是static方法,private方法,final方法等描述的方法是不能被代理的;
- 做了方法訪問優化,使用建立方法索引的方式避免了傳統JDK動態代理需要通過Method方法反射調用;
- 提供callback 和filter設計,可以靈活地給不同的方法綁定不同的callback。編碼更方便靈活;
- CGLIB會默認代理Object中equals,toString,hashCode,clone等方法。比JDK代理多了clone。
4. 總結
-
靜態代理是通過在代碼中顯式編碼定義一個業務實現類的代理類,在代理類中對同名的業務方法進行包裝,用戶通過代理類調用委托類的業務方法;
-
JDK動態代理是通過接口中的方法名,在動態生成的代理類中調用業務實現類的同名方法;
-
CGlib動態代理是通過繼承業務類,生成的動態代理類是業務類的子類,通過重寫業務方法進行代理;
-
靜態代理在編譯時產生class字節碼文件,可以直接使用,效率高。動態代理必須實現InvocationHandler接口,通過invoke調用被委托類接口方法是通過反射方式,比較消耗系統性能,但可以減少代理類的數量,使用更靈活。cglib代理無需實現接口,通過生成類字節碼實現代理,比反射稍快,不存在性能問題,但cglib會繼承目標對象,需要重寫方法,所以目標對象不能為final類。
參考文章:
5. 代碼倉庫
https://github.com/goSilver/daydayup/tree/master/java/src/designpattern/proxy