一 基本理解
AOP,面向切面編程,作為Spring的核心思想之一,度娘上有太多的教程啊、解釋啊,但博主還是要自己按照自己的思路和理解再來闡釋一下。原因很簡單,別人的思想終究是別人的,自己的理解才是自己的,尤其當用文字、代碼來闡述一遍過后,理解層面上又似乎變得不一樣了。
博主就不概念化解釋AOP了,這里只簡單說下為啥要使用這樣一種編程思想和相關的AOP技術。其實很簡單,就是為了業務模塊間的解耦,尤其在現代的軟件設計中強調高內聚、低耦合,要求我們的業務模塊化,各個功能模塊只關注自己的邏輯實現,而不用關注與主業務邏輯不相關的功能。然而,在面向對象的系統設計中,系統中不可或缺的一些功能如日志、事務是散布在應用各處與主邏輯代碼高度耦合的,這讓主業務代碼變得相當冗余、難以復用。而在面向切面的編程思想中,我們是考慮將那些散布在應用多處的重復性代碼抽離出來封裝成模塊化的功能類,一來讓主業務邏輯更加專注、簡單,二來模塊化的日志、事務也便於復用和移植,這就是解耦的思想。但是,解耦並不等於斷耦,抽離的功能最終還是要以某種方式"還"(qie)回去,否則應用的功能就不完善了。這里,"還"(qie)回去的技術就是AOP技術,而這種解耦的編程思想就是AOP的編程思想。在Java的生態中,提供AOP技術的框架也有不少,主要的運用就是Spring的AOP和Spring"借鑒"並包含進了自己的生態體系的 AspectJ的AOP。
二 核心概念
為便於理解闡述,博主先嘮叨幾句。上面的基本闡述中,我們知道,AOP要干的事情其實也很簡單,就是要將對象編程中,抽離出來的模塊代碼(權限、日志、事務)還(qie)回去,但肯定不能是對象思維中的代碼冗雜的組合,而是應該更加高明一些,最好能在原來的業務代碼執行的過程中不知不覺的還(qie)回去——也就是說要在主業務邏輯執行的流程里,動態的添加(權限、日志、事務)代碼抽離前干的那些事情。怎么能做到呢?用代理啊,親!想想,我們對一個目標對象采用代理不就是為了在目標對象邏輯執行時候通過在代理對象中干點額外的事情嗎?這樣,雖然,原目標對象並沒有增加任何額外的功能,通過代理的一番暗中騷操作,展示給調用者的就好像目標對象有了代理對象中的那些額外的功能一樣。於是你也很好理解,為什么Spring的AOP中要用到動態代理了。好了,經過一番嘮叨,我們再來看AOP的相關術語就要好理解得多——
1、橫切關注點
如上描述,我們把日志、事務、權限等代碼重復性極高卻散布在應用程序各個地方的功能稱為橫切關注點。
2、連接點(Join Point)
被代理的目標對象在業務邏輯執行的過程中,可以被代理對象動態切入代理功能的一些時機節點,比如方法執行前、后,異常時,成功返回時等等。當然,這只是針對Spring來說的,因為Spring基於動態代理,只支持方法級別的AOP切入,實際上,AspectJ、JBoss等框架的AOP還能提供構造器以及更細粒度字段等的連接點支持。
3、通知(Advice)
如上描述,就是代理對象在什么時機要為目標對象額外增加的功能代碼,因而很多教程資料上稱之為 增強。請注意博主對通知的描述里有提到什么時機,這很好理解,你的代理對象要給目標對象增加額外功能,總得清楚要增加在哪些時機吧,所以,我們的通知按照功能切入的時機分為以下5個類型:
前置通知(Before):被代理對象目標方法被調用之前執行通知代碼;
后置通知(After):被代理對象目標方法執行完成之后執行通知代碼,不管方法是否成功執行(這相當於異常捕獲中的finally塊,總是會執行的意思,所以博主覺得如果將其命名為最終通知要更好理解些);
異常通知(After-throwing):被代理對象目標方法拋出異常后執行通知代碼;
返回通知(After-returning):被代理對象目標方法成功執行后執行通知代碼;
環繞通知(Around) :包裹被代理對象的目標方法,相當於結合了以上的所有通知類型。
4、切點(Pointcut)
被代理對象目標方法執行過程中真正的要執行通知代碼的一個或多個連接點,這會通過切點表達式語言進行匹配。
6、切面(Aspect)
通知和切點的結合,切面完整的包含了代理對象對目標對象進行通知的三個基本要素:何時(前、后、異常、環繞、返回等),何地(切點),干什么(通知切入的功能)。
7、織入(Weaving)
將切面應用到被代理對象並創建代理對象的的過程。切面會在指定的連接點(切點)被織入到被代理對象的執行方法中。其實,被代理對象的生命周期中有多個時機(編譯、類加載、運行)都可以進行織入,就 Spring 而言,是在被代理對象運行期進行代理對象的創建,織入切面邏輯的。
注:以上描述都是基於Spring 方法級別的AOP 來進行闡述
三 基礎代碼示例
說了那么多,還是上代碼最簡單直接。准備工作:
① 測試依賴的包及其版本(注:很多教程中都提到需要 aopalliance包,但是博主測試過程中並沒有確認此包存在的必要性)
aspectjweaver-1.9.2.jar
commons-logging-1.2.jar
spring-aop-4.3.18.RELEASE.jar
spring-beans-4.3.18.RELEASE.jar
spring-context-4.3.18.RELEASE.jar
spring-core-4.3.18.RELEASE.jar
spring-expression-4.3.18.RELEASE.jar
spring-test-4.3.18.RELEASE.jar
② 定義兩個基礎模型類(如下),業務是:給只有打電話功能的手機動態的添加 拍照、玩游戲這樣的非主業務功能。
//主業務功能 public class HuaWeiPhone { public void ring() { System.out.println("華為手機,產銷第一"); } } //額外添加的功能 public class Photograph { public void takePictures(){ System.out.println("華為手機,拍照牛批"); } public void playGames(){ System.out.println("華為手機,游戲玩得也這么暢快"); } }
1、XML配置的方式
根據以上Java代碼,進行非常簡單的配置,就能看到動態的為手機增加了拍照功能的效果了——
<bean class="main.java.model.HuaWeiPhone"/> <bean id="photograph" class="main.model.Photograph"/> <aop:config> <aop:pointcut id="ring" expression="execution(* main.model.HuaWeiPhone.ring(..))"/> <aop:aspect ref="photograph"> <aop:before method="takePictures" pointcut-ref="ring"/> <aop:after method="playGames" pointcut-ref="ring"/> </aop:aspect> </aop:config>
在Spring環境下測試類XML配置——
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:main/resource/applicationContext.xml") public class SpringTest { @Autowired HuaWeiPhone huaWeiPhone; @Test public void testXml(){ huaWeiPhone.ring(); } }
輸出結果
2、Java注解的方式
需要先說明的是,Spring的基於注解的 AOP 實際上是借鑒吸收了AspectJ的功能,所以你會看到很多類似 AspectJ 框架的注解。在之前的模型類上通過添加相應的注解改造成一個切面——
@Aspect //將該類標注為一個AOP切面 @Component public class Photograph { @Pointcut("execution(* main.model.HuaWeiPhone.ring(..))") public void chenbenbuyi (){} @Before("chenbenbuyi()") public void takePictures(){ System.out.println("華為手機,拍照牛批"); } @After("chenbenbuyi()") public void playGames(){ System.out.println("華為手機,游戲玩得也這么暢快"); } }
同樣的,目標類(HuaWeiPhone)上也要添加@Componet注解將其交給Spring 容器管理。然后,如果是純注解的話,還要一個配置類——
//配置注解掃描 @ComponentScan(basePackages = "main") //啟用AspectJ的自動代理功能 @EnableAspectJAutoProxy public class JavaConfig { }
最后,在Spring的環境下測試——
@RunWith(SpringJUnit4ClassRunner.class) //@ContextConfiguration(locations = "classpath:main/resource/applicationContext.xml") @ContextConfiguration(classes = JavaConfig.class) public class SpringTest { @Autowired HuaWeiPhone huaWeiPhone; @Test public void testAnno(){ huaWeiPhone.ring(); } }
結果同上,這里就不展示了。不過需要注意的是,不管什么配置方式,基於Spring 的AOP編程實現的前提都是要將通知對象和被通知方法交給Spring IOC容器管理,也就是要聲明為Spring 容器中的Bean。
四 需求升級
在第三部分中,博主只是展示了最最簡單的AOP功能實現,還有稍微復雜的技能點沒有列出。比如,5種通知類型中的環繞通知呢?再比如,我的切面代碼如果要傳參數怎么辦呢?接下來博主依次講解。
① 關於環繞通知的運用
基於 二 中的闡述,5 種通知類型中 環繞通知 是功能最為強大,實際上,我們可以在環繞通知中個性化的定制出前置 、后置、異常和返回的通知類型,而如果單獨的采用前置、后置等通知類型,如果業務涉及多線程對成員變量的修改,可能出現並發問題,所以環繞要比單獨的使用另外的幾種通知類型更加的安全。我們對上面的切面基於環繞通知進行修改,使之包含所有的通知類型的功能——
@Aspect @Component public class Photograph { @Pointcut("execution(* main.model.HuaWeiPhone.ring(..))") public void chenbenbuyi (){} @Around("chenbenbuyi()") public void surround(ProceedingJoinPoint joinPoint){ try { System.out.println("目標方法執行前執行,我就是前置通知"); joinPoint.proceed();// ① // int i =1/0; // ② 制造異常 System.out.println("正常返回,我就是返回通知"); } catch (Throwable e) { System.out.println("出異常了,我就是異常通知"); }finally { System.out.println("后置通知,我就是最終要執行的通知"); } } }
XML的配置和上面的其它通知類型一樣,只不過元素標簽為 <aop:around />而已。上面的打印語句的位置就對應了其它幾種通知類型執行切面邏輯的時機。這里注意,環繞通知方法體中需要有 ProceedingJoinPoint 接口作為參數,在環繞通知中,通過執行該參數的 proceed() 方法來調用通知需要切入的目標方法。如果不執行 ① 處的調用,被通知方法實際上會被阻塞掉,所以你會看到,明明測試中執行了被通知的方法,實際卻沒有執行。該參數對象還可以獲取方法簽名、代理對象、目標對象等信息,可以自己測試着玩。
② 關於通知的傳參問題
切面雖然是通用邏輯,但實際在切入不同的目標方的時候,可能還是希望通知方法根據被通知方法的不同(比如參數不同)而執行不一樣的邏輯,這就要求我們的通知也能獲取到被通知方法傳入的參數。通過切點表達式,這也很容易辦到。首先我們修改被通知的方法可以傳參:
public void ring(String str) { System.out.println("華為手機,產銷第一"); int i =1/0; }
然后切面中切點表達式和切面方法也做對應的修改——
@Aspect @Component public class Photograph { /** * Spring 借助於 AspectJ的切點表達式語言中的arg()表達式執行參數的傳遞工作 */ @Pointcut("execution(* main.model.HuaWeiPhone.ring(String))&&args(name)") public void chenbenbuyi (String name){} /** * ① 在引用空標方法的切點表達式時同時也就要傳入相應的參數 * ② 傳入的參數形參名字必須和切點表達式中的相同 */ @Before("chenbenbuyi(name)") public void takePictures(String name){ System.out.println("喂喂,你好我是 "+ name); } /** * 對於異常通知,有專門的異常參數可以直接獲取到被通知方法出現異常后信息的 */ @AfterThrowing(pointcut = "chenbenbuyi(name)",throwing = "e") public void excep(String name,Throwable e){ System.out.println("出異常了,異常信息是:"+e.getMessage()); } }
XML中配置參數傳遞
<bean class="main.java.model.HuaWeiPhone"/> <bean id="photograph" class="main.java.model.Photograph"/> <aop:config> <aop:pointcut id="ring" expression="execution(* main.java.model.HuaWeiPhone.ring(..)) and args(name)"/> <aop:aspect ref="photograph"> <aop:before method="takePictures" pointcut-ref="ring" arg-names="name" /> <aop:after-throwing method="excep" throwing="e" arg-names="name,e" pointcut-ref="ring"/> </aop:aspect> </aop:config>
測試代碼——
@RunWith(SpringJUnit4ClassRunner.class) //@ContextConfiguration(locations = "classpath:main/resource/applicationContext.xml") @ContextConfiguration(classes = JavaConfig.class) public class SpringTest { @Autowired HuaWeiPhone huaWeiPhone; @Test public void testAnno(){ huaWeiPhone.ring("博客園 陳本布衣"); } }
最終測試的執行結果——
注意點:
① XML配置中由於 &符號有特殊含義,所以 切點表達式中 連接形參名的時候就不能再使用注解中的 && ,而應該使用 and 代替,同樣的如果有 或(|| )非 (!)操作,分別使用 or 和 not 代替。
② 注解和XML配置中切點表達式描述形參類型的地方博主采用了不同的方式,因為 .. 就表示任意類型,可以不用指明。
五 切點表達式常用圖解