Spring Aspect Oriented Programming

    本文是一篇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方式咱們不作額外介紹。

    準備工做完成後,咱們即可以進行切面編程了:

1,實例

    在一個訂單系統,和支付系統中,咱們都須要嚴格的用戶身份認證以及日誌記錄功能,如用戶下訂單,瀏覽訂單,支付前,都須要判斷用戶是否登陸等,而在下訂單,支付完成功一般都須要進行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的方方面面。

2,切面(@Aspect)

    Spring AOP中的切面能夠當作一個常規的類,該類須要被@Aspect註解,其中能夠包含切片,通知等聲明,上面的例子中,咱們在切面中聲明瞭後置通知。你能夠能夠將切片聲明其中,具體怎樣作,還看我的習慣,以及系統組織架構,業務邏輯須要而定了。

3,切片(@Pointcut)

    切片的做用是用來決定咱們聲明的通知在何時被執行。一個切片的生命有兩部分組成: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)

    咱們不一一分析了,相信你能理解這些表達式的意思。

4,通知(@Advice)

    明白了切片,咱們再來了解下通知。一般,通知是須要跟切片結合在一塊兒使用,而且會在與切片匹配的方法的先後被執行。而此處的切片既能夠是對其它切片的簡單引用(經過切片簽名),亦能夠是一個切片表達式。

    上面已經說過,Spring中支持前置通知,環繞通知,AfterReturning通知,After通知,異常拋出通知,下面咱們逐個介紹。

    4,1 前置通知(@Before)

@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兩個方法會在匹配的鏈接點的執行以前被執行。

    4,2 AfterReturning通知

@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屬性來訪問鏈接點方法執行後返回的結果。

    4,3 AfterThrowing通知

@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 通知會在匹配的鏈接點方法中拋出異常後執行,而且你能夠像第二個實例中那樣來捕獲鏈接點中拋出的異常實例。

    4,4 After(finally)Advice

@Aspect
public class AfterFinallyExample {    
    @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doReleaseLock() {        
        // ...
    }
}

    After 通知不管鏈接點方法的執行結果如何都會獲得執行。

    4,5 Around Advice

    環繞通知顧名思義,會在鏈接點的先後都被執行,而且能夠決定鏈接點是否被執行,一般環繞通知會被用在鏈接點執行的先後須要共享數據的場景中。可是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;
    }
}

    4,6 爲通知傳遞參數

    前面提到在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中的經常使用概念也基本分析了,固然還有不少沒有提到,不過哪些已經超出本文的定位了,另外以上內容應該也能夠應對平常開發工做中的大部分需求了。

    歡迎討論,拍磚

相關文章
相關標籤/搜索