Spring 的 AOP 的支持

面向切面編程(Aspect-oriented Programming,AOP)經過提供另外一種思考程序結構的方法來補充面向對象編程(Object-oriented Programming,OOP)。OOP中模塊化的關鍵單元是類,而AOP中模塊化的單元是切面。切面支持跨多個類型和對象的關注點(例如事務管理)的模塊化java

1、AOP 的概念

AOP 術語

在使用 AOP 以前,先熟悉一下 AOP 概念和術語。這些術語並不特定於 Spring,而是與 AOP 有關的spring

描述
Aspect(切面) 跨越多個類的關注點的模塊化,切面是通知和切點的結合。通知和切點共同定義了切面的所有內容——它是什麼,在什麼時候和何處完成其功能。事務處理和日誌處理能夠理解爲切面
Join point(鏈接點) 程序執行過程當中的一個點,如方法的執行或異常的處理
Advice(通知) 切面在特定鏈接點上採起的動做
Pointcut(切點) 匹配鏈接點的斷言。通知與切入點表達式相關聯,並在切入點匹配的任何鏈接點上運行(例如,具備特定名稱的方法的執行)。切入點表達式匹配的鏈接點概念是AOP的核心,Spring默認使用AspectJ切入點表達式語言
Introduction(引用) 爲類型聲明其餘方法或字段。Spring AOP容許您向任何建議的對象引入新的接口(和相應的實現)。例如,您可使用介紹使bean實現IsModified接口,以簡化緩存
Target object(目標) 由一個或多個切面通知的對象。也稱爲「通知對象」。因爲Spring AOP是經過使用運行時代理實現的,因此這個對象始終是代理對象
AOP proxy(代理) AOP框架爲實現切面契約(通知方法執行等)而建立的對象。在Spring框架中,AOP代理是JDK動態代理或CGLIB代理
Weaving(織入) 織入是將通知添加對目標類具體鏈接點上的過程,能夠在編譯時(例如使用AspectJ編譯器)、加載時或運行時完成

Spring切面能夠應用5種類型的通知:express

  • 前置通知(Before):在目標方法被調用以前調用通知
  • 後置通知(After):在目標方法完成以後調用通知(不管是正常仍是異常退出)
  • 返回通知(After-returning):在目標方法成功執行以後調用通知
  • 異常通知(After-throwing):在目標方法拋出異常後調用通知
  • 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法調用以前和調用以後執行自定義的行爲

其中環繞通知包括前置、後置、返回、異常通知,這個在後面的例子體現編程

2、使用 XML 配置的AOP支持

xml配置中須要aop命名空間標記,須要先導入 spring-aop 模式緩存

<?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:aop="http://www.springframework.org/schema/aop" 
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- bean definitions here -->
</beans>

也是須要AspectJ的 aspectjweaver.jar 包,spring-aspects 模塊是依賴該jar包的
spring-aspects 模塊集成自 AspectJ 框架, 主要是爲 Spring AOP 提供多種 AOP 實現方法app

一、聲明一個切面

使用 <aop:aspect> 標籤聲明切面,使用 ref 屬性去引用支持的 bean框架

<aop:config>
   <aop:aspect id="myAspect" ref="aspectBean">
   ...
   </aop:aspect>
</aop:config>
<bean id="aspectBean" class="com.demo.aspect.AspectBean">
...
</bean>

切面能夠具備與任何其餘類相同的方法和字段。它們還能夠包含切入點、通知和引入(內部類型)聲明。模塊化

二、聲明切入點

聲明切入點是肯定通知感興趣的或是將要織入的鏈接點(即目標方法)測試

<aop:config>
  <aop:aspect id="myAspect" ref="aspectBean">
    <aop:pointcut id="businessService"
         expression="execution(* com.demo.service.*.*(..))"/>
    ...
  </aop:aspect>
</aop:config>

<bean id="aspectBean" class="com.demo.aspect.AspectBean">
</bean>

表達式 expression 是對鏈接點的篩選,上面的例子是 com.demo.service 包下全部類的全部方法
詳細的表達式語法見後面詳說 也還能夠經過名稱匹配切入點參數與建議方法參數ui

三、聲明通知

使用 <aop:{ADVICE-NAME}> 標籤聲明五個建議中的任意一個,以下

<aop:config>
  <aop:aspect id="myAspect" ref="aspectBean">
    <aop:pointcut id="businessService"
        expression="execution(* com.demo.service.*.*(..))" />
    
    <!-- 前置通知定義 -->
    <aop:before pointcut-ref="businessService" 
        method="doBeforeTask" />
    <!-- 後置通知定義 -->
    <aop:after pointcut-ref="businessService" 
        method="doAfterTask" />
    <!-- 返回通知定義 -->
    <!-- doReturnTask方法必需要有一個名字與「returning」值一致(如retVal)的參數,這個是目標方法的返回值 -->
    <aop:after-returning
        pointcut-ref="businessService" returning="retVal"
        method="doReturnTask" />
    <!-- 異常通知定義 -->
    <!-- doRequiredTask方法必需要有一個名字與「throwing」值一致(如ex)的參數,這個是目標方法的拋出的異常-->
    <aop:after-throwing pointcut-ref="businessService"
        throwing="ex" method="doThrowTask" />

    <!-- 環繞通知定義(環繞通知包含了其餘的通知) -->
    <!-- <aop:around pointcut-ref="businessService"
        method="doAroundTask" /> -->

  </aop:aspect>
</aop:config>

<bean id="aspectBean" class="com.demo.aspect.AspectBean">
</bean>

其中環繞通知包括前置、後置、返回、異常通知,在待會的例子中能夠看出
這裏pointcut-ref="businessService"是在<aop:config>裏定義的切入點。也能夠改成內聯切入點,使用pointcut屬性替換pointcut-ref屬性

<aop:aspect id="beforeExample" ref="aBean">
    <aop:before
        pointcut="execution(* com.demo.service.*.*(..))"
        method="doAccessCheck"/>
    ...
</aop:aspect>

例子
咱們使用上面的例子,定義的切入點爲 com.demo.service 下的全部的類的全部方法,該切面支持的bean爲 com.demo.aspect 下的 AspectBean。咱們的切入點也是須要在IoC容器中被管理的 bean,能夠用註解也能夠xml配置

<bean id="targetService" class="com.demo.service.TargetService"/>

TargetService,目標對象,這個operation方法即是咱們的切入點

public class TargetService {
    public String operation(String msg) {
        System.out.println("執行目標方法,方法參數[msg:" + msg + "]");
        // 測試異常通知
        // throw new RuntimeException("我是個異常");
        return msg;
    }
}

同時我也寫一下 AspectBean 裏面的方法

public class AspectBean {
    private static int step = 0;

    public void doBeforeTask(String msg) {
        System.out.println(++step + " 前置通知,參數爲[msg:" + msg + "]");
    }

    public void doAfterTask() {
        System.out.println(++step + " 後置通知");
    }

    public void doReturnTask(Object retVal) {
        System.out.println(++step + " 返回通知,返回值爲:" + retVal.toString());
    }

    public void doThrowTask(Exception ex) {
        System.out.println(++step + " 異常通知,異常信息爲:" + ex.getMessage());
    }

    /**
     * 環繞通知須要攜帶ProceedingJoinPoint類型的參數 
     * 環繞通知相似於動態代理的全過程ProceedingJoinPoint類型的參數能夠決定是否執行目標方法 
     * 且環繞通知必須有返回值,返回值即目標方法的返回值
     */
    public Object doAroundTask(ProceedingJoinPoint pjp) {
        String methodname = pjp.getSignature().getName();
        Object result = null;
        try {
            // 前置通知
            System.out.println("目標方法" + methodname + "開始,參數爲" + Arrays.asList(pjp.getArgs()));
            // 執行目標方法
            result = pjp.proceed();
            // 返回通知
            System.out.println("目標方法" + methodname + "執行成功,返回" + result);
        } catch (Throwable e) {
            // 異常通知
            System.out.println("目標方法" + methodname + "拋出異常: " + e.getMessage());
        }
        // 後置通知
        System.out.println("目標方法" + methodname + "結束");
        return result;
    }
}

咱們先註釋掉xml中環繞通知的配置,運行一下看下結果如何

public class MainApp {
    public static void main( String[] args ){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");
        TargetService targetService  = (TargetService) applicationContext.getBean("targetService");
        targetService.operation("zou");
    }
}

結果爲

1 前置通知
執行目標方法,方法參數[msg:zou]
2 後置通知
3 返回通知,返回值爲:zou

而後咱們註釋掉其餘其餘通知,測試一下環繞通知,而且咱們在目標方法中拋出一個異常,再運行一下,結果爲

目標方法operation開始,參數爲[zou]
執行目標方法,方法參數[msg:zou]
目標方法operation拋出異常: 我是個異常
目標方法operation結束

3、使用 @AspectJ 的AOP支持

一、啓用@AspectJ支持

要在Spring配置中使用@AspectJ切面,須要啓用Spring支持
使用XML配置啓用@AspectJ支持

<aop:aspectj-autoproxy/>

這裏也須要AspectJ的 aspectjweaver.jar 包,spring-aspects 模塊是依賴該jar包的

二、聲明一個切面

Aspects 類和其餘任何正常的 bean 同樣,除了它們將會用 @AspectJ 註釋以外,它和其餘類同樣可能有方法和字段

import org.aspectj.lang.annotation.Aspect;
@Aspect
public class AnnotationAspect {

}

也能夠在 xml 中配置,和其餘 bean 同樣

<bean id="myAspect" class="org.xyz.AnnotationAspect">
    <!-- configure properties of the aspect here -->
</bean>

切面(用@Aspect標註的類)能夠具備與任何其餘類相同的方法和字段。它們還能夠包含切入點、通知和引入(內部類型)聲明。

三、聲明切入點

切入點聲明有兩部分:包含名稱和任何參數的簽名,以及準確肯定咱們感興趣的方法執行的切入點表達式
在AOP的@AspectJ註釋樣式中,切入點簽名是由一個常規方法定義提供的,切入點表達式是經過使用@Pointcut註釋表示的(做爲切入點簽名的方法必須爲void返回類型)

以下代碼,定義了一個名爲anyOldTransfer的切入點,該切入點與任何名爲transfer的方法的執行匹配

@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature

表達式見

四、聲明通知

你可使用 @{ADVICE-NAME} 註釋聲明五個建議中的任意一個,以下所示。這假設你已經定義了一個切入點標籤方法 operation()

@Aspect
public class AnnotationAspect {
    private static int step = 0;

    @Pointcut("execution(* transfer(..))") // the pointcut expression
    private void operation() {}

    @Before("operation()")
    public void doBeforeTask() {
        System.out.println(++step + " 前置通知");
    }

    @After("operation()")
    public void doAfterTask() {
        System.out.println(++step + " 後置通知");
    }

    @AfterReturning(pointcut = "operation()", returning = "retVal")
    public void doAfterReturnningTask(Object retVal) {
        System.out.println(++step + " 返回通知,返回值爲:" + retVal.toString());
    }

    @AfterThrowing(pointcut = "operation()", throwing = "ex")
    public void doAfterThrowingTask(Exception ex) {
        System.out.println(++step + " 異常通知,異常信息爲:" + ex.getMessage());
    }

    /**
     * 環繞通知須要攜帶ProceedingJoinPoint類型的參數 
     * 環繞通知相似於動態代理的全過程ProceedingJoinPoint類型的參數能夠決定是否執行目標方法 
     * 且環繞通知必須有返回值,返回值即目標方法的返回值
     */
    //@Around("operation()")
    public Object doAroundTask(ProceedingJoinPoint pjp) {
        String methodname = pjp.getSignature().getName();
        Object result = null;
        try {
            // 前置通知
            System.out.println("目標方法" + methodname + "開始,參數爲" + Arrays.asList(pjp.getArgs()));
            // 執行目標方法
            result = pjp.proceed();
            // 返回通知
            System.out.println("目標方法" + methodname + "執行成功,返回" + result);
        } catch (Throwable e) {
            // 異常通知
            System.out.println("目標方法" + methodname + "拋出異常: " + e.getMessage());
        }
        // 後置通知
        System.out.println("目標方法" + methodname + "結束");
        return result;
    }
}

也能夠爲任意一個通知直接寫入內聯切入點

@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
    // ...
}

其中環繞通知包括前置、後置、返回、異常通知 運行的結果這裏不給了,能夠參考上面xml配置的例子

相關文章
相關標籤/搜索