Spring AOP使用動態代理技術在運行期織入增強的代碼,為了揭示Spring AOP底層的工作機理,有必要對涉及到的Java知識進行學習。Spring AOP使用了兩種代理機制:一種是基于JDK的動態代理;另一種是基于CGLib的動態代理。之所以需要兩種代理機制,很大程度上是因為JDK本身只提供接口的代理,而不支持類的代理。
帶有橫切邏輯的實例 我們通過具體化代碼實現上一節所介紹例子的性能監視橫切邏輯,并通過動態代理技術對此進行改造。在調用每一個目標類方法時啟動方法的性能監視,在目標類方法調用完成時記錄方法的花費時間。
代碼清單6-2 ForumService:包含性能監視橫切代碼
- package com.baobaotao.proxy;
- public class ForumServiceImpl implements ForumService {
- public void removeTopic(int topicId) {
-
-
- PerformanceMonitor.begin(
- "com.baobaotao.proxy.ForumServiceImpl. removeTopic");
- System.out.println("模擬刪除Topic記錄:"+topicId);
- try {
- Thread.currentThread().sleep(20);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
-
-
- PerformanceMonitor.end();
- }
-
- public void removeForum(int forumId) {
-
- PerformanceMonitor.begin(
- "com.baobaotao.proxy.ForumServiceImpl. removeForum");
- System.out.println("模擬刪除Forum記錄:"+forumId);
- try {
- Thread.currentThread().sleep(40);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
-
-
- PerformanceMonitor.end();
- }
- }
package com.baobaotao.proxy;
public class ForumServiceImpl implements ForumService {
public void removeTopic(int topicId) {
//①-1開始對該方法進行性能監視
PerformanceMonitor.begin(
"com.baobaotao.proxy.ForumServiceImpl. removeTopic");
System.out.println("模擬刪除Topic記錄:"+topicId);
try {
Thread.currentThread().sleep(20);
} catch (Exception e) {
throw new RuntimeException(e);
}
//①-2結束對該方法進行性能監視
PerformanceMonitor.end();
}
public void removeForum(int forumId) {
//②-1開始對該方法進行性能監視
PerformanceMonitor.begin(
"com.baobaotao.proxy.ForumServiceImpl. removeForum");
System.out.println("模擬刪除Forum記錄:"+forumId);
try {
Thread.currentThread().sleep(40);
} catch (Exception e) {
throw new RuntimeException(e);
}
//②-2結束對該方法進行性能監視
PerformanceMonitor.end();
}
}
代碼清單6-2中粗體表示的代碼就是具有橫切邏輯特征的代碼,每個Service類和每個業務方法體的前后都執行相同的代碼邏輯:方法調用前啟動PerformanceMonitor,方法調用后通知PerformanceMonitor結束性能監視并給記錄性能監視結果。
PerformanceMonitor是性能監視的實現類,我們給出一個非常簡單的實現版本,其代碼如代碼清單6-3所示:
代碼清單6-3 PerformanceMonitor
- package com.baobaotao.proxy;
- public class PerformanceMonitor {
-
- private static ThreadLocal<MethodPerformace> performanceRecord =
- new ThreadLocal<MethodPerformance>();
-
-
- public static void begin(String method) {
- System.out.println("begin monitor...");
- MethodPerformance mp = new MethodPerformance(method);
- performanceRecord.set(mp);
- }
- public static void end() {
- System.out.println("end monitor...");
- MethodPerformance mp = performanceRecord.get();
-
-
- mp.printPerformance();
- }
- }
package com.baobaotao.proxy;
public class PerformanceMonitor {
//①通過一個ThreadLocal保存調用線程相關的性能監視信息
private static ThreadLocal<MethodPerformace> performanceRecord =
new ThreadLocal<MethodPerformance>();
//②啟動對某一目標方法的性能監視
public static void begin(String method) {
System.out.println("begin monitor...");
MethodPerformance mp = new MethodPerformance(method);
performanceRecord.set(mp);
}
public static void end() {
System.out.println("end monitor...");
MethodPerformance mp = performanceRecord.get();
//③打印出方法性能監視的結果信息。
mp.printPerformance();
}
}
ThreadLocal是將非線程安全類改造為線程安全類的法寶,在9.2節中我們將詳細介紹這個Java基礎知識。PerformanceMonitor提供了兩個方法:通過調用begin(String method)方法開始對某個目標類方法的監視,method為目標類方法的全限定名;而end()方法結束對目標類方法的監視,并給出性能監視的信息。這兩個方法必須配套使用。
用于記錄性能監視信息的MethodPerformance類的代碼如所示:
代碼清單6-4 MethodPerformance
- package com.baobaotao.proxy;
- public class MethodPerformance {
- private long begin;
- private long end;
- private String serviceMethod;
- public MethodPerformance(String serviceMethod){
- this.serviceMethod = serviceMethod;
-
-
- this.begin = System.currentTimeMillis();
-
- }
- public void printPerformance(){
-
-
- end = System.currentTimeMillis();
- long elapse = end - begin;
-
-
- System.out.println(serviceMethod+"花費"+elapse+"毫秒。");
- }
- }
package com.baobaotao.proxy;
public class MethodPerformance {
private long begin;
private long end;
private String serviceMethod;
public MethodPerformance(String serviceMethod){
this.serviceMethod = serviceMethod;
//①記錄目標類方法開始執行點的系統時間
this.begin = System.currentTimeMillis();
}
public void printPerformance(){
//②獲取目標類方法執行完成后的系統時間,并進而計算出目標類方法執行時間
end = System.currentTimeMillis();
long elapse = end - begin;
//③報告目標類方法的執行時間
System.out.println(serviceMethod+"花費"+elapse+"毫秒。");
}
}
通過下面的代碼測試擁有性能監視能力的ForumServiceImpl業務方法:
- package com.baobaotao.proxy;
-
- public class TestForumService {
- public static void main(String[] args) {
- ForumService forumService = new ForumServiceImpl();
- forumService .removeForum(10);
- forumService .removeTopic(1012);
- }
- }
package com.baobaotao.proxy;
public class TestForumService {
public static void main(String[] args) {
ForumService forumService = new ForumServiceImpl();
forumService .removeForum(10);
forumService .removeTopic(1012);
}
}
我們得到以下輸出信息:
引用
begin monitor... ①removeForum(10)方法的性能監視報告
模擬刪除Forum記錄:10
end monitor...
com.baobaotao.proxy.ForumServiceImpl.removeForum花費47毫秒。
begin monitor... ①removeTopic(1012)方法的性能監視報告
模擬刪除Topic記錄:1012
end monitor...
com.baobaotao.proxy.ForumServiceImpl.removeTopic花費26毫秒。
正如代碼清單6 2實例所示,當某個方法需要進行性能監視,就必須調整方法代碼,在方法體前后分別添加上開啟性能監視和結束性能監視的代碼。這些非業務邏輯的性能監視代碼破壞了ForumServiceImpl業務邏輯的純粹性。我們希望通過代理的方式,將業務類方法中開啟和結束性能監視的這些橫切代碼從業務類中完全移除。并通過JDK動態代理技術或CGLib動態代理技術將橫切代碼動態織入到目標方法的相應位置。
JDK動態代理 JDK 1.3以后,Java提供了動態代理的技術,允許開發者在運行期創建接口的代理實例。在Sun剛推出動態代理時,還很難想象它有多大的實際用途,現在我們終于發現動態代理是實現AOP的絕好底層技術。
JDK的動態代理主要涉及到java.lang.reflect包中的兩個類:Proxy和InvocationHandler。其中InvocationHandler是一個接口,可以通過實現該接口定義橫切邏輯,并通過反射機制調用目標類的代碼,動態將橫切邏輯和業務邏輯編織在一起。
而Proxy利用InvocationHandler動態創建一個符合某一接口的實例,生成目標類的代理對象。這樣講一定很抽象,我們馬上著手使用Proxy和InvocationHandler這兩個魔法戒對上一節中的性能監視代碼進行革新。
首先,我們從業務類ForumServiceImpl中刪除性能監視的橫切代碼,使ForumServiceImpl只負責具體的業務邏輯,如代碼清單6-5所示:
代碼清單6-5 ForumServiceImpl:移除性能監視橫切代碼
- package com.baobaotao.proxy;
-
- public class ForumServiceImpl implements ForumService {
-
- public void removeTopic(int topicId) {
- ①
- System.out.println("模擬刪除Topic記錄:"+topicId);
- try {
- Thread.currentThread().sleep(20);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- ①
- }
- public void removeForum(int forumId) {
- ②
- System.out.println("模擬刪除Forum記錄:"+forumId);
- try {
- Thread.currentThread().sleep(40);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- ②
- }
- }
package com.baobaotao.proxy;
public class ForumServiceImpl implements ForumService {
public void removeTopic(int topicId) {
①
System.out.println("模擬刪除Topic記錄:"+topicId);
try {
Thread.currentThread().sleep(20);
} catch (Exception e) {
throw new RuntimeException(e);
}
①
}
public void removeForum(int forumId) {
②
System.out.println("模擬刪除Forum記錄:"+forumId);
try {
Thread.currentThread().sleep(40);
} catch (Exception e) {
throw new RuntimeException(e);
}
②
}
}
在代碼清單6-5中的①和②處,原來的性能監視代碼被移除了,我們只保留了真正的業務邏輯。
從業務類中移除的性能監視橫切代碼當然不能漂浮在空氣中,它還得找到一個安身之所,InvocationHandler就是橫切代碼的安家樂園,我們將性能監視的代碼安置在PerformanceHandler中,如代碼清單6-6所示:
- 代碼清單6-6 PerformanceHandler
- package com.baobaotao.proxy;
- import java.lang.reflect.InvocationHandler;
- import java.lang.reflect.Method;
-
- public class PerformanceHandler implements InvocationHandler {
- private Object target;
- public PerformanceHandler(Object target){
- this.target = target;
- }
- public Object invoke(Object proxy, Method method, Object[] args) ③
- throws Throwable {
- PerformanceMonitor.begin(target.getClass().getName()+"."+ method. getName());③-1
- Object obj = method.invoke(target, args);
- PerformanceMonitor.end();③-1
- return obj;
- }
- }
代碼清單6-6 PerformanceHandler
package com.baobaotao.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class PerformanceHandler implements InvocationHandler {//①實現InvocationHandler
private Object target;
public PerformanceHandler(Object target){ //②target為目標的業務類
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) ③
throws Throwable {
PerformanceMonitor.begin(target.getClass().getName()+"."+ method. getName());③-1
Object obj = method.invoke(target, args);// ③-2通過反射方法調用業務類的目標方法
PerformanceMonitor.end();③-1
return obj;
}
}
③處invoke()方法中粗體所示部分的代碼為性能監視的橫切代碼,我們發現,橫切代碼只出現一次,而不是原來那樣星灑各處。③-2處的method.invoke()語句通過Java反射機制間接調用目標對象的方法,這樣InvocationHandler的invoke()方法就將橫切邏輯代碼(③-1)和業務類方法的業務邏輯代碼(③-2)編織到一起了,所以我們可以將InvocationHandler看成是一個編織器。下面,我們對這段代碼做進一步的說明。
首先,我們實現InvocationHandler接口,該接口定義了一個 invoke(Object proxy, Method method, Object[] args)的方法,proxy是最終生成的代理實例,一般不會用到;method是被代理目標實例的某個具體方法,通過它可以發起目標實例方法的反射調用;args是通過被代理實例某一個方法的入參,在方法反射調用時使用。
此外,我們在構造函數里通過target傳入希望被代理的目標對象,如②處所示,在InvocationHandler接口方法invoke(Object proxy, Method method, Object[] args)里,將目標實例傳給method.invoke()方法,調用目標實例的方法,如③所示。
下面,我們通過Proxy結合PerformanceHandler創建ForumService接口的代理實例,如代碼清單6-7所示:
代碼清單6-7 TestForumService:創建代理實例
- package com.baobaotao.proxy;
- import java.lang.reflect.Proxy;
- public class TestForumService {
- public static void main(String[] args) {
-
-
- ForumService target = new ForumServiceImpl();
-
-
- PerformanceHandler handler = new PerformanceHandler(target);
-
-
- ForumService proxy = (ForumService) Proxy.newProxyInstance(
- target.getClass().getClassLoader(),
- target.getClass().getInterfaces(),
- handler);
-
-
- proxy.removeForum(10);
- proxy.removeTopic(1012);
- }
- }
package com.baobaotao.proxy;
import java.lang.reflect.Proxy;
public class TestForumService {
public static void main(String[] args) {
//①希望被代理的目標業務類
ForumService target = new ForumServiceImpl();
//②將目標業務類和橫切代碼編織到一起
PerformanceHandler handler = new PerformanceHandler(target);
//③根據編織了目標業務類邏輯和性能監視橫切邏輯的InvocationHandler實例創建代理實例
ForumService proxy = (ForumService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
handler);
//④調用代理實例
proxy.removeForum(10);
proxy.removeTopic(1012);
}
}
上面的代碼完成業務類代碼和橫切代碼的編織工作并生成了代理實例。在②處,我們讓PerformanceHandler將性能監視橫切邏輯編織到ForumService實例中,然后在③處,通過Proxy的newProxyInstance()靜態方法為編織了業務類邏輯和性能監視邏輯的handler創建一個符合ForumService接口的代理實例。該方法的第一個入參為類加載器;第二個入參為創建代理實例所需要實現的一組接口;第三個參數是整合了業務邏輯和橫切邏輯的編織器對象。
按照③處的設置方式,這個代理實例實現了目標業務類的所有接口,即Forum ServiceImpl的ForumService接口。這樣,我們就可以按照調用ForumService接口實例相同的方式調用代理實例,如④所示。運行以上的代碼,輸出以下信息:
引用
begin monitor...
模擬刪除Forum記錄:10
end monitor...
com.baobaotao.proxy.ForumServiceImpl.removeForum花費47毫秒。
begin monitor...
模擬刪除Topic記錄:1012
end monitor...
com.baobaotao.proxy.ForumServiceImpl.removeTopic花費26毫秒。
我們發現,程序的運行效果和直接在業務類中編寫性能監視邏輯的效果一致,但是在這里,原來分散的橫切邏輯代碼已經被我們抽取到PerformanceHandler中。當其他業務類(如UserService、SystemService等)的業務方法也需要使用性能監視時,我們只要按照代碼清單6-7相似的方式,分別為它們創建代理對象就可以了。下面,我們通過時序圖描述通過創建代理對象進行業務方法調用的整體邏輯,以進一步認識代理對象的本質,如圖6-3所示。
我們在上圖中使用虛線的方式對通過Proxy創建的ForumService代理實例加以凸顯,ForumService代理實例內部利用PerformaceHandler整合橫切邏輯和業務邏輯。調用者調用代理對象的removeForum()和removeTopic()方法時,上圖的內部調用時序清晰地告訴我們實際上所發生的一切。
CGLib動態代理 使用JDK創建代理有一個限制,即它只能為接口創建代理實例,這一點我們可從Proxy的接口newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)的方法簽名中就看得很清楚:第二個入參interfaces就是需要代理實例實現的接口列表。雖然面向接口編程的思想被很多大師級人物(包括Rod Johnson)推崇,但在實際開發中,許多開發者也對此深感困惑:難道對一個簡單業務表的操作也需要老老實實地創建5個類(領域對象類、Dao接口,Dao實現類,Service接口和Service實現類)嗎?難道不能直接通過實現類構建程序嗎?對于這個問題,我們很難給出一個孰好孰劣的準確判斷,但我們確實發現有很多不使用接口的項目也取得了非常好的效果(包括大家所熟悉的SpringSide開源項目)。
對于沒有通過接口定義業務方法的類,如何動態創建代理實例呢?JDK的代理技術顯然已經黔驢技窮,CGLib作為一個替代者,填補了這個空缺。
CGLib采用非常底層的字節碼技術,可以為一個類創建子類,并在子類中采用方法攔截的技術攔截所有父類方法的調用,并順勢織入橫切邏輯。下面,我們采用CGLib技術,編寫一個可以為任何類創建織入性能監視橫切邏輯代理對象的代理創建器,如代碼清單 6-8所示:
代碼清單6-8 CglibProxy
- package com.baobaotao.proxy;
- import java.lang.reflect.Method;
- import net.sf.cglib.proxy.Enhancer;
- import net.sf.cglib.proxy.MethodInterceptor;
- import net.sf.cglib.proxy.MethodProxy;
-
- public class CglibProxy implements MethodInterceptor {
- private Enhancer enhancer = new Enhancer();
- public Object getProxy(Class clazz) {
- enhancer.setSuperclass(clazz);
- enhancer.setCallback(this);
- return enhancer.create();
-
- }
-
-
- public Object intercept(Object obj, Method method, Object[] args,
- MethodProxy proxy) throws Throwable {
- PerformanceMonitor.begin(obj.getClass().getName()+"."+method. getName());
- Object result=proxy.invokeSuper(obj, args); ③-2
- PerformanceMonitor.end();
- return result;
- }
- }
package com.baobaotao.proxy;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class CglibProxy implements MethodInterceptor {
private Enhancer enhancer = new Enhancer();
public Object getProxy(Class clazz) {
enhancer.setSuperclass(clazz); //① 設置需要創建子類的類
enhancer.setCallback(this);
return enhancer.create(); //②通過字節碼技術動態創建子類實例
}
//③攔截父類所有方法的調用
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
PerformanceMonitor.begin(obj.getClass().getName()+"."+method. getName());//③-1
Object result=proxy.invokeSuper(obj, args); ③-2
PerformanceMonitor.end();//③-1通過代理類調用父類中的方法
return result;
}
}
在上面代碼中,用戶可以通過getProxy(Class clazz)為一個類創建動態代理對象,該代理對象通過擴展clazz創建代理對象。在這個代理對象中,我們織入性能監視的橫切邏輯(③-1)。intercept(Object obj, Method method, Object[] args,MethodProxy proxy)是CGLib定義的Interceptor接口的方法,它攔截所有目標類方法的調用,obj表示目標類的實例;method為目標類方法的反射對象;args為方法的動態入參;而proxy為代理類實例。
下面,我們通過CglibProxy為ForumServiceImpl類創建代理對象,并測試代理對象的方法,如代碼清單6-9所示:
代碼清單6-9 TestForumService:測試Cglib創建的代理類
- package com.baobaotao.proxy;
- import java.lang.reflect.Proxy;
- public class TestForumService {
- public static void main(String[] args) {
- CglibProxy proxy = new CglibProxy();
- ForumServiceImpl forumService = ①
- (ForumServiceImpl )proxy.getProxy(ForumServiceImpl.class);
- forumService.removeForum(10);
- forumService.removeTopic(1023);
- }
- }
package com.baobaotao.proxy;
import java.lang.reflect.Proxy;
public class TestForumService {
public static void main(String[] args) {
CglibProxy proxy = new CglibProxy();
ForumServiceImpl forumService = ①
(ForumServiceImpl )proxy.getProxy(ForumServiceImpl.class);
forumService.removeForum(10);
forumService.removeTopic(1023);
}
}
在①中,我們通過CglibProxy為ForumServiceImpl動態創建了一個織入性能監視邏輯的代理對象,并調用代理類的業務方法。運行上面的代碼,輸入以下信息:
引用
begin monitor...
模擬刪除Forum記錄:10
end monitor...
com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0.removeForum花費47毫秒。
begin monitor...
模擬刪除Topic記錄:1023
end monitor...
com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0.removeTopic花費16毫秒。
觀察以上的輸出,除了發現兩個業務方法中都織入了性能監控的邏輯外,我們還發現代理類的名字是com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0,這個特殊的類就是CGLib為ForumServiceImpl動態創建的子類。
代理知識小結 Spring AOP的底層就是通過使用JDK動態代理或CGLib動態代理技術為目標Bean織入橫切邏輯。在這里,我們對前面兩節動態創建代理對象作一個小結。
我們雖然通過PerformanceHandler或CglibProxy實現了性能監視橫切邏輯的動態織入,但這種實現方式存在三個明顯需要改進的地方:
1)目標類的所有方法都添加了性能監視橫切邏輯,而有時,這并不是我們所期望的,我們可能只希望對業務類中的某些特定方法添加橫切邏輯;
2)我們通過硬編碼的方式指定了織入橫切邏輯的織入點,即在目標類業務方法的開始和結束前織入代碼;
3)我們手工編寫代理實例的創建過程,為不同類創建代理時,需要分別編寫相應的創建代碼,無法做到通用。
以上三個問題,在AOP中占用重要的地位,因為Spring AOP的主要工作就是圍繞以上三點展開:Spring AOP通過Pointcut(切點)指定在哪些類的哪些方法上織入橫切邏輯,通過Advice(增強)描述橫切邏輯和方法的具體織入點(方法前、方法后、方法的兩端等)。此外,Spring通過Advisor(切面)將Pointcut和Advice兩者組裝起來。有了Advisor的信息,Spring就可以利用JDK或CGLib的動態代理技術采用統一的方式為目標Bean創建織入切面的代理對象了。