1、AOP的各種實現
AOP就是面向切面編程,我們可以從以下幾個層面來實現AOP
- 在編譯期修改源代碼
- 在運行期字節碼加載前修改字節碼
- 在運行期字節碼加載后動態創建代理類的字節碼
2、AOP各種實現機制的比較
以下是各種實現機制的比較:
類別 | 機制 | 原理 | 優點 | 缺點 |
---|---|---|---|---|
靜態AOP | 靜態織入 | 在編譯期,切面直接以字節碼的形式編譯到目標字節碼文件中 | 對系統無性能影響 | 靈活性不夠 |
動態AOP | 動態代理 | 在運行期,目標類加載后,為接口動態生成代理類,將切面織入到代理類中 | 相對於靜態AOP更加靈活 | 切入的關注點需要實現接口。 對系統有一點性能影響 |
動態字節碼生成 | CGLIB | 在運行期,目標類加載后,動態構建字節碼文件生成目標類的子類,將切面邏輯加入到子類中 | 沒有接口也可以織入 | 擴展類的實例方法為final時,則無法進行織入 |
自定義類加載器 | 在運行期,目標加載前,將切面邏輯加到目標字節碼里 | 可以對絕大部分類進行織入 | 代碼中如果使用了其他類加載器,則這些類將不會被織入 | |
字節碼轉換 | 在運行期,所有類加載器加載字節碼前進行攔截 | 可以對所有類進行織入 |
3、AOP里的公民
- Joinpoint:攔截點,如某個業務方法
- Pointcut:Joinpoint的表達式,表示攔截哪些方法。一個Pointcut對應多個Joinpoint
- Advice:要切入的邏輯
- Before Advice:在方法前切入
- After Advice:在方法后切入,拋出異常則不會切入
- After Returning Advice:在方法返回后切入,拋出異常則不會切入
- After Throwing Advice:在方法拋出異常時切入
- Around Advice:在方法執行前后切入,可以中斷或忽略原有流程的執行
- 公民之間的關系
織入器通過在切面中定義pointcout來搜索目標(被代理類)的JoinPoint(切入點),然后把要切入的邏輯(Advice)織入到目標對象里,生成代理類
4、AOP的實現機制
- 動態代理
- 動態字節碼生成
- 自定義類加載器
- 字節碼轉換
4.1 動態代理
靜態代理:由程序員創建或特定工具自動生成源代碼,再對其編譯。在程序運行前,代理類的.class文件就已經存在了
動態代理:即在運行期動態創建代理類,使用動態代理實現AOP需要4個角色:
- 被代理的類:即AOP里所說的目標對象
- 被代理類的接口
- 織入器:使用接口反射機制生成一個代理類,在這個代理類中織入代碼
- InvocationHandler切面:切面,包含了Advice和Pointcut
4.1.1 動態代理的演示
例子演示的是在方法執行前織入一段記錄日志的代碼,其中
- Business是代理類
- LogInvocationHandler是記錄日志的切面
- IBusiness、IBusiness2是代理類的接口
- Proxy.newProxyInstance是織入器
public interface IBusiness { void doSomeThing(); } public interface IBusiness2 { void doSomeThing2(); } public class Business implements IBusiness, IBusiness2 { @Override public void doSomeThing() { System.out.println("執行業務邏輯"); } @Override public void doSomeThing2() { System.out.println("執行業務邏輯2"); } }
package aop; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; /** * 打印日志的切面 */ public class LogInvocationHandler implements InvocationHandler { private Object target;//目標對象 public LogInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //執行織入的日志,你可以控制哪些方法執行切入邏輯 if (method.getName().equals("doSomeThing2")) { System.out.println("記錄日志"); } //執行原有邏輯 Object recv = method.invoke(target, args); return recv; } }
package aop; import java.lang.reflect.Proxy; public class Main { public static void main(String[] args) { //需要代理的類接口,被代理類實現的多個接口都必須在這這里定義 Class[] proxyInterface = new Class[] {IBusiness.class, IBusiness2.class}; //構建AOP的Advice,這里需要傳入業務類的實例 LogInvocationHandler handler = new LogInvocationHandler(new Business()); //生成代理類的字節碼加載器 ClassLoader classLoader = Business.class.getClassLoader(); //織入器,織入代碼並生成代理類 IBusiness2 proxyBusiness = (IBusiness2) Proxy.newProxyInstance(classLoader, proxyInterface, handler); proxyBusiness.doSomeThing2(); ((IBusiness)proxyBusiness).doSomeThing(); } }
執行結果:
記錄日志
執行業務邏輯2
執行業務邏輯
4.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里
查找代理類getProxyClass(loader, interfaces)方法:
1 // 緩存的key使用接口名稱生成的List 2 Object key = Arrays.asList(interfaceNames); 3 synchronized (cache) { 4 do { 5 Object value = cache.get(key); 6 // 緩存里保存了代理類的引用 7 if (value instanceof Reference) { 8 proxyClass = (Class) ((Reference) value).get(); 9 } 10 if (proxyClass != null) { 11 // 代理類已經存在則返回 12 return proxyClass; 13 } else if (value == pendingGenerationMarker) { 14 // 如果代理類正在產生,則等待 15 try { 16 cache.wait(); 17 } catch (InterruptedException e) { 18 } 19 continue; 20 } else { 21 //沒有代理類,則標記代理准備生成 22 cache.put(key, pendingGenerationMarker); 23 break; 24 } 25 } while (true); 26 }
生成加載代理類:
//生成代理類的字節碼文件並保存到硬盤中(默認不保存到硬盤) proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces); //使用類加載器將字節碼加載到內存中 proxyClass = defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);
代理類生成過程ProxyGenerator.generateProxyClass()方法的核心代碼分析:
//添加接口中定義的方法,此時方法體為空 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);
通過以上分析,我們可以推出動態代理為我們生產了一個這樣的代理類。把方法soSomeThing的方法體修改為調用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) { // 異常處理(略) } } @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(); } }
4.1.3 小結
從前兩節的分析我們可以看出,動態代理在運行期通過接口動態生成代理類,這為其帶來了一定的靈活性,但這個靈活性卻帶來了兩個問題:
- 第一,代理類必須實現一個接口,如果沒實現接口會拋出一個異常
- 第二,性能影響,因為動態代理是使用反射機制實現的,首先反射肯定比直接調用要慢,其次使用反射大量生成類文件可能引起full gc,因為字節碼文件加載后會存放在JVM運行時方法區(或者叫永久代、元空間)中,當方法區滿時會引起full gc,所以當你大量使用動態代理時,可以將永久代設置大一些,減少full gc的次數
4.2 CGLIB動態字節碼生成
使用動態字節碼生成技術實現AOP原理是在運行期間目標字節碼加載后,生成目標類的子類,將切面邏輯加入到子類中,所以cglib實現AOP不需要基於接口
本節介紹如何使用cglib來實現動態字節碼技術。
cglib是一個強大的、高性能的Code生成類庫,它可以在運行期間擴展Java類和實現Java接口,它封裝了Asm,所以使用cglib前需要引入Asm的jar
4.2.1 使用cglib實現AOP
1 package cglib; 2 3 /** 4 * 這個是沒有實現接口的實現類 5 */ 6 public class BookFacadeImpl { 7 public void addBook() { 8 System.out.println("增加圖書的普通方法。。。"); 9 } 10 11 public void deleteBook() { 12 System.out.println("刪除圖書的普通方法。。。"); 13 } 14 }
1 package cglib; 2 3 import net.sf.cglib.proxy.Enhancer; 4 import net.sf.cglib.proxy.MethodInterceptor; 5 import net.sf.cglib.proxy.MethodProxy; 6 7 import java.lang.reflect.Method; 8 9 /** 10 * 使用cglib動態代理 11 */ 12 public class BookFacadeCglib implements MethodInterceptor { 13 14 private Object target; 15 16 /** 17 * 創建代理對象 18 * 19 * @param target 20 * @return 21 */ 22 public Object getInstance(Object target) { 23 this.target = target; 24 Enhancer enhancer = new Enhancer(); 25 enhancer.setSuperclass(this.target.getClass()); 26 //回調方法 27 enhancer.setCallback(this); 28 //創建代理 29 return enhancer.create(); 30 } 31 32 //回調方法 33 @Override 34 public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { 35 if (method.getName().equals("addBook")) { 36 System.out.println("記錄增加圖書的日志"); 37 } 38 methodProxy.invokeSuper(obj, args); 39 return null; 40 } 41 }
package cglib; /** * 測試cglib字節碼代理 */ public class TestCglib { public static void main(String[] args) { BookFacadeCglib cglib = new BookFacadeCglib(); BookFacadeImpl bookFacade = (BookFacadeImpl) cglib.getInstance(new BookFacadeImpl()); bookFacade.addBook(); bookFacade.deleteBook(); } }
執行結果:
記錄增加圖書的日志
增加圖書的普通方法。。。
刪除圖書的普通方法。。。
4.3 自定義類加載器
如果我們實現了一個自定義類加載器,在類加載到JVM之前直接修改某些類的方法,並將切入邏輯織入到這個方法里,然后將修改后的字節碼文件交給虛擬機運行,那豈不是更直接
Javassist是一個編輯字節碼的框架,可以讓你很簡單地操作字節碼。它可以在運行期定義或修改Class。使用Javassist實現AOP的原理是在字節碼加載前直接修改需要切入的方法
這比使用cglib實現AOP更加高效,並且沒有太多限制,實現原理如下圖:
我們使用類加載器啟動我們自定義的類加載器,在這個類加載器里加一個類加載監聽器,監聽器發現目標類被加載時就織入切入邏輯
4.3.1 Javassist實現AOP的代碼
清單1:啟動自定義的類加載器
//獲取存放CtClass的容器ClassPool ClassPool cp = ClassPool.getDefault(); //創建一個類加載器 Loader cl = new Loader(); //增加一個轉換器 cl.addTranslator(cp, new MyTranslator()); //啟動MyTranslator的main函數 cl.run("jsvassist.JavassistAopDemo$MyTranslator", args);
清單2:類加載監聽器
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(); } }
輸出:
執行業務邏輯2
記錄日志
執行業務邏輯
4.3.2 小結
從本節中可知,使用自定義的類加載器實現AOP在性能上有優於動態代理和cglib,因為它不會產生新類,但是它仍人存在一個問題,就是如果其他的類加載器來加載類的話,這些類就不會被攔截
4.4 字節碼轉換
自定義類加載器實現AOP只能攔截自己加載的字節碼,那么有一種方式能夠監控所有類加載器加載的字節碼嗎?
有,使用Instrumentation,它是Java5的新特性,使用Instrument,開發者可以構建一個字節碼轉換器,在字節碼加載前進行轉換
本節使用Instrumentation和javassist來實現AOP
4.4.1 構建字節碼轉換器
首先需要創建字節碼轉換器,該轉換器負責攔截Business類,並在Business類的doSomeThing方法前使用javassist加入記錄日志的代碼
1 public class MyClassFileTransformer implements ClassFileTransformer { 2 3 /** 4 * 字節碼加載到虛擬機前會進入這個方法 5 */ 6 @Override 7 public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, 8 ProtectionDomain protectionDomain, byte[] classfileBuffer) 9 throws IllegalClassFormatException { 10 System.out.println(className); 11 //如果加載Business類才攔截 12 if (!"model/Business".equals(className)) { 13 return null; 14 } 15 16 //javassist的包名是用點分割的,需要轉換下 17 if (className.indexOf("/") != -1) { 18 className = className.replaceAll("/", "."); 19 } 20 try { 21 //通過包名獲取類文件 22 CtClass cc = ClassPool.getDefault().get(className); 23 //獲得指定方法名的方法 24 CtMethod m = cc.getDeclaredMethod("doSomeThing"); 25 //在方法執行前插入代碼 26 m.insertBefore("{ System.out.println(\"記錄日志\"); }"); 27 return cc.toBytecode(); 28 } catch (NotFoundException e) { 29 } catch (CannotCompileException e) { 30 } catch (IOException e) { 31 //忽略異常處理 32 } 33 return null; 34 }
4.4.2 注冊轉換器
使用premain函數注冊字節碼轉換器,該方法在main函數之前執行
public class MyClassFileTransformer implements ClassFileTransformer { public static void premain(String options, Instrumentation ins) { //注冊我自己的字節碼轉換器 ins.addTransformer(new MyClassFileTransformer()); } }
4.4.3 配置和執行
需要告訴JVM在啟動main函數之前,需要先執行premain函數。
首先,需要將premain函數所在的類打成jar包,並修改jar包里的META-INF\MANIFEST.MF文件
1 Manifest-Version: 1.0 2 Premain-Class: bci. MyClassFileTransformer
其次,在JVM的啟動參數里加上-javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar
4.4.4 輸出
執行main函數,你會發現切入的代碼無侵入性的織入進去了
1 public static void main(String[] args) { 2 new Business().doSomeThing(); 3 new Business().doSomeThing2(); 4 } 5
輸出:
1 model/Business 2 sun/misc/Cleaner 3 java/lang/Enum 4 model/IBusiness 5 model/IBusiness2 6 記錄日志 7 執行業務邏輯 8 執行業務邏輯2 9 java/lang/Shutdown 10 java/lang/Shutdown$Lock
從輸出中可以看到系統類加載器加載的類也經過了這里
5、AOP實戰
5.1 AOP功能
- 性能監控:在方法調用前后記錄調用時間,方法執行太長或超時報警
- 緩存代理:緩存某方法的返回值,下次執行該方法時,直接從緩存里獲取
- 軟件破解:使用AOP修改軟件的驗證類的判斷邏輯
- 記錄日志:在方法執行前后記錄系統日志
- 工作流系統:工作流系統需要將業務代碼和流程引擎代碼混合在一起執行,那么我們可以使用AOP將其分離,並動態掛接業務
- 權限驗證:方法執行前驗證是否有權限執行當前方法,沒有則拋出沒有權限執行異常,由業務代碼捕捉
5.2 Spring的AOP
Spring默認采取動態代理機制實現AOP,當動態代理不可用時(代理類無接口)會使用cglib機制
但Spring的AOP有一定的缺點:
- 第一,只能對方法進行切入,不能對接口、字段、靜態代碼塊進行切入(切入接口的某個方法,則該接口下所有實現類的該方法都將被切入)
- 第二,同類中的互相調用方法將不會使用代理類。因為要使用代理類必須從Spring容器中獲取Bean
- 第三,性能不是最好的。從前面幾節得知,我們自定義的類加載器,性能優於動態代理和cglib
public IMsgFilterService getThis() { return (IMsgFilterService) AopContext.currentProxy(); } public boolean evaluateMsg () { // 執行此方法將織入切入邏輯 return getThis().evaluateMsg(String message); } @MethodInvokeTimesMonitor("KEY_FILTER_NUM") public boolean evaluateMsg(String message) {
public boolean evaluateMsg () { // 執行此方法將不會織入切入邏輯 return evaluateMsg(String message); } @MethodInvokeTimesMonitor("KEY_FILTER_NUM") public boolean evaluateMsg(String message) {
6、參考資料
http://www.iteye.com/topic/1116696