Spring AOP 實(shí)現機制標簽:
javaspringaop2014-05-23 11:43 12359人閱讀
評論(1)
舉報 分類(lèi):
JAVA技術(shù)(34)
技術(shù)類(lèi)(11)
(1)AOP的各種實(shí)現
在編譯器修改源代碼、在運行期字節碼加載前修改字節碼或字節碼加載后動(dòng)態(tài)創(chuàng )建代理類(lèi)的字節碼。以下是各種實(shí)現機制的比較:
類(lèi)別分為靜態(tài)AOP(包括靜態(tài)織入)和動(dòng)態(tài)AOP(包括動(dòng)態(tài)代理、動(dòng)態(tài)字節碼生成、自定義類(lèi)加載器、字節碼轉換)。
靜態(tài)織入:
a、原理:在編譯期,切面直接以字節碼形式編譯到目標字節碼文件中 ;
b、優(yōu)點(diǎn):對系統性能無(wú)影響;
c、缺點(diǎn):不夠靈活;
動(dòng)態(tài)代理 :
a、原理:在運行期,目標類(lèi)加載后,為接口動(dòng)態(tài)生成代理類(lèi)。將切面織入到代理類(lèi)中;
b、優(yōu)點(diǎn):更靈活;
c、缺點(diǎn):切入的關(guān)注點(diǎn)要實(shí)現接口;
動(dòng)態(tài)字節碼生成:
a、原理:在運行期,目標類(lèi)加載后,動(dòng)態(tài)構建字節碼文件生成目標類(lèi)的子類(lèi),將切面邏輯加入到子類(lèi)中;
b、優(yōu)點(diǎn):沒(méi)有接口也可以織入;
c、缺點(diǎn):擴展類(lèi)的實(shí)例方法為final時(shí),無(wú)法進(jìn)行織入;
自定義類(lèi)加載器
a、原理:在運行期,目標加載前,將切面邏輯加到目標字節碼里;
b、優(yōu)點(diǎn):可以對絕大部分類(lèi)進(jìn)行織入;
c、缺點(diǎn):代碼中若使用了其它類(lèi)加載器,則這些類(lèi)將不會(huì )被織入;
字節碼轉換
a、原理:在運行期,所有類(lèi)加載器加載字節碼前進(jìn)行攔截;
b、優(yōu)點(diǎn):可以對所有類(lèi)進(jìn)行織入;
c、缺點(diǎn):
(2)
Joinpoint:攔截點(diǎn),如某個(gè)業(yè)務(wù)方法;
Pointcut:Jointpoint的表達式,表示攔截哪些方法。一個(gè)Pointcut對應多個(gè)Joinpoint;
Advice:要切入的邏輯。
Before Advice:在方法前切入;
After Advice:在方法后切入,拋出異常時(shí)也會(huì )切入;
After Returning Advice:在方法返回后切入,拋出異常不會(huì )切入;
After Throwing Advice:在方法拋出異常時(shí)切入;
Around Advice:在方法執行前后切入,可以中斷或忽略原有流程的執行;
目標 切面 織入器 代理類(lèi)
Jointpoint Advice
Pointcut
Pointcut
織入器通過(guò)在切面中定義pointcut來(lái)搜索目標(被代理類(lèi))的Jointpoint(切入點(diǎn)),然后把要切入的邏輯(advice)織入到目標對象里,生成代理類(lèi)。
(3)動(dòng)態(tài)代理的實(shí)現
Java在JDK1.3后引入的動(dòng)態(tài)代理機制,使我們可以在運行期動(dòng)態(tài)的創(chuàng )建代理類(lèi)。
使用動(dòng)態(tài)代理實(shí)現AOP需要四個(gè)角色:被代理的類(lèi)、被代理類(lèi)的接口、織入器(Proxy.newProxyInstance())、InvocationHandler??椚肫魇褂媒涌诜瓷錂C制生成一個(gè)代理類(lèi),然后在這個(gè)代理類(lèi)中織入代碼(切入邏輯)。InvocationHandler是切面,包含了Advice和Pointcut。
動(dòng)態(tài)代理在運行期通過(guò)接口動(dòng)態(tài)生成代理類(lèi)。
使用反射大量生成類(lèi)文件可能引起Full GC造成性能影響,因為字節碼文件加載后會(huì ) 存放在JVM運行時(shí)區的方法區中(或持久代)。當方法區滿(mǎn)的時(shí)候,會(huì )引起Full GC。因此當大量使用動(dòng)態(tài)代理時(shí),可以將持久代設置大一些,減少Full GC次數。
動(dòng)態(tài)代理的核心其實(shí)就是代理對象的生成,即Proxy.newProxyInstance()。其中g(shù)etProxyClass()方法用于獲取代理類(lèi),主要做了三件事:在當前類(lèi)加載器的緩存里搜索是否有代理類(lèi),沒(méi)有則生成代理類(lèi)并緩存在本地JVM里。
可以使用JD-GUI反編譯軟件打開(kāi)jre\lib\rt.jar。
動(dòng)態(tài)代理生成的代理類(lèi),類(lèi)似于:
public class ProxyBusiness implements IBusiness {
private InvocationHandler h;
public ProxyBusiness(InvocationHandler h) {
this.h = h;
}
public void doSomeThing() {
tyr{
Method m = (h.target).getClass().getMethod("doSomeThing", null);
h.invoke(this, m , null);
} catch(Throwable e) {
}
}
//測試
public static void main(String[] args) {
LogInvocationHandler handler = new LogInvocationHandler(new Business());
new ProxyBusiness(handler).doSomeThing();
}
}
代理的目的是調用目標方法時(shí)轉而執行InvocationHandler類(lèi)的invoke方法!
(4)動(dòng)態(tài)字節碼生成
使用動(dòng)態(tài)字節碼生成技術(shù)實(shí)現AOP原理:在運行期間目標字節碼加載后,生成目標類(lèi)的子類(lèi),將切面邏輯加入到子類(lèi)中,所以使用Cglib實(shí)現AOP不需要基于接口。
使用Cglib實(shí)現動(dòng)態(tài)字節碼:
Cglib是高性能的code生成類(lèi)庫,可以在運行期間擴展java類(lèi)和實(shí)現java接口。它封裝了Asm,使用Cglib前要引入Asm的jar。
如:
public static void byteCodeGe() {
//創(chuàng )建一個(gè)織入器
Enhancer enhancer = new Enhancer();
//設置父類(lèi)
enhancer.setSuperclass(Business.class);
//設置需要織入的邏輯
enhancer.setCallback(new LogIntercept());
//使用織入器創(chuàng )建子類(lèi)
IBusiness newBusiness = (IBusiness)enhancer.create();
newBusiness.doSomeThing();
}
public class LogIntercept implements MethodInterceptor {
public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
//執行原有邏輯,注意這里是invokeSuper
Object rev = proxy.invokeSuper(target, args);
//執行織入的日志
if(method.getName().equals("doSomeThing")) {
System.out.println("記錄日志");
}
return rev;
}
}
(5)自定義類(lèi)加載器
實(shí)現一個(gè)自定義類(lèi)加載器,在類(lèi)加載到JVM之前直接修改類(lèi)的方法,并將切入邏輯織入到這個(gè)方法里,然后將修改后的字節碼文件交給JVM運行。
Javassist是一個(gè)編輯字節碼的框架,可以很簡(jiǎn)單操作字節碼。它可以在運行期定義或修改Class。使用Javassist實(shí)現AOP的原理是在字節碼加載前直接修改需要切入的方法,比使用Cglib實(shí)現AOP更加高效。
原理:
系統類(lèi)加載器——>啟動(dòng)——>自定義類(lèi)加載器(類(lèi)加載監聽(tīng)器)——>載入——>類(lèi)文件
使用系統類(lèi)加載器啟動(dòng)自定義的類(lèi)加載器,在這個(gè)類(lèi)加載器里加一個(gè)類(lèi)加載監聽(tīng)器,監聽(tīng)器發(fā)現目標類(lèi)被加載時(shí)就織入切入邏輯。
啟動(dòng)自定義的類(lèi)加載器:
//獲取存放CtClass的容器ClassPool
ClassPool cp = ClassPool.getDefault();
//創(chuàng )建一個(gè)類(lèi)加載器
Loader cl = new Loader();
//增加一個(gè)轉換器
cl.addTranslator(cp, new MyTranslator());
//啟動(dòng)MyTranslator的main函數
cl.run("javassist.JavassistAopDemo$MyTranslator", args);
類(lèi)加載監聽(tīng)器:
public static class MyTranslator implements Translator {
public void start(ClassPool pool) throws Exception {
}
/**類(lèi)加載到JVM前進(jìn)行代碼織入*/
public void onLoad(ClassPool pool, String classname) {
if(!"model$Business".equals(classname)) {
return ;
}
//通過(guò)獲取類(lèi)文件
try{
CtClass cc = pool.get(classname);
//獲得指定方法名的方法
CtMethod m = cc.getDeclaredMethod("doSomeThing");
//在方法執行前插入代碼
m.insertBefore("{System.out.println(\"記錄日志\");}");
} catch(Exception) {
}
}
public static void main(String[] args) {
Business b = new Business();
b.doSomeThing();
}
}
CtClass是一個(gè)class文件的抽象描述??梢允褂胕nsertAfter()在方法的末尾插入代碼,使用insertAt()在指定行插入代碼。
使用自定義的類(lèi)加載器實(shí)現AOP在性能上要優(yōu)于動(dòng)態(tài)代理和Cglib,因為它不會(huì )產(chǎn)生新類(lèi)。但存在一個(gè)問(wèn)題,就是若其他的類(lèi)加載器來(lái)加載類(lèi)的話(huà),這些類(lèi)將不會(huì )被攔截。
(6)字節碼轉換
自定義的類(lèi)加載器實(shí)現AOP只能攔截自己加載的字節碼,有沒(méi)有能夠監控所有類(lèi)加載器加載字節碼?——>有,使用Instrumentation,它是Java 5提供的新特性。使用Instrumentation可以構建一個(gè)字節碼轉換器,在字節碼加載前進(jìn)行轉換。使用Instrumentation和javassist實(shí)現AOP。
一、構建字節碼轉換器
首先創(chuàng )建字節碼轉換器,該轉換器負責攔截Business類(lèi),并在Business類(lèi)的doSomeThing方法前使用javassist加入記錄日志的代碼。
public class MyClassFileTransformer implements ClassFileTransformer {
/**字節碼加載到JVM前會(huì )進(jìn)入這個(gè)方法*/
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws Exception{
//若加載Business類(lèi)才攔截
if(!"model/Business".equals(className)) {
return null;
}
//javassist的包名是用點(diǎn)分割的,要轉換下
if(className.indexOf("/") != -1) {
className = className.replaceAll("/", ".");
}
try{
//通過(guò)包名獲取類(lèi)文件
CtClass cc = ClassPool.getDefault().get(className);
//獲得指定方法名的方法
CtMethod m = cc.getDeclaredMethod("doSomeThing");
//在方法執行前插入代碼
m.insertBefore("{System.out.println(\"記錄日志\");}");
return cc.toBytecode();
}catch (Exception e) {
}
return null;
}
}
二、注冊轉換器
使用premain函數注冊字節碼轉換器,該方法在main函數之前執行。
public class MyClassFileTransformer implements ClassFileTransformer {
public static void premain(String options, Instrumentation ins) {
//注冊自己的字節碼轉換器
ins.addTransformer(new MyClassFileTransformer());
}
}
三、配置和執行
需要告訴JVM在啟動(dòng)main函數之前,需要先執行premain函數。首先需要將premain函數所在的類(lèi)打成jar包。并修改該jar包里的META-INF\MANIFEST.MF文件。
Manifest-Version:1.0
Premain-Class:bci. MyClassFileTransformer
然后在JVM的啟動(dòng)參數里加上:
-javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar
四、輸出
執行main函數,會(huì )發(fā)現切入的代碼無(wú)侵入性的織入進(jìn)去了。
public static void main(String[] args) {
new Business().doSomeThing();
}
2、PS
(1)AOP能做的事情:
性能監控:在方法調用前后記錄調用時(shí)間,方法執行太長(cháng)或超時(shí)報警。
緩存代理:緩存某方法的返回值,下次執行該方法時(shí),直接從緩存里獲取。
軟件破解:使用AOP修改軟件的驗證類(lèi)的判斷邏輯。
工作流系統:工作流系統需要將業(yè)務(wù)代碼和流程引擎代碼混合在一起執行,可以使用AOP將其分離,并動(dòng)態(tài)掛接業(yè)務(wù)。
權限驗證:方法執行前驗證是否有權限執行當前方法。
(2)方法調用成功后——>統計調用次數——>存入緩存服務(wù)器——>每日存入
數據庫因為每天的方法調用次數近百萬(wàn),為了降低數據庫壓力不能實(shí)時(shí)入庫。
一、如何使用:
只要配置了注解的方法將會(huì )被統計調用次數。
@MethodInvokeTimesMonitor(value="aaa", returnValue=false)
public void aa() {
}
二、如何配置:
使用AspectJ的方式配置AOP。需要啟動(dòng)對AspectJ的支持。
<aop:aspectj-autoproxy proxy-target-class="true"/>
true表示讓
spring使用Cglib實(shí)現AOP,配置為false表示使用動(dòng)態(tài)代理實(shí)現AOP,默認使用動(dòng)態(tài)代理。
三、定義注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodInvokeTimesMonitor{
String value();
boolean returnValue() default true;
}
四、定義切面,在切面中定義攔截的方法和在方法返回后記錄調用次數的Advice。在這里定義攔截所有配置了注解的方法。
@Aspect
public class MethodAspect {
/**切入點(diǎn),所有配置MethodInvokeTimesMonitor注解的方法*/
@Pointcut("@annotation(org.cendy.MethodInvokeTimesMonitor)")
public void allMethodInvokeTimesMonitor() {
}
/**統計方法的調用次數*/
@AfterReturning(value="MethodAspect.allMethodInvokeTimesMonitor() && @annotation(methodInvokeTimesMonitor)", returning="retVal")
public void statInvokeTimes(MethodInvokeTimesMonitor methodInvokeTimesMonitor, Object retVal)
String name = methodInvokeTimesMonitor.value();
boolean returnValue = methodInvokeTimesMonitor.returnValue();
.....
}
其中@Pointcut用于定義切入點(diǎn)表達式。@AfterReturning表示在方法執行后進(jìn)行切入,里面的MethodAspect.allMethodInvokeTimesMonitor()表示使用這個(gè)方法的切入點(diǎn)表達式,@annotation(methodInvokeTimesMonitor)表示將當參數傳遞給statInvokeTimes()方法,returning="retVal"表示將被切入方法的返回值賦值給retVal,并傳遞給statInvokeTimes()方法。
(3)spring默認采用動(dòng)態(tài)代理機制實(shí)現AOP,當動(dòng)態(tài)代理不可用時(shí)(代理類(lèi)無(wú)接口)會(huì )使用Cglib機制。
使用spring的AOP缺點(diǎn):
a、只能對方法進(jìn)行切入,不能對接口、屬性、靜態(tài)代碼塊進(jìn)行切入(切入接口的某個(gè)方法,則該接口下所有實(shí)現類(lèi)的該方法將被切入)
b、同類(lèi)中的互相調用方法將不會(huì )使用代理類(lèi)。因為要使用代理類(lèi)必須從spring容器中獲取bean。
獲取代理類(lèi),如:
public IMsgFilterService getThis() {
return (IMsgFilterService)AopContext.currentProxy();
}
1 AOP各種的實(shí)現
AOP就是面向切面編程,我們可以從幾個(gè)層面來(lái)實(shí)現AOP。
在編譯器修改源代碼,在運行期字節碼加載前修改字節碼或字節碼加載后動(dòng)態(tài)創(chuàng )建代理類(lèi)的字節碼,以下是各種實(shí)現機制的比較。
類(lèi)別
機制
原理
優(yōu)點(diǎn)
缺點(diǎn)
靜態(tài)AOP
靜態(tài)織入
在編譯期,切面直接以字節碼的形式編譯到目標字節碼文件中。
對系統無(wú)性能影響。
靈活性不夠。
動(dòng)態(tài)AOP
動(dòng)態(tài)代理
在運行期,目標類(lèi)加載后,為接口動(dòng)態(tài)生成代理類(lèi),將切面植入到代理類(lèi)中。
相對于靜態(tài)AOP更加靈活。
切入的關(guān)注點(diǎn)需要實(shí)現接口。對系統有一點(diǎn)性能影響。
動(dòng)態(tài)字節碼生成
在運行期,目標類(lèi)加載后,動(dòng)態(tài)構建字節碼文件生成目標類(lèi)的子類(lèi),將切面邏輯加入到子類(lèi)中。
沒(méi)有接口也可以織入。
擴展類(lèi)的實(shí)例方法為final時(shí),則無(wú)法進(jìn)行織入。
自定義類(lèi)加載器
在運行期,目標加載前,將切面邏輯加到目標字節碼里。
可以對絕大部分類(lèi)進(jìn)行織入。
代碼中如果使用了其他類(lèi)加載器,則這些類(lèi)將不會(huì )被織入。
字節碼轉換
在運行期,所有類(lèi)加載器加載字節碼前,前進(jìn)行攔截。
可以對所有類(lèi)進(jìn)行織入。
2 AOP里的公民 ?
Joinpoint:攔截點(diǎn),如某個(gè)業(yè)務(wù)方法。
Pointcut:Joinpoint的表達式,表示攔截哪些方法。一個(gè)Pointcut對應多個(gè)Joinpoint。
Advice: 要切入的邏輯。
Before Advice 在方法前切入。
After Advice 在方法后切入,拋出異常時(shí)也會(huì )切入。
After Returning Advice 在方法返回后切入,拋出異常則不會(huì )切入。
After Throwing Advice 在方法拋出異常時(shí)切入。
Around Advice 在方法執行前后切入,可以中斷或忽略原有流程的執行。 ?
公民之間的關(guān)系
織入器通過(guò)在切面中定義pointcut來(lái)搜索目標(被代理類(lèi))的JoinPoint(切入點(diǎn)),然后把要切入的邏輯(Advice)織入到目標對象里,生成代理類(lèi)。
3 AOP的實(shí)現機制
本章節將詳細介紹AOP有各種實(shí)現機制。
3.1 動(dòng)態(tài)代理
Java在JDK1.3后引入的動(dòng)態(tài)代理機制,使我們可以在運行期動(dòng)態(tài)的創(chuàng )建代理類(lèi)。使用動(dòng)態(tài)代理實(shí)現AOP需要有四個(gè)角色:被代理的類(lèi),被代理類(lèi)的接口,織入器,和InvocationHandler,而織入器使用接口反射機制生成一個(gè)代理類(lèi),然后在這個(gè)代理類(lèi)中織入代碼。被代理的類(lèi)是AOP里所說(shuō)的目標,InvocationHandler是切面,它包含了Advice和Pointcut。
3.1.1 使用動(dòng)態(tài)代理
那如何使用動(dòng)態(tài)代理來(lái)實(shí)現AOP。下面的例子演示在方法執行前織入一段記錄日志的代碼,其中Business是代理類(lèi),LogInvocationHandler是記錄日志的切面,IBusiness, IBusiness2是代理類(lèi)的接口,Proxy.newProxyInstance是織入器。
清單一:動(dòng)態(tài)代理的演示
Java代碼
public static void main(String[] args) {
//需要代理的接口,被代理類(lèi)實(shí)現的多個(gè)接口都必須在這里定義
Class[] proxyInterface = new Class[] { IBusiness.class, IBusiness2.class };
//構建AOP的Advice,這里需要傳入業(yè)務(wù)類(lèi)的實(shí)例
LogInvocationHandler handler = new LogInvocationHandler(new Business());
//生成代理類(lèi)的字節碼加載器
ClassLoader classLoader = DynamicProxyDemo.class.getClassLoader();
//織入器,織入代碼并生成代理類(lèi)
IBusiness2 proxyBusiness = (IBusiness2) Proxy.newProxyInstance(classLoader, proxyInterface, handler);
//使用代理類(lèi)的實(shí)例來(lái)調用方法。
proxyBusiness.doSomeThing2();
((IBusiness) proxyBusiness).doSomeThing();
}
/**
* 打印日志的切面
*/
public static class LogInvocationHandler implements InvocationHandler {
private Object target; //目標對象
LogInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//執行原有邏輯
Object rev = method.invoke(target, args);
//執行織入的日志,你可以控制哪些方法執行切入邏輯
if (method.getName().equals("doSomeThing2")) {
System.out.println("記錄日志");
}
return rev;
}
}
接口IBusiness和IBusiness2定義省略。
業(yè)務(wù)類(lèi),需要代理的類(lèi)。
Java代碼
public class Business implements IBusiness, IBusiness2 {
@Override
public boolean doSomeThing() {
System.out.println("執行業(yè)務(wù)邏輯");
return true;
}
@Override
public void doSomeThing2() {
System.out.println("執行業(yè)務(wù)邏輯2");
}
}
輸出
Java代碼
執行業(yè)務(wù)邏輯2
記錄日志
執行業(yè)務(wù)邏輯
可以看到“記錄日志”的邏輯切入到Business類(lèi)的doSomeThing方法前了。
3.1.2 動(dòng)態(tài)代理原理
本節將結合動(dòng)態(tài)代理的源代碼講解其實(shí)現原理。動(dòng)態(tài)代理的核心其實(shí)就是代理對象的生成,即Proxy.newProxyInstance(classLoader, proxyInterface, handler)。讓我們進(jìn)入newProxyInstance方法觀(guān)摩下,核心代碼其實(shí)就三行。
清單二:生成代理類(lèi)
Java代碼
//獲取代理類(lèi)
Class cl = getProxyClass(loader, interfaces);
//獲取帶有InvocationHandler參數的構造方法
Constructor cons = cl.getConstructor(constructorParams);
//把handler傳入構造方法生成實(shí)例
return (Object) cons.newInstance(new Object[] { h });
其中g(shù)etProxyClass(loader, interfaces)方法用于獲取代理類(lèi),它主要做了三件事情:在當前類(lèi)加載器的緩存里搜索是否有代理類(lèi),沒(méi)有則生成代理類(lèi)并緩存在本地JVM里。清單三:查找代理類(lèi)。
Java代碼
// 緩存的key使用接口名稱(chēng)生成的List
Object key = Arrays.asList(interfaceNames);
synchronized (cache) {
do {
Object value = cache.get(key);
// 緩存里保存了代理類(lèi)的引用
if (value instanceof Reference) {
proxyClass = (Class) ((Reference) value).get();
}
if (proxyClass != null) {
// 代理類(lèi)已經(jīng)存在則返回
return proxyClass;
} else if (value == pendingGenerationMarker) {
// 如果代理類(lèi)正在產(chǎn)生,則等待
try {
cache.wait();
} catch (InterruptedException e) {
}
continue;
} else {
//沒(méi)有代理類(lèi),則標記代理準備生成
cache.put(key, pendingGenerationMarker);
break;
}
} while (true);
}
代理類(lèi)的生成主要是以下這兩行代碼。 清單四:生成并加載代理類(lèi)
Java代碼
//生成代理類(lèi)的字節碼文件并保存到硬盤(pán)中(默認不保存到硬盤(pán))
proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);
//使用類(lèi)加載器將字節碼加載到內存中
proxyClass = defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);
ProxyGenerator.generateProxyClass()方法屬于sun.misc包下,Oracle并沒(méi)有提供源代碼,但是我們可以使用JD-GUI這樣的反編譯軟件打開(kāi)jre\lib\rt.jar來(lái)一探究竟,以下是其核心代碼的分析。
清單五:代理類(lèi)的生成過(guò)程
Java代碼
//添加接口中定義的方法,此時(shí)方法體為空
for (int i = 0; i < this.interfaces.length; i++) {
localObject1 = this.interfaces[i].getMethods();
for (int k = 0; k < localObject1.length; k++) {
addProxyMethod(localObject1[k], this.interfaces[i]);
}
}
//添加一個(gè)帶有InvocationHandler的構造方法
MethodInfo localMethodInfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1);
//循環(huán)生成方法體代碼(省略)
//方法體里生成調用InvocationHandler的invoke方法代碼。(此處有所省略)
this.cp.getInterfaceMethodRef("InvocationHandler", "invoke", "Object; Method; Object;")
//將生成的字節碼,寫(xiě)入硬盤(pán),前面有個(gè)if判斷,默認情況下不保存到硬盤(pán)。
localFileOutputStream = new FileOutputStream(ProxyGenerator.access$000(this.val$name) + ".class");
localFileOutputStream.write(this.val$classFile);
那么通過(guò)以上分析,我們可以推出動(dòng)態(tài)代理為我們生成了一個(gè)這樣的代理類(lèi)。把方法doSomeThing的方法體修改為調用LogInvocationHandler的invoke方法。
清單六:生成的代理類(lèi)源碼
Java代碼
public class ProxyBusiness implements IBusiness, IBusiness2 {
private LogInvocationHandler h;
@Override
public void doSomeThing2() {
try {
Method m = (h.target).getClass().getMethod("doSomeThing", null);
h.invoke(this, m, null);
} catch (Throwable e) {
// 異常處理(略)
}
}
@Override
public boolean doSomeThing() {
try {
Method m = (h.target).getClass().getMethod("doSomeThing2", null);
return (Boolean) h.invoke(this, m, null);
} catch (Throwable e) {
// 異常處理(略)
}
return false;
}
public ProxyBusiness(LogInvocationHandler h) {
this.h = h;
}
//測試用
public static void main(String[] args) {
//構建AOP的Advice
LogInvocationHandler handler = new LogInvocationHandler(new Business());
new ProxyBusiness(handler).doSomeThing();
new ProxyBusiness(handler).doSomeThing2();
}
}
3.1.3 小結
從前兩節的分析我們可以看出,動(dòng)態(tài)代理在運行期通過(guò)接口動(dòng)態(tài)生成代理類(lèi),這為其帶來(lái)了一定的靈活性,但這個(gè)靈活性卻帶來(lái)了兩個(gè)問(wèn)題,第一代理類(lèi)必須實(shí)現一個(gè)接口,如果沒(méi)實(shí)現接口會(huì )拋出一個(gè)異常。第二性能影響,因為動(dòng)態(tài)代理使用反射的機制實(shí)現的,首先反射肯定比直接調用要慢,經(jīng)過(guò)測試大概每個(gè)代理類(lèi)比靜態(tài)代理多出10幾毫秒的消耗。其次使用反射大量生成類(lèi)文件可能引起Full GC造成性能影響,因為字節碼文件加載后會(huì )存放在JVM運行時(shí)區的方法區(或者叫持久代)中,當方法區滿(mǎn)的時(shí)候,會(huì )引起Full GC,所以當你大量使用動(dòng)態(tài)代理時(shí),可以將持久代設置大一些,減少Full GC次數。
3.2 動(dòng)態(tài)字節碼生成
使用動(dòng)態(tài)字節碼生成技術(shù)實(shí)現AOP原理是在運行期間目標字節碼加載后,生成目標類(lèi)的子類(lèi),將切面邏輯加入到子類(lèi)中,所以使用Cglib實(shí)現AOP不需要基于接口。
本節介紹如何使用Cglib來(lái)實(shí)現動(dòng)態(tài)字節碼技術(shù)。Cglib是一個(gè)強大的,高性能的Code生成類(lèi)庫,它可以在運行期間擴展Java類(lèi)和實(shí)現Java接口,它封裝了Asm,所以使用Cglib前需要引入Asm的jar。 清單七:使用CGLib實(shí)現AOP
Java代碼
public static void main(String[] args) {
byteCodeGe();
}
public static void byteCodeGe() {
//創(chuàng )建一個(gè)織入器
Enhancer enhancer = new Enhancer();
//設置父類(lèi)
enhancer.setSuperclass(Business.class);
//設置需要織入的邏輯
enhancer.setCallback(new LogIntercept());
//使用織入器創(chuàng )建子類(lèi)
IBusiness2 newBusiness = (IBusiness2) enhancer.create();
newBusiness.doSomeThing2();
}
/**
* 記錄日志
*/
public static class LogIntercept implements MethodInterceptor {
@Override
public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
//執行原有邏輯,注意這里是invokeSuper
Object rev = proxy.invokeSuper(target, args);
//執行織入的日志
if (method.getName().equals("doSomeThing2")) {
System.out.println("記錄日志");
}
return rev;
}
}
3.3 自定義類(lèi)加載器
如果我們實(shí)現了一個(gè)自定義類(lèi)加載器,在類(lèi)加載到JVM之前直接修改某些類(lèi)的方法,并將切入邏輯織入到這個(gè)方法里,然后將修改后的字節碼文件交給虛擬機運行,那豈不是更直接。
Javassist是一個(gè)編輯字節碼的框架,可以讓你很簡(jiǎn)單地操作字節碼。它可以在運行期定義或修改Class。使用Javassist實(shí)現AOP的原理是在字節碼加載前直接修改需要切入的方法。這比使用Cglib實(shí)現AOP更加高效,并且沒(méi)太多限制,實(shí)現原理如下圖:
我們使用系統類(lèi)加載器啟動(dòng)我們自定義的類(lèi)加載器,在這個(gè)類(lèi)加載器里加一個(gè)類(lèi)加載監聽(tīng)器,監聽(tīng)器發(fā)現目標類(lèi)被加載時(shí)就織入切入邏輯,咱們再看看使用Javassist實(shí)現AOP的代碼:
清單八:?jiǎn)?dòng)自定義的類(lèi)加載器
Java代碼
//獲取存放CtClass的容器ClassPool
ClassPool cp = ClassPool.getDefault();
//創(chuàng )建一個(gè)類(lèi)加載器
Loader cl = new Loader();
//增加一個(gè)轉換器
cl.addTranslator(cp, new MyTranslator());
//啟動(dòng)MyTranslator的main函數
cl.run("jsvassist.JavassistAopDemo$MyTranslator", args);
清單九:類(lèi)加載監聽(tīng)器
Java代碼
public static class MyTranslator implements Translator {
public void start(ClassPool pool) throws NotFoundException, CannotCompileException {
}
/* *
* 類(lèi)裝載到JVM前進(jìn)行代碼織入
*/
public void onLoad(ClassPool pool, String classname) {
if (!"model$Business".equals(classname)) {
return;
}
//通過(guò)獲取類(lèi)文件
try {
CtClass cc = pool.get(classname);
//獲得指定方法名的方法
CtMethod m = cc.getDeclaredMethod("doSomeThing");
//在方法執行前插入代碼
m.insertBefore("{ System.out.println(\"記錄日志\"); }");
} catch (NotFoundException e) {
} catch (CannotCompileException e) {
}
}
public static void main(String[] args) {
Business b = new Business();
b.doSomeThing2();
b.doSomeThing();
}
}
輸出:
Java代碼
執行業(yè)務(wù)邏輯2
記錄日志
執行業(yè)務(wù)邏輯
其中Bussiness類(lèi)在本文的清單一中定義??雌饋?lái)是不是特別簡(jiǎn)單,CtClass是一個(gè)class文件的抽象描述。咱們也可以使用insertAfter()在方法的末尾插入代碼,使用insertAt()在指定行插入代碼。
3.3.1 小結
從本節中可知,使用自定義的類(lèi)加載器實(shí)現AOP在性能上要優(yōu)于動(dòng)態(tài)代理和Cglib,因為它不會(huì )產(chǎn)生新類(lèi),但是它仍然存在一個(gè)問(wèn)題,就是如果其他的類(lèi)加載器來(lái)加載類(lèi)的話(huà),這些類(lèi)將不會(huì )被攔截。
3.4 字節碼轉換
自定義的類(lèi)加載器實(shí)現AOP只能攔截自己加載的字節碼,那么有沒(méi)有一種方式能夠監控所有類(lèi)加載器加載字節碼呢?有,使用Instrumentation,它是 Java 5 提供的新特性,使用 Instrumentation,開(kāi)發(fā)者可以構建一個(gè)字節碼轉換器,在字節碼加載前進(jìn)行轉換。本節使用Instrumentation和javassist來(lái)實(shí)現AOP。
3.4.1 構建字節碼轉換器
首先需要創(chuàng )建字節碼轉換器,該轉換器負責攔截Business類(lèi),并在Business類(lèi)的doSomeThing方法前使用javassist加入記錄日志的代碼。
Java代碼
public class MyClassFileTransformer implements ClassFileTransformer {
/**
* 字節碼加載到虛擬機前會(huì )進(jìn)入這個(gè)方法
*/
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
System.out.println(className);
//如果加載Business類(lèi)才攔截
if (!"model/Business".equals(className)) {
return null;
}
//javassist的包名是用點(diǎn)分割的,需要轉換下
if (className.indexOf("/") != -1) {
className = className.replaceAll("/", ".");
}
try {
//通過(guò)包名獲取類(lèi)文件
CtClass cc = ClassPool.getDefault().get(className);
//獲得指定方法名的方法
CtMethod m = cc.getDeclaredMethod("doSomeThing");
//在方法執行前插入代碼
m.insertBefore("{ System.out.println(\"記錄日志\"); }");
return cc.toBytecode();
} catch (NotFoundException e) {
} catch (CannotCompileException e) {
} catch (IOException e) {
//忽略異常處理
}
return null;
}
3.4.2 注冊轉換器
使用premain函數注冊字節碼轉換器,該方法在main函數之前執行。
Java代碼
public class MyClassFileTransformer implements ClassFileTransformer {
public static void premain(String options, Instrumentation ins) {
//注冊我自己的字節碼轉換器
ins.addTransformer(new MyClassFileTransformer());
}
}
3.4.3 配置和執行
需要告訴JVM在啟動(dòng)main函數之前,需要先執行premain函數。首先需要將premain函數所在的類(lèi)打成jar包。并修改該jar包里的META-INF\MANIFEST.MF 文件。
Java代碼
Manifest-Version: 1.0
Premain-Class: bci. MyClassFileTransformer
然后在JVM的啟動(dòng)參數里加上。-javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar
3.4.4 輸出
執行main函數,你會(huì )發(fā)現切入的代碼無(wú)侵入性的織入進(jìn)去了。
Java代碼
public static void main(String[] args) {
new Business().doSomeThing();
new Business().doSomeThing2();
}
輸出
Java代碼
model/Business
sun/misc/Cleaner
java/lang/Enum
model/IBusiness
model/IBusiness2
記錄日志
執行業(yè)務(wù)邏輯
執行業(yè)務(wù)邏輯2
java/lang/Shutdown
java/lang/Shutdown$Lock
從輸出中可以看到系統類(lèi)加載器加載的類(lèi)也經(jīng)過(guò)了這里。
4 AOP實(shí)戰
說(shuō)了這么多理論,那AOP到底能做什么呢? AOP能做的事情非常多。
性能監控,在方法調用前后記錄調用時(shí)間,方法執行太長(cháng)或超時(shí)報警。
緩存代理,緩存某方法的返回值,下次執行該方法時(shí),直接從緩存里獲取。
軟件破解,使用AOP修改軟件的驗證類(lèi)的判斷邏輯。
記錄日志,在方法執行前后記錄系統日志。
工作流系統,工作流系統需要將業(yè)務(wù)代碼和流程引擎代碼混合在一起執行,那么我們可以使用AOP將其分離,并動(dòng)態(tài)掛接業(yè)務(wù)。
權限驗證,方法執行前驗證是否有權限執行當前方法,沒(méi)有則拋出沒(méi)有權限執行異常,由業(yè)務(wù)代碼捕捉。
4.1 Spring的AOP
Spring默認采取的動(dòng)態(tài)代理機制實(shí)現AOP,當動(dòng)態(tài)代理不可用時(shí)(代理類(lèi)無(wú)接口)會(huì )使用CGlib機制。但Spring的AOP有一定的缺點(diǎn),第一個(gè)只能對方法進(jìn)行切入,不能對接口,字段,靜態(tài)代碼塊進(jìn)行切入(切入接口的某個(gè)方法,則該接口下所有實(shí)現類(lèi)的該方法將被切入)。第二個(gè)同類(lèi)中的互相調用方法將不會(huì )使用代理類(lèi)。因為要使用代理類(lèi)必須從Spring容器中獲取Bean。第三個(gè)性能不是最好的,從3.3章節我們得知使用自定義類(lèi)加載器,性能要優(yōu)于動(dòng)態(tài)代理和CGlib。
可以獲取代理類(lèi)
Java代碼
public IMsgFilterService getThis()
{
return (IMsgFilterService) AopContext.currentProxy();
}
public boolean evaluateMsg () {
// 執行此方法將織入切入邏輯
return getThis().evaluateMsg(String message);
}
@MethodInvokeTimesMonitor("KEY_FILTER_NUM")
public boolean evaluateMsg(String message) {
不能獲取代理類(lèi)
Java代碼
public boolean evaluateMsg () {
// 執行此方法將不會(huì )織入切入邏輯
return evaluateMsg(String message);
}
@MethodInvokeTimesMonitor("KEY_FILTER_NUM")
public boolean evaluateMsg(String message) {
4.2 參考資料
Java 動(dòng)態(tài)代理機制分析及擴展
CGlib的官方網(wǎng)站
ASM官方網(wǎng)站
JbossAOP
Java5特性Instrumenttation實(shí)踐
AOP的實(shí)現機制.pdf (794.8 KB)
下載次數: 3975
Aop實(shí)現機制測試代碼.rar (2.3 MB)
下載次數: 2211
查看圖片附件