本文是一篇Spring AOP的基礎知識分析文章,其中不牽扯源碼分析,只包含AOP中重要概念的講解,分析,以及Spring AOP的用法。java
Spring 從2.0版本引入了更加簡單卻強大的基於xml和AspectJ註解的面向切面的編程方式。在深刻了解如何用Spring 進行面向切面的編程前,咱們先了解AOP中的幾個重要的基本概念,這幾個概念並不是Spring特有的,而且從字面上看有些難於理解,不過我會盡可能用實例和通俗的語言來進行闡述。程序員
首先,到底什麼是AOP呢,它有什麼用處呢,對咱們程序員有什麼好處呢,相信這是全部第一次接觸AOP的開發者最想知道的幾個問題。咱們不妨用一些例子(事務處理或者權限認證等)來稍做解釋,一般,在一個應用中咱們會爲不一樣的業務模塊建立不一樣的服務類,而大多時候每一個服務類中都包含save/remove等業務邏輯不一樣但應用邏輯相同的接口(此處業務邏輯表示不一樣的業務需求,而應用邏輯表示增刪改查等應用中常見的邏輯)。然而,大多數狀況下咱們須要對這些方法進行事務控制,好比在全部save/remove 方法執行前開啓一個事務,而後方法執行完成後提交事務。這時,若是沒有AOP的支持,咱們可能就要對於每個方法都要寫一大串重複的毫無興奮點的代碼(固然咱們這裏暫不提動態代理,其實Spring AOP默認使用動態代理實現)。然而利用AOP咱們就能夠避免這樣的麻煩了,那我該怎樣作呢?
web
第一步,咱們須要定義咱們的事務類,該類中包含事務的啓用,提交等方法,這個類咱們稱它爲切面(Aspect)。
spring
第二步,切面定義好了,咱們還須要定義其中事務行爲,也就是咱們須要爲當前方法添加的額外行爲,咱們稱之爲通知(或者加強)(Advice)。
編程
第三步,咱們須要經過某種定義來決定哪些方法將會被通知(即須要事物處理),咱們將這個定義秤爲切入點(Pointcut),而每個被處理的方法咱們稱之爲鏈接點(Join Point)。
數組
經過以上幾步,咱們即可以大體瞭解了這幾個基本概念,切面,通知,切入點,鏈接點。若是還不明白,你能夠這樣理解,首先咱們須要肯定哪些(切入點)方法須要被處理,而後就是對這些方法(鏈接點)執行哪些額外的代碼(通知),以及這些代碼在什麼時機執行(前置通知。。。), 最後封裝通知,切入點的類或接口就是咱們的切面)。 另外,對於通知,咱們一般分爲前置通知,後置通知,環繞通知等等,用於肯定通知在鏈接點執行的何種時機被調用,具體咱們下面分析。架構
這裏要說明下,因爲Spring AOP是使用JDK動態代理和CGLIB代理實現的,所以Spring AOP只能夠對方法的執行進行攔截,若是須要攔截字段的訪問或更新,則須要像AspectJ這樣的AOP語言。另外Spring能夠無縫的集成IOC,Spring AOP 以及AspectJ AOP。
app
上面已經提到Spring AOP提供了基於AspectJ註解和XML兩種編程方式,可是這篇文章咱們只分析如何給予註解進行編程。
ide
@AspectJ 註解方式,指的是一種使用Java 註解的方式來進行AOP編程的方式,而這些註解都是AspectJ 項目中引入的,而Spring 可使用AspectJ庫解釋這些註解以完成切入點(@Pointcut)的解析和匹配,而且這一切都不須要依賴於AspectJ的編譯器和切面編織器。源碼分析
爲了使用@AspectJ註解,咱們須要導入aspectjweaver.jar包,而且啓用beans的自動代理,不管這些beans是否被切面攔截,換句話說,自動代理會檢測被切面攔截的beans,而後爲這些beans自動生成代理以完成對相應方法的加強。咱們可使用XML或者Java代碼的方式啓用自動代理配置。
<aop:aspectj-autoproxy/>
Java方式咱們不作額外介紹。
準備工做完成後,咱們即可以進行切面編程了:
在一個訂單系統,和支付系統中,咱們都須要嚴格的用戶身份認證以及日誌記錄功能,如用戶下訂單,瀏覽訂單,支付前,都須要判斷用戶是否登陸等,而在下訂單,支付完成功一般都須要進行log,或發信通知,而這些功能相對重複,咱們能夠將它看作一個切面用在任何須要的地方,而不須要repeat yourself。既然需求定下了咱們開始編碼。
下面是咱們的applicationContext.xml的內容,咱們使用註解的方式配置beans,並啓用Spring包自動掃描,若是對這個配置,能夠閱讀個人其餘關於spring的文章:
<context:component-scan base-package="aop"/>//包名就aop吧省事 <context:annotation-config/> <aop:aspectj-autoproxy/>
下面是咱們的OrderService 和PaymentService接口與實現:
package aop; //interfaces public interface OrderService { public void save(); public void read(); } public interface PayService { public void save(); } //implementations import org.springframework.stereotype.Component; @Component("orderService") public class OrderServiceImpl implements OrderService { @Override public void save() { System.out.println("order saved."); } @Override public void read() { System.out.println("order read."); } } @Component("payService") public class PayServiceImpl implements PayService { @Override public void save() { System.out.println("pay saved."); } }
服務接口定義完了,下面咱們須要定義咱們的切面了,下面是攔截規則,也就是切點Pointcut:
import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Component public class Pointcuts { @Pointcut("execution(* aop.*.save())") public void notice(){} @Pointcut("execution(* aop.*.*())") public void securityCheck(){} }
以上定義了兩個切點aop.Pointcuts.notice()和aop.Pointcuts.securityCheck(),分別指定了哪些方法須要notice攔截和securitycheck攔截。接下來就是定義對被攔截的方法執行哪些額外操做了,也就是咱們的通知。
import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect @Component public class NoticeAspect { @AfterReturning(pointcut="aop.Pointcuts.notice()") public void notice(){ System.out.println("Users have been noticed."); } }
這個類有一個@Aspect 註解,代表該類是一個切面,其中定義的方法有一個@AfterReturning方法,該註解代表相應方法爲一個後置通知,它有一個pointcut屬性來引用上面定義的切面,肯定攔截哪些方法。好了,這樣一個再簡單不過的切面編程就完成了,咱們看下啓動方法:
import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Main { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("/application.xml"); OrderService orderService = (OrderService)context.getBean("orderService"); orderService.save(); //orderService.read(); PayService payService = (PayService)context.getBean("payService"); payService.save(); } }
最終的運行結果是:
order saved. Users have been noticed. pay saved. Users have been noticed.
上面的代碼能夠說是最簡單的AOP了吧,下面咱們就深刻的分析一下,AOP的方方面面。
Spring AOP中的切面能夠當作一個常規的類,該類須要被@Aspect註解,其中能夠包含切片,通知等聲明,上面的例子中,咱們在切面中聲明瞭後置通知。你能夠能夠將切片聲明其中,具體怎樣作,還看我的習慣,以及系統組織架構,業務邏輯須要而定了。
切片的做用是用來決定咱們聲明的通知在何時被執行。一個切片的生命有兩部分組成:1)由名稱和任意參數組成的切片簽名,2)切片表達式。在Spring AOP 中的註解方式中,切片的簽名就是該常規方法定義的簽名,如上例的
@Pointcut("execution(* aop.*.save())")//切片表達式 public void notice(){}//切片簽名
注:做爲切片簽名的方法必須是void返回值。
Spring AOP目前僅支持部分AspectJ中定義的切片標識符, 下面即是完整的支持列表:
execution - 切片定義的主要用法,用於匹配方法鏈接點的執行。 within - 將鏈接點的匹配限定在某個特定類型中 this - 將AOP代理的類型限定在某個特定的類型中 target - 將目標對象的類型限定在某個特定的類型中 args - 限定鏈接點的參數爲某些特定類型 @target - 限定目標對象爲具備某個註解的特定類型 @args - 限定鏈接點的參數類型具備特定的註解 @within - 將鏈接點的匹配限定在某個具備特定註解的類型中 @annotation - 限定執行該鏈接點的對象爲某個特定的類型
如下則是Spring AOP還未實現的標識符call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this
, and @withincode
若是無心使用了這些還沒支持的切片,則Spring會拋出IllegalArgumentException。
前面曾提到Spring AOP是基於代理的實現方式,所以咱們會用target表示被代理對象(也就是上例的OrderServiceImpl),用this表示代理對象(也就是上例的Spring 爲攔截被代理對象所自動建立的對象,詳細分析可閱讀關於Java動態代理的文章)
另外,Spring 還支持bean切片,用於將鏈接點的匹配限定在指定的bean內。用法以下:
bean(idOrNameOfBean)
其中idOrNameOfBean能夠是任意的Spring Bean的名字或者ID,而且支持*通配符,所以若是你的項目遵循好的命名規範,你能夠很容易的寫出強大的bean切片。
再編寫切片時,咱們一般會用到一下幾個小技巧:
A,切片表達式支持%%, || 以及!。
B,切片表達式支持對切片簽名的引用。
C,咱們能夠將經常使用的切片進行統一管理(參考上例)
public class Pointcuts { @Pointcut("execution(public * *(..))")//公共方法切片 public void publicOperation(){} @Pointcut("within(aop.service..*)") public void notice(){} @Pointcut("publicOperation() && notice()") public void noticePublic(){} }
經過以上技巧,咱們能夠組合各類各樣複雜的切片。另外須要注意,在引用切片簽名時,須要遵循常規的Java方法的可見性約束,如同一個類型中能夠訪問private 切片定義。如下是一個Spring 提供的可能的企業開發中的切片組織方案:
package com.xyz.someapp; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; @Aspect public class SystemArchitecture { //用來匹配web層的切片,匹配位於web及其子包中定義的類中的方法 @Pointcut("within(com.xyz.someapp.web..*)") public void inWebLayer() {} //同上,用於匹配service層的切片 @Pointcut("within(com.xyz.someapp.service..*)") public void inServiceLayer() {} //用來匹配dao層 @Pointcut("within(com.xyz.someapp.dao..*)") public void inDataAccessLayer() {} //如下切片表達式,假設咱們的包結構爲 //com.aop.app.order.service... //com.aop.app.pay.service... //該切片會匹配這些service包下的全部類的全部方法的執行 @Pointcut("execution(* com.xyz.someapp..service.*.*(..))") public void businessService() {} //匹配dao包下的全部方法的執行 @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))") public void dataAccessOperation() {} }
這樣你就能夠在任意地方來引用這些切片定義了。
Execution 表達式
execution是最經常使用的一種切片標識符,咱們有必要分析下該切片表達式的格式:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
有上述表達式能夠看出,除了返回值類型(ret-type-pattern),名字(name-pattern),參數(param-pattern),其餘部分都是可選的。其中ret-type-pattern決定了鏈接點的返回值,大多數狀況下咱們用*通配符類匹配全部的返回值。name-pattern用來匹配方法名稱,咱們一樣能夠用*來做爲方法名的所有或部分。
對於param-pattern來講:()匹配沒有參數的方法,(..)匹配任意數量參數的方法,(*)匹配有一個任意參數的方法,(*, String)匹配兩個參數的方法,第一個參數爲任意類型,第二個參數爲String類型。如下是一些經常使用的切片表達式:
execution(public * *(..)) execution(* set*(..)) execution(* com.xyz.service.AccountService.*(..)) execution(* com.xyz.service.*.*(..)) execution(* com.xyz.service..*.*(..)) within(com.xyz.service.*) within(com.xyz.service..*) this(com.xyz.service.AccountService) target(com.xyz.service.AccountService) args(java.io.Serializable) @target(org.springframework.transaction.annotation.Transactional) @within(org.springframework.transaction.annotation.Transactional) bean(*Service)
咱們不一一分析了,相信你能理解這些表達式的意思。
明白了切片,咱們再來了解下通知。一般,通知是須要跟切片結合在一塊兒使用,而且會在與切片匹配的方法的先後被執行。而此處的切片既能夠是對其它切片的簡單引用(經過切片簽名),亦能夠是一個切片表達式。
上面已經說過,Spring中支持前置通知,環繞通知,AfterReturning通知,After通知,異常拋出通知,下面咱們逐個介紹。
@Aspect public class BeforeAdvice{ @Before("aop.Pointcuts.notice()")//reference to another pointcut definition. public void before(){ //... } @Before("execution(* aop.OrderService.*())") public void before1(){ //... } }
上面的兩種前置通知定義方式都是可行的,before與before1兩個方法會在匹配的鏈接點的執行以前被執行。
@Aspect public class AfterReturningExample { @AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()") public void doAccessCheck() { // ... } @AfterReturning( pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()", returning="retVal") public void doAccessCheck(Object retVal) { // ... } }
上面是AfterReturning通知的兩個實例,其中第二個實例中,咱們能夠經過@AfterReturning註解中的returning屬性來訪問鏈接點方法執行後返回的結果。
@Aspect public class AfterThrowingExample { @AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()") public void doRecoveryActions() { // ... } @AfterThrowing( pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()", throwing="ex") public void doRecoveryActions(DataAccessException ex) { // ... } }
AfterThrowing 通知會在匹配的鏈接點方法中拋出異常後執行,而且你能夠像第二個實例中那樣來捕獲鏈接點中拋出的異常實例。
@Aspect public class AfterFinallyExample { @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()") public void doReleaseLock() { // ... } }
After 通知不管鏈接點方法的執行結果如何都會獲得執行。
環繞通知顧名思義,會在鏈接點的先後都被執行,而且能夠決定鏈接點是否被執行,一般環繞通知會被用在鏈接點執行的先後須要共享數據的場景中。可是Spring建議咱們不要一味的使用Around 通知,而是使用能知足你需求的最簡單的通知類型。好比Before ,AfterReturning等。
Around 通知經過@Advice註解聲明,而且通知方法的第一個參數必須是ProceedingJoinPoint。而後在方法體內調用該實例的proceed()來調用鏈接點方法,proceed()也可接受Object[]參數,其中每一個元素都做爲鏈接點方法的參數。
@Aspect public class AroundExample { @Around("com.xyz.myapp.SystemArchitecture.businessService()") public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // start stopwatch Object retVal = pjp.proceed(); // stop stopwatch return retVal; } }
前面提到在AfterReturning通知和AfterThrowing通知中均可以經過參數來訪問到返回值或異常實例等,然而有的時候咱們可能須要在通知方法中訪問鏈接點方法中的變量,好比咱們須要攔截一個含有user參數的方法,而且但願在通知也操做該user實例,那麼咱們能夠這樣作:
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(user,..)") public void validate(User user) { // ...} //或者 @Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(user,..)") private void accountDataAccessOperation(User user) {} @Before("accountDataAccessOperation(user)") public void validate(User user) { // ...}
args(user, ...)是切片表達式的一部分,它定義了鏈接點至少應該有一個參數,而且應該是User類型的,另外它可使得該user實例做爲通知方法的參數來使用。
文章到這也就基本結束了,Spring AOP中的經常使用概念也基本分析了,固然還有不少沒有提到,不過哪些已經超出本文的定位了,另外以上內容應該也能夠應對平常開發工做中的大部分需求了。
歡迎討論,拍磚