AOP的實現機制,AOP實現機制
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是織入器。
清單一:動態代理的演示
Java代碼
- 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方法觀摩下,核心代碼其實就三行。
清單二:生成代理類
Java代碼
- //獲取代理類
- 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來一探究竟,以下是其核心代碼的分析。
清單五:代理類的生成過程
Java代碼
- //添加接口中定義的方法,此時方法體為空
- 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方法。
清單六:生成的代理類源碼
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 小結
從前兩節的分析我們可以看出,動態代理在運行期通過接口動態生成代理類,這為其帶來了一定的靈活性,但這個靈活性卻帶來了兩個問題,第一代理類必須實現一
個接口,如果沒實現接口會拋出一個異常。第二性能影響,因為動態代理使用反射的機制實現的,首先反射肯定比直接調用要慢,經過測試大概每個代理類比靜態代
理多出10幾毫秒的消耗。其次使用反射大量生成類文件可能引起Full
GC造成性能影響,因為字節碼文件加載後會存放在JVM運行時區的方法區(或者叫持久代)中,當方法區滿的時候,會引起Full
GC,所以當你大量使用動態代理時,可以將持久代設置大一些,減少Full GC次數。
3.2 動態字節碼生成
使用動態字節碼生成技術實現AOP原理是在運行期間目標字節碼加載後,生成目標類的子類,將切面邏輯加入到子類中,所以使用Cglib實現AOP不需要基於接口。
本節介紹如何使用Cglib來實現動態字節碼技術。Cglib是一個強大的,高性能的Code生成類庫,它可以在運行期間擴展Java類和實現Java接口,它封裝了Asm,所以使用Cglib前需要引入Asm的jar。 清單七:使用CGLib實現AOP
Java代碼
- public static void main(String[] args) {
- byteCodeGe();
- }
-
- public static void byteCodeGe() {
- //創建一個織入器
- Enhancer enhancer = new Enhancer();
- //設置父類
- enhancer.setSuperclass(Business.class);
- //設置需要織入的邏輯
- enhancer.setCallback(new LogIntercept());
- //使用織入器創建子類
- 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 自定義類加載器
如果我們實現了一個自定義類加載器,在類加載到JVM之前直接修改某些類的方法,並將切入邏輯織入到這個方法裡,然後將修改後的字節碼文件交給虛擬機運行,那豈不是更直接。
Javassist是一個編輯字節碼的框架,可以讓你很簡單地操作字節碼。它可以在運行期定義或修改Class。使用Javassist實現AOP的原理是在字節碼加載前直接修改需要切入的方法。這比使用Cglib實現AOP更加高效,並且沒太多限制,實現原理如下圖:
我們使用系統類加載器啟動我們自定義的類加載器,在這個類加載器裡加一個類加載監聽器,監聽器發現目標類被加載時就織入切入邏輯,咱們再看看使用Javassist實現AOP的代碼:
清單八:啟動自定義的類加載器
Java代碼
- //獲取存放CtClass的容器ClassPool
- ClassPool cp = ClassPool.getDefault();
- //創建一個類加載器
- Loader cl = new Loader();
- //增加一個轉換器
- cl.addTranslator(cp, new MyTranslator());
- //啟動MyTranslator的main函數
- cl.run("jsvassist.JavassistAopDemo$MyTranslator", args);
清單九:類加載監聽器
Java代碼
- public static class MyTranslator implements Translator {
-
- public void start(ClassPool pool) throws NotFoundException, CannotCompileException {
- }
-
- /* *
- * 類裝載到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 (NotFoundException e) {
- } catch (CannotCompileException e) {
- }
- }
-
- public static void main(String[] args) {
- Business b = new Business();
- b.doSomeThing2();
- b.doSomeThing();
- }
- }
輸出:
Java代碼
- 執行業務邏輯2
- 記錄日志
- 執行業務邏輯
其中Bussiness類在本文的清單一中定義。看起來是不是特別簡單,CtClass是一個class文件的抽象描述。咱們也可以使用insertAfter()在方法的末尾插入代碼,使用insertAt()在指定行插入代碼。
3.3.1 小結
從本節中可知,使用自定義的類加載器實現AOP在性能上要優於動態代理和Cglib,因為它不會產生新類,但是它仍然存在一個問題,就是如果其他的類加載器來加載類的話,這些類將不會被攔截。
3.4 字節碼轉換
自定義的類加載器實現AOP只能攔截自己加載的字節碼,那麼有沒有一種方式能夠監控所有類加載器加載字節碼呢?有,使用Instrumentation,
它是 Java 5 提供的新特性,使用
Instrumentation,開發者可以構建一個字節碼轉換器,在字節碼加載前進行轉換。本節使用Instrumentation和
javassist來實現AOP。
3.4.1 構建字節碼轉換器
首先需要創建字節碼轉換器,該轉換器負責攔截Business類,並在Business類的doSomeThing方法前使用javassist加入記錄日志的代碼。
Java代碼
- public class MyClassFileTransformer implements ClassFileTransformer {
-
- /**
- * 字節碼加載到虛擬機前會進入這個方法
- */
- @Override
- public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
- ProtectionDomain protectionDomain, byte[] classfileBuffer)
- throws IllegalClassFormatException {
- System.out.println(className);
- //如果加載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 (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在啟動main函數之前,需要先執行premain函數。首先需要將premain函數所在的類打成jar包。並修改該jar包裡的META-INF\MANIFEST.MF 文件。
Java代碼
- Manifest-Version: 1.0
- Premain-Class: bci. MyClassFileTransformer
然後在JVM的啟動參數裡加上。-javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar
3.4.4 輸出
執行main函數,你會發現切入的代碼無侵入性的織入進去了。
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
- 記錄日志
- 執行業務邏輯
- 執行業務邏輯2
- java/lang/Shutdown
- java/lang/Shutdown$Lock
從輸出中可以看到系統類加載器加載的類也經過了這裡。
4 AOP實戰
說了這麼多理論,那AOP到底能做什麼呢? AOP能做的事情非常多。
- 性能監控,在方法調用前後記錄調用時間,方法執行太長或超時報警。
- 緩存代理,緩存某方法的返回值,下次執行該方法時,直接從緩存裡獲取。
- 軟件破解,使用AOP修改軟件的驗證類的判斷邏輯。
- 記錄日志,在方法執行前後記錄系統日志。
- 工作流系統,工作流系統需要將業務代碼和流程引擎代碼混合在一起執行,那麼我們可以使用AOP將其分離,並動態掛接業務。
- 權限驗證,方法執行前驗證是否有權限執行當前方法,沒有則拋出沒有權限執行異常,由業務代碼捕捉。
4.1 Spring的AOP
Spring默認采取的動態代理機制實現AOP,當動態代理不可用時(代理類無接口)會使用CGlib機制。但Spring的AOP有一定的缺點,第一個
只能對方法進行切入,不能對接口,字段,靜態代碼塊進行切入(切入接口的某個方法,則該接口下所有實現類的該方法將被切入)。第二個同類中的互相調用方法
將不會使用代理類。因為要使用代理類必須從Spring容器中獲取Bean。第三個性能不是最好的,從3.3章節我們得知使用自定義類加載器,性能要優於
動態代理和CGlib。
可以獲取代理類
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) {
不能獲取代理類
Java代碼
- public boolean evaluateMsg () {
- // 執行此方法將不會織入切入邏輯
- return evaluateMsg(String message);
- }
-
- @MethodInvokeTimesMonitor("KEY_FILTER_NUM")
- public boolean evaluateMsg(String message) {