在Java EE應用中,常常通過AOP來處理一些具有橫切性質的系統級服務,如交易管理、安全檢查、緩衝、對象池管理等。 1.AOP的基本概念
AOP從程式運行角度考慮程式的流程,提取業務處理過程的切面。AOP面向的是程式運行中各個步驟,希望以更好的方式來組合業務處理的步驟。
AOP架構並不與特定的代碼耦合,AOP架構能處理常式中特定切入點(Pointcut),而不與具體某個具體類別結合程度。
AOP架構具有如下兩個特徵:
☞ 各步驟之間良好隔離性
☞ 原始碼無關性
下面是關於面向切面編程的一些術語:
☞ 切面(AspectJ):商務程序啟動並執行某個特定步驟,也就是應用運行過程的關注點,關注點可能橫切多個對象,所以常常也稱為橫切關注點
☞ 連接點(Joinpoint):程式執行過程中明確的點,如方法的調用,或者異常的拋出。Spring AOP中,連接點總是方法的調用
☞ 增強處理(Advice):AOP架構在特定的切入點執行的增強處理。處理有“around”、“before”和“after”等類型
☞ 切入點(Pointcut):可以插入增強處理的連接點。簡而言之,當某個連接點滿足指定要求時,該連接點將被添加增強處理,該連接點也就變成了切入點。Spring預設使用AspectJ切入點文法
☞ 引入:將方法或欄位添加到被處理的類中。Spring允許引入新的介面到任何被處理的對象。例如,你可以使用一個引入,使任何對象實現IsModified介面,以此來簡化緩衝
☞ 目標對象:被AOP架構進行增強處理的對象,也被稱為被增強對象
☞ AOP代理:AOP架構建立的對象,簡單的說,代理就是對目標對象的加強。Spring中的AOP代理可以是JDK動態代理,也可以是CGLIB代理
☞ 織入(Weaving):將增強處理添加到目標對象中,並建立一個被增強對象(AOP代理)的過程就是織入。織入有兩種實現方式:編譯時間增強(例如AspectJ)和運行時增強(例如CGLIB)。Spring和其他純Java AOP架構一樣,在運行時完成織入。
由前面的介紹知道:AOP代理其實是由AOP架構動態產生的一個對象,該對象可作為目標對象使用。AOP代理包含了目標對象的全部方法,但AOP代理中的方法與目標對象的方法存在差異:AOP方法在特定切入點添加了增強處理,並回調了目標對象的方法 2.Spring的AOP支援
Spring中AOP代理由Spring的IOC容器負責產生、管理,其依賴關係也有IOC容器負責管理。因此,AOP代理可以直接使用容器中的其他Bean執行個體作為目標,這種關係可由IOC容器的依賴注入提供。Spring預設使用Java動態代理來建立AOP代理,這樣就可以為任何介面執行個體建立代理了。
Spring也可以使用CGLIB代理,在需要代理類而不是代理介面的時候,Spring自動會切換為使用CGLIB代理。
Spring目前僅支援將方法調用作為連接點(Joinpoint),如果需要把對Field的訪問和更新也作為增強處理的連接點,則可以考慮使用AspectJ。
縱觀AOP編程,其中需要程式員參與的只有三個部分:
☞ 定義普通業務組件
☞ 定義切入點,一個切入點可能橫切多個業務組件
☞ 定義增強處理,增強處理就是在AOP架構為普通業務組件織入的處理動作 3.基於Annotation的“零配置”方式
為了啟用Spring對@AspectJ切面配置的支援,並保證Spring容器中的目標Bean被一個或多個切面自動增強,必須在Spring設定檔中配置如下片段:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> <!-- 啟動AspectJ支援 --> <aop:aspectj-autoproxy/></beans>
如果不打算使用Spring的XML Schema配置方式,則應該在Spring設定檔中增加如下片段來啟用@AspectJ支援。
<!-- 啟動AspectJ支援 --><bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" />
為了在Spring應用中啟動@AspectJ支援,還需要在應用的類載入路徑中增加兩個AspectJ庫:aspectjweaver.jar和aspectjrt.jar ①定義切面
當啟動了@AspectJ支援後,只要我們在Spring容器中配置一個帶@Aspect注釋的Bean,Spring將會自動識別該Bean,並將該Bean作為切面處理。
使用@Aspect標註一個Java類,該Java類將會作為切面Bean,如下面的程式碼片段所示。
@Aspectpublic class LogAspect { // 定義該類的其他內容}
切面類(用@Aspect修飾的類)和其他類一樣可以有方法、屬性定義,還可能包含切入點、增強處理定義。
當我們使用@Aspect來修飾一個Java類之後,Spring將不會把該Bean當做組件Bean處理,因此負責自動增強後處理Bean將會略過該Bean,不會對該Bean進行任何增強處理。 ②定義Before曾強處理
當我們在一個切面類裡使用@Before來標註一個方法時,該方法將作為Before增強處理。使用@Before標註時,通常需要指定一個value屬性,該屬性值指定一個切入點運算式,用於指定該增強處理將被織入哪些切入點。例如:
@Component@Aspectpublic class BeforeAdviceTest { // 匹配org.crazyit.app.service.impl包下所有類的所有方法的執行作為切入點 @Before("execution(* org.crazyit.app.service.impl.*.*(..))") public void authority() { System.out.println("類比執行許可權檢查"); }}
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> <!-- 自動搜尋組件 --> <context:component-scan base-package="main.java.service.impl,main.java.aspect"/> <!-- 啟動AspectJ支援 --> <aop:aspectj-autoproxy /></beans>
註:需要在加入aopalliance.jar ③ 定義AfterReturning增強處理
類似於使用@Before註解可標註Before增強處理,使用@AfterReturning來標註一個AfterReturning增強處理,AfterReturning增強處理將在目標方法正常完成後被織入
使用@AfterReturning註解時可指定如下兩個常用屬性:
☞ pointcut/value:這兩個屬性的作用是一樣的,他們都用於指定該切入點對應的切入運算式。當指定了pointcut屬性值後,value屬性值將會被覆蓋
☞ returning:指定一個傳回值形參名,增強處理定義的方法可通過該形參名來訪問目標方法的傳回值
@Component@Aspectpublic class AfterReturningAdviceTest { @AfterReturning(returning = "rvt", pointcut = "execution(* main.java.service.*.*(..))") public void log(Object rvt) { System.out.println("擷取目標方法傳回值:" + rvt); System.out.println("類比記錄日誌功能..."); }}
正如上面的程式中看到的,程式中使用@AfterReturning時,指定了一個returning屬性,該屬性值為rvt,這表明允許在增強處理方法(log方法)中使用名為rvt的形參,該形參代表目標方法的傳回值。 ④ 定義AfterThrowing增強處理
使用@AfterThrowing註解可用於標註一個AfterThrowing增強處理,AfterThrowing增強處理主要用於處理常式中未處理的異常。
使用@AfterThrowing時,可指定如下兩個常用屬性:
☞ pointcut/value:這兩個屬性的作用是一樣的,他們都用於指定該切入點對應的切入運算式。當指定了pointcut屬性值後,value屬性值將會被覆蓋
☞ returning:指定一個傳回值形參名,增強處理定義的方法可通過該形參名來訪問目標方法中所拋出的異常對象
@Component@Aspectpublic class AfterThrowingAdviceTest { @AfterThrowing(throwing = "ex", pointcut = "execution(* main.java.service.*.*(..))") public void doRecoveryActions(Throwable ex) { System.out.println("目標方法中拋出的異常:" + ex); System.out.println("類比拋出異常後的增強處理..."); }}
正如上面的程式中看到的,程式中使用 @AfterThrowing時,指定了一個throwing屬性,該屬性這為ex,這允許在增強處理方法中使用名為ex的形參,該形參代表目標方法所拋出的異常
@Componentpublic class Chinese implements Person { @Override public String sayHello(String name) { try { System.out.println("sayHello方法開始被執行..."); new java.io.FileInputStream("a.txt"); } catch (Exception ex) { System.out.println("目標類的異常處理" + ex.getMessage()); } return name + " Hello, Spring AOP"; } public void eat(String food) { int a = 5 / 0; System.out.println("我正在吃:" + food); }}
上面的程式中的sayHello()和eat()兩個方法都會拋出異常,但sayHello()方法中異常將由該方法顯示捕捉,所以Spring AOP不會處理該異常;而eat()方法將拋出一個AirthmeticException異常,且該異常沒有被任何程式所處理,故Spring AOP會對該異常進行處理。
類比執行許可權檢查...sayHello方法開始被執行...目標類的異常處理a.txt (系統找不到指定的檔案。)擷取目標方法傳回值:qiuxiao Hello, Spring AOP類比記錄日誌功能...qiuxiao Hello, Spring AOP類比執行許可權檢查...目標方法中拋出的異常:java.lang.ArithmeticException: / by zero類比拋出異常後的增強處理...Exception in thread "main" java.lang.ArithmeticException: / by zero at main.java.service.impl.Chinese.eat(Chinese.java:23) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:309) at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:183) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150) at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:50) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:55) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) at org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor.invoke(AfterReturningAdviceInterceptor.java:50) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:89) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202) at com.sun.proxy.$Proxy8.eat(Unknown Source) at main.test.Test.main(Test.java:13)
注意:使用throwing屬性還有一個額外的作用:它可用於限定切入點只匹配指定類型的異常——加入在上面的doRecoveryActions()方法中定義了ex形參的類型是NullPointerException,則該切入點只匹配拋出NullPointerException異常的情況。上面doRecoveryActions()方法的ex形參類型是Throwable,這表明該切入點可匹配拋出任何異常的情況
AOP的AfterThrowing處理雖然可以對目標方法的異常進行處理,但是這種處理與直接使用catch捕捉不同:catch捕捉意味著完全處理該異常,如果catch塊中沒有重新拋出新異常,則該方法可以正常結束;而AfterThrowing處理雖然處理了該異常,但它不能完全處理該異常,該異常依然會傳播到上一級調用者 ⑤After增強處理
Spring還提供了一個After增強處理,它與AfterReturning增強處理有點相似,但也有區別:
☞ AfterReturning增強處理只有在目標方法成功完成後才會被織入
☞ After增強處理不管目標方法如何約束(包括成功完成和遇到異常中止兩種情況),它都會被織入
因為不論一個方法是如何結束的,After增強處理都會被織入,因此After增強處理必須準備處理正常返回和異常返回兩種情況,這種增強處理通常用於釋放資源。
使用@After時,需要指定一個value屬性,該屬性值用於指定該增強處理被織入的切入點
@Component@Aspectpublic class AfterAdviceTest { @After(value = "execution(* main.java.service.*.*(..))") public void release() { System.out.println("類比方法結束後的釋放資源..."); }}
⑥Around增強處理
@Around用於標註Around增強處理,Around曾強處理是功能比較強大的增強處理,它近似於Before和AfterReturning增強處理的總和,Around增強處理既可在執行目標方法前織入增強動作,也可在執行目標方法之後織入增強動作。
與Before、AfterReturning增強處理不同的是,Around增強處理甚至可以決定目標方法在什麼時候執行,如何執行,甚至可以完全阻止目標方法的執行。
Around增強處理可以改變執行目標方法的參數值,也可改變執行目標方法之後的傳回值。
Around增強處理的功能雖然強大,但通常需要線上程安全的環境下使用。如果需要目標方法執行之前和之後共用某種狀態資料,則應該考慮使用Around增強處理;尤其是需要使用增強處理阻止目標的執行,或需要改變目標方法傳回值時,則只能使用Around增強處理了。
使用@Around時需要指定一個value屬性,該屬性指定增強處理被織入的切入點。
當定義一個Around增強處理方法時,該方法的第一個參數必須是ProceedingJoinPoint類型(至少包含一個形參),在增強處理方法體內,調用ProceedingJoinPoint的proceed()方法才會執行目標方法——這就是Around增強處理可以完全控制目標方法執行時機、如何執行的關鍵;如果程式沒有調用ProceedingJoinPoint的proceed()方法,則目標方法不會被執行。
調用ProceedingJoinPoint的proceed()方法時,還可以傳入一個Object[]對象,該數組中的值將被傳入目標方法作為執行方法的實參。
@Component@Aspectpublic class AroundAdviceTest { @Around(value = "execution(* main.java.service.*.*(..))") public Object processTx(ProceedingJoinPoint jp) throws Throwable { System.out.println("執行目標方法之前,類比開始事物..."); // 執行目標方法,並儲存目標方法執行後的回值 Object rvt = jp.proceed(new String[] { "被改變的參數" }); System.out.println("執行目標方法之後,類比結束事物..."); return rvt + "新增的內容"; }}
執行目標方法之前,類比開始事物...類比執行許可權檢查...sayHello方法開始被執行...目標類的異常處理a.txt (系統找不到指定的檔案。)執行目標方法之後,類比結束事物...擷取目標方法傳回值:被改變的參數 Hello, Spring AOP新增的內容類比記錄日誌功能...類比方法結束後的釋放資源...被改變的參數 Hello, Spring AOP新增的內容執行目標方法之前,類比開始事物...類比執行許可權檢查...我正在吃:被改變的參數目標方法中拋出的異常:java.lang.ArithmeticException: / by zero類比拋出異常後的增強處理...類比方法結束後的釋放資源...Exception in thread "main" java.lang.ArithmeticException: / by zero at main.java.service.impl.Chinese.eat(Chinese.java:24) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:309) at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:183) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150) at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:50) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:91) at main.java.aspect.AroundAdviceTest.processTx(AroundAdviceTest.java:16) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:621) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:610) at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:65) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:55) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) at org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor.invoke(AfterReturningAdviceInterceptor.java:50) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) at org.springframework.aop.aspectj.AspectJAfterAdvice.invoke(AspectJAfterAdvice.java:42) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:89) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202) at com.sun.proxy.$Proxy10.eat(Unknown Source) at main.test.Test.main(Test.java:13)
當調用ProceedingJoinPoint的proceed()方法時,傳入的Object[]參數值將作為目標方法的參數,如果傳入的Object[]數組長度與目標方法所需要參數的個數不相等,或者Object[]數組元素與目標方法所需參數的類型不符,程式就會出現異常。
為了能擷取目標方法的參數的個數和類型,需要增強處理方法能訪問執行目標方法的參數了。