Spring AOP 實現機制
(1)AOP的各種實現
在編譯器修改原始碼、在執行期位元組碼載入前修改位元組碼或位元組碼載入後動態建立代理類的位元組碼。以下是各種實現機制的比較:
類別分為靜態AOP(包括靜態織入)和動態AOP(包括動態代理、動態位元組碼生成、自定義類載入器、位元組碼轉換)。
靜態織入:
a、原理:在編譯期,切面直接以位元組碼形式編譯到目標位元組碼檔案中 ;
b、優點:對系統性能無影響;
c、缺點:不夠靈活;
動態代理 :
a、原理:在執行期,目標類載入後,為介面動態生成代理類。將切面織入到代理類中;
b、優點:更靈活;
c、缺點:切入的關注點要實現介面;
動態位元組碼生成:
a、原理:在執行期,目標類載入後,動態構建位元組碼檔案生成目標類的子類,將切面邏輯加入到子類中;
b、優點:沒有介面也可以織入;
c、缺點:擴充套件類的例項方法為final時,無法進行織入;
自定義類載入器
a、原理:在執行期,目標載入前,將切面邏輯加到目標位元組碼裡;
b、優點:可以對絕大部分類進行織入;
c、缺點:程式碼中若使用了其它類載入器,則這些類將不會被織入;
位元組碼轉換
a、原理:在執行期,所有類載入器載入位元組碼前進行攔截;
b、優點:可以對所有類進行織入;
c、缺點:
(2)
Joinpoint:攔截點,如某個業務方法;
Pointcut:Jointpoint的表示式,表示攔截哪些方法。一個Pointcut對應多個Joinpoint;
Advice:要切入的邏輯。
Before Advice:在方法前切入;
After Advice:在方法後切入,丟擲異常時也會切入;
After Returning Advice:在方法返回後切入,丟擲異常不會切入;
After Throwing Advice:在方法丟擲異常時切入;
Around Advice:在方法執行前後切入,可以中斷或忽略原有流程的執行;
目標 切面 織入器 代理類
Jointpoint Advice
Pointcut
Pointcut
織入器通過在切面中定義pointcut來搜尋目標(被代理類)的Jointpoint(切入點),然後把要切入的邏輯(advice)織入到目標物件裡,生成代理類。
(3)動態代理的實現
Java在JDK1.3後引入的動態代理機制,使我們可以在執行期動態的建立代理類。
使用動態代理實現AOP需要四個角色:被代理的類、被代理類的介面、織入器(Proxy.newProxyInstance())、InvocationHandler。織入器使用介面反射機制生成一個代理類,然後在這個代理類中織入程式碼(切入邏輯)。InvocationHandler是切面,包含了Advice和Pointcut。
動態代理在執行期通過介面動態生成代理類。
使用反射大量生成類檔案可能引起Full GC造成效能影響,因為位元組碼檔案載入後會 存放在JVM執行時區的方法區中(或持久代)。當方法區滿的時候,會引起Full GC。因此當大量使用動態代理時,可以將持久代設定大一些,減少Full GC次數。
動態代理的核心其實就是代理物件的生成,即Proxy.newProxyInstance()。其中getProxyClass()方法用於獲取代理類,主要做了三件事:在當前類載入器的快取裡搜尋是否有代理類,沒有則生成代理類並快取在本地JVM裡。
可以使用JD-GUI反編譯軟體開啟jre\lib\rt.jar。
動態代理生成的代理類,類似於:
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();
}
}
代理的目的是呼叫目標方法時轉而執行InvocationHandler類的invoke方法!
(4)動態位元組碼生成
使用動態位元組碼生成技術實現AOP原理:在執行期間目標位元組碼載入後,生成目標類的子類,將切面邏輯加入到子類中,所以使用Cglib實現AOP不需要基於介面。
使用Cglib實現動態位元組碼:
Cglib是高效能的code生成類庫,可以在執行期間擴充套件java類和實現java介面。它封裝了Asm,使用Cglib前要引入Asm的jar。
如:
public static void byteCodeGe() {
//建立一個織入器
Enhancer enhancer = new Enhancer();
//設定父類
enhancer.setSuperclass(Business.class);
//設定需要織入的邏輯
enhancer.setCallback(new LogIntercept());
//使用織入器建立子類
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)自定義類載入器
實現一個自定義類載入器,在類載入到JVM之前直接修改類的方法,並將切入邏輯織入到這個方法裡,然後將修改後的位元組碼檔案交給JVM執行。
Javassist是一個編輯位元組碼的框架,可以很簡單操作位元組碼。它可以在執行期定義或修改Class。使用Javassist實現AOP的原理是在位元組碼載入前直接修改需要切入的方法,比使用Cglib實現AOP更加高效。
原理:
系統類載入器——>啟動——>自定義類載入器(類載入監聽器)——>載入——>類檔案
使用系統類載入器啟動自定義的類載入器,在這個類載入器里加一個類載入監聽器,監聽器發現目標類被載入時就織入切入邏輯。
啟動自定義的類載入器:
//獲取存放CtClass的容器ClassPool
ClassPool cp = ClassPool.getDefault();
//建立一個類載入器
Loader cl = new Loader();
//增加一個轉換器
cl.addTranslator(cp, new MyTranslator());
//啟動MyTranslator的main函式
cl.run("javassist.JavassistAopDemo$MyTranslator", args);
類載入監聽器:
public static class MyTranslator implements Translator {
public void start(ClassPool pool) throws Exception {
}
/**類載入到JVM前進行程式碼織入*/
public void onLoad(ClassPool pool, String classname) {
if(!"model$Business".equals(classname)) {
return ;
}
//通過獲取類檔案
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是一個class檔案的抽象描述。可以使用insertAfter()在方法的末尾插入程式碼,使用insertAt()在指定行插入程式碼。
使用自定義的類載入器實現AOP在效能上要優於動態代理和Cglib,因為它不會產生新類。但存在一個問題,就是若其他的類載入器來載入類的話,這些類將不會被攔截。
(6)位元組碼轉換
自定義的類載入器實現AOP只能攔截自己載入的位元組碼,有沒有能夠監控所有類載入器載入位元組碼?——>有,使用Instrumentation,它是Java 5提供的新特性。使用Instrumentation可以構建一個位元組碼轉換器,在位元組碼載入前進行轉換。使用Instrumentation和javassist實現AOP。
一、構建位元組碼轉換器
首先建立位元組碼轉換器,該轉換器負責攔截Business類,並在Business類的doSomeThing方法前使用javassist加入記錄日誌的程式碼。
public class MyClassFileTransformer implements ClassFileTransformer {
/**位元組碼載入到JVM前會進入這個方法*/
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws Exception{
//若載入Business類才攔截
if(!"model/Business".equals(className)) {
return null;
}
//javassist的包名是用點分割的,要轉換下
if(className.indexOf("/") != -1) {
className = className.replaceAll("/", ".");
}
try{
//通過包名獲取類檔案
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在啟動main函式之前,需要先執行premain函式。首先需要將premain函式所在的類打成jar包。並修改該jar包裡的META-INF\MANIFEST.MF檔案。
Manifest-Version:1.0
Premain-Class:bci. MyClassFileTransformer
然後在JVM的啟動引數里加上:
-javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar
四、輸出
執行main函式,會發現切入的程式碼無侵入性的織入進去了。
public static void main(String[] args) {
new Business().doSomeThing();
}
2、PS
(1)AOP能做的事情:
效能監控:在方法呼叫前後記錄呼叫時間,方法執行太長或超時報警。
快取代理:快取某方法的返回值,下次執行該方法時,直接從快取裡獲取。
軟體破解:使用AOP修改軟體的驗證類的判斷邏輯。
工作流系統:工作流系統需要將業務程式碼和流程引擎程式碼混合在一起執行,可以使用AOP將其分離,並動態掛接業務。
許可權驗證:方法執行前驗證是否有許可權執行當前方法。
(2)方法呼叫成功後——>統計呼叫次數——>存入快取伺服器——>每日存入資料庫
因為每天的方法呼叫次數近百萬,為了降低資料庫壓力不能實時入庫。
一、如何使用:
只要配置了註解的方法將會被統計呼叫次數。
@MethodInvokeTimesMonitor(value="aaa", returnValue=false)
public void aa() {
}
二、如何配置:
使用AspectJ的方式配置AOP。需要啟動對AspectJ的支援。
<aop:aspectj-autoproxy proxy-target-class="true"/>
true表示讓spring使用Cglib實現AOP,配置為false表示使用動態代理實現AOP,預設使用動態代理。
三、定義註解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodInvokeTimesMonitor{
String value();
boolean returnValue() default true;
}
四、定義切面,在切面中定義攔截的方法和在方法返回後記錄呼叫次數的Advice。在這裡定義攔截所有配置了註解的方法。
@Aspect
public class MethodAspect {
/**切入點,所有配置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用於定義切入點表示式。@AfterReturning表示在方法執行後進行切入,裡面的MethodAspect.allMethodInvokeTimesMonitor()表示使用這個方法的切入點表示式,@annotation(methodInvokeTimesMonitor)表示將當引數傳遞給statInvokeTimes()方法,returning="retVal"表示將被切入方法的返回值賦值給retVal,並傳遞給statInvokeTimes()方法。
(3)spring預設採用動態代理機制實現AOP,當動態代理不可用時(代理類無介面)會使用Cglib機制。
使用spring的AOP缺點:
a、只能對方法進行切入,不能對介面、屬性、靜態程式碼塊進行切入(切入介面的某個方法,則該介面下所有實現類的該方法將被切入)
b、同類中的互相呼叫方法將不會使用代理類。因為要使用代理類必須從spring容器中獲取bean。
獲取代理類,如:
public IMsgFilterService getThis() {
return (IMsgFilterService)AopContext.currentProxy();
}
1 AOP各種的實現
AOP就是面向切面程式設計,我們可以從幾個層面來實現AOP。
在編譯器修改原始碼,在執行期位元組碼載入前修改位元組碼或位元組碼載入後動態建立代理類的位元組碼,以下是各種實現機制的比較。
類別 |
機制 |
原理 |
優點 |
缺點 |
靜態AOP |
靜態織入 |
在編譯期,切面直接以位元組碼的形式編譯到目標位元組碼檔案中。 |
對系統無效能影響。 |
靈活性不夠。 |
動態AOP |
動態代理 |
在執行期,目標類載入後,為介面動態生成代理類,將切面植入到代理類中。 |
相對於靜態AOP更加靈活。 |
切入的關注點需要實現介面。對系統有一點效能影響。 |
動態位元組碼生成 |
在執行期,目標類載入後,動態構建位元組碼檔案生成目標類的子類,將切面邏輯加入到子類中。 |
沒有介面也可以織入。 |
擴充套件類的例項方法為final時,則無法進行織入。 |
|
自定義類載入器 |
在執行期,目標載入前,將切面邏輯加到目標位元組碼裡。 |
可以對絕大部分類進行織入。 |
程式碼中如果使用了其他類載入器,則這些類將不會被織入。 |
|
位元組碼轉換 |
在執行期,所有類載入器載入位元組碼前,前進行攔截。 |
可以對所有類進行織入。 |
2 AOP裡的公民
- Joinpoint:攔截點,如某個業務方法。
- Pointcut:Joinpoint的表示式,表示攔截哪些方法。一個Pointcut對應多個Joinpoint。
- Advice: 要切入的邏輯。
- Before Advice 在方法前切入。
- After Advice 在方法後切入,丟擲異常時也會切入。
- After Returning Advice 在方法返回後切入,丟擲異常則不會切入。
- After Throwing Advice 在方法丟擲異常時切入。
- Around Advice 在方法執行前後切入,可以中斷或忽略原有流程的執行。
- 公民之間的關係
織入器通過在切面中定義pointcut來搜尋目標(被代理類)的JoinPoint(切入點),然後把要切入的邏輯(Advice)織入到目標物件裡,生成代理類。
3 AOP的實現機制
本章節將詳細介紹AOP有各種實現機制。
3.1 動態代理
Java在JDK1.3後引入的動態代理機制,使我們可以在執行期動態的建立代理類。使用動態代理實現AOP需要有四個角色:被代理的類,被代理類的介面,織入器,和InvocationHandler,而織入器使用介面反射機制生成一個代理類,然後在這個代理類中織入程式碼。被代理的類是AOP裡所說的目標,InvocationHandler是切面,它包含了Advice和Pointcut。
3.1.1 使用動態代理
那如何使用動態代理來實現AOP。下面的例子演示在方法執行前織入一段記錄日誌的程式碼,其中Business是代理類,LogInvocationHandler是記錄日誌的切面,IBusiness, IBusiness2是代理類的介面,Proxy.newProxyInstance是織入器。
清單一:動態代理的演示
- public static void main(String[] args) {
- //需要代理的介面,被代理類實現的多個介面都必須在這裡定義
- Class[] proxyInterface = new Class[] { IBusiness.class, IBusiness2.class };
- //構建AOP的Advice,這裡需要傳入業務類的例項
- LogInvocationHandler handler = new LogInvocationHandler(new Business());
- //生成代理類的位元組碼載入器
- ClassLoader classLoader = DynamicProxyDemo.class.getClassLoader();
- //織入器,織入程式碼並生成代理類
- IBusiness2 proxyBusiness = (IBusiness2) Proxy.newProxyInstance(classLoader, proxyInterface, handler);
- //使用代理類的例項來呼叫方法。
- 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定義省略。
業務類,需要代理的類。
Java程式碼- public class Business implements IBusiness, IBusiness2 {
- @Override
- public boolean doSomeThing() {
- System.out.println("執行業務邏輯");
- return true;
- }
- @Override
- public void doSomeThing2() {
- System.out.println("執行業務邏輯2");
- }
- }
輸出
Java程式碼- 執行業務邏輯2
- 記錄日誌
- 執行業務邏輯
可以看到“記錄日誌”的邏輯切入到Business類的doSomeThing方法前了。
3.1.2 動態代理原理
本節將結合動態代理的原始碼講解其實現原理。動態代理的核心其實就是代理物件的生成,即Proxy.newProxyInstance(classLoader, proxyInterface, handler)。讓我們進入newProxyInstance方法觀摩下,核心程式碼其實就三行。
清單二:生成代理類
- //獲取代理類
- Class cl = getProxyClass(loader, interfaces);
- //獲取帶有InvocationHandler引數的構造方法
- Constructor cons = cl.getConstructor(constructorParams);
- //把handler傳入構造方法生成例項
- return (Object) cons.newInstance(new Object[] { h });
其中getProxyClass(loader, interfaces)方法用於獲取代理類,它主要做了三件事情:在當前類載入器的快取裡搜尋是否有代理類,沒有則生成代理類並快取在本地JVM裡。清單三:查詢代理類。
Java程式碼- // 快取的key使用介面名稱生成的List
- Object key = Arrays.asList(interfaceNames);
- synchronized (cache) {
- do {
- Object value = cache.get(key);
- // 快取裡儲存了代理類的引用
- if (value instanceof Reference) {
- proxyClass = (Class) ((Reference) value).get();
- }
- if (proxyClass != null) {
- // 代理類已經存在則返回
- return proxyClass;
- } else if (value == pendingGenerationMarker) {
- // 如果代理類正在產生,則等待
- try {
- cache.wait();
- } catch (InterruptedException e) {
- }
- continue;
- } else {
- //沒有代理類,則標記代理準備生成
- cache.put(key, pendingGenerationMarker);
- break;
- }
- } while (true);
- }
代理類的生成主要是以下這兩行程式碼。 清單四:生成並載入代理類
Java程式碼- //生成代理類的位元組碼檔案並儲存到硬碟中(預設不儲存到硬碟)
- proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);
- //使用類載入器將位元組碼載入到記憶體中
- proxyClass = defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);
ProxyGenerator.generateProxyClass()方法屬於sun.misc包下,Oracle並沒有提供原始碼,但是我們可以使用JD-GUI這樣的反編譯軟體開啟jre\lib\rt.jar來一探究竟,以下是其核心程式碼的分析。
清單五:代理類的生成過程
- //新增介面中定義的方法,此時方法體為空
- 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]);
- }
- }
- //新增一個帶有InvocationHandler的構造方法
- MethodInfo localMethodInfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1);
- //迴圈生成方法體程式碼(省略)
- //方法體裡生成呼叫InvocationHandler的invoke方法程式碼。(此處有所省略)
- this.cp.getInterfaceMethodRef("InvocationHandler", "invoke", "Object; Method; Object;")
- //將生成的位元組碼,寫入硬碟,前面有個if判斷,預設情況下不儲存到硬碟。
- localFileOutputStream = new FileOutputStream(ProxyGenerator.access$000(this.val$name) + ".class");
- localFileOutputStream.write(this.val$classFile);
那麼通過以上分析,我們可以推出動態代理為我們生成了一個這樣的代理類。把方法doSomeThing的方法體修改為呼叫LogInvocationHandler的invoke方法。
清單六:生成的代理類原始碼
- 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) {