1. 程式人生 > >Spring——面向切面程式設計(AOP)詳解

Spring——面向切面程式設計(AOP)詳解

宣告:本部落格僅僅是一個初學者的學習記錄、心得總結,其中肯定有許多錯誤,不具有參考價值,歡迎大佬指正,謝謝!想和我交流、一起學習、一起進步的朋友可以加我微信Liu__66666666

這是簡單學習一遍之後的記錄,後期還會修改。

一、問題引入

​ 在日常寫專案的時候,肯定少不了要列印日誌。例如,要向資料庫中insert一個使用者,我想在插入前輸出一下相關資訊,怎麼實現呢?最基本的做法是:在insert方法中寫日誌輸出語句。這樣寫完全能實現功能,但是會不會顯得很冗餘?耦合度是不是很高?程式設計的準則是“高內聚,低耦合”,低耦合的意思就是類與類之間的依賴關係儘量少、關聯程度儘量小。

​ 而如果在上述情景中使用面向切面程式設計(AOP),就可以不在insert方法中寫日誌輸出語句卻能實現日誌輸出功能。當然,AOP不止如此。

二、概念引入

1.AOP

在軟體業,AOP為Aspect Oriented Programming的縮寫,意為:面向切面程式設計,通過預編譯方 式和執行期動態代理實現程式功能的統一維護的一種技術。AOP是OOP的延續,是軟體開發中的一個 熱點,也是Spring框架中的一個重要內容,是函數語言程式設計的一種衍生範型。利用AOP可以對業務邏輯 的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高 了開發的效率。

2.幾個基本概念

- 切入點:所有要操作的方法定義,要求業務層方法風格統一
- 分離點:將不可分割的元件單獨提取出去定義為單獨的操作功能
- 橫切關注點:將所有與開發無關的程式組成類單獨提取後組織執行
- 織入:將所有切入點、關注點的程式碼組成在一張完整的程式結構中

3.通知(Advice)

​ AOP是通過通知來實現功能的,有如下五種:

  • 前置通知(BeforeAdvice)
  • 後置通知(AfterAdvice)

  • 後置返回通知(AfterReturningAdvice)
  • 後置異常通知(AfterThrowingAdvice)
  • 環繞通知(AroundAdvice)

三、Pointcut與Execution表示式

​ pointcut使用execution表示式表示要被切入的方法(即定義切入點)。

​ execution表示式,功能類似於正則表示式,都是用來匹配篩選,只不過正則表示式用來篩選字串,而execution表示式用來篩選要被切入的方法。

​ execution表示式的格式為:

execution(<註解>? <修飾符>? <返回值型別> <方法名模式>(<引數模式>) <異常>?)) <and args()>?)

​ 例:execution(@Deprecated public Void aop.MyAspect.hello(int,String) throws Exception))')

package aop;

public class AspectDemo {
  @Deprecated
  public void hello(int i,String s) throws Exception{

  }
}

​ 其實不難發現,這個表示式和我們宣告的方法的各個部分一一對應

  • 註解:(可省略)例如上面程式碼中的@Deprecated,就是篩選帶有該註解的方法

  • 修飾符(可省略)

    • public
    • protected
    • private

    當然一般用萬用字元 *

  • 返回值型別

    寫各種返回值,一般用萬用字元 *

  • 方法名模式

    • 包名部分:在上例中,AspectDemo是位於aop包中的,所以可以通過包名.包名.類名的格式來定位到某個類,例如aop.AspectDemo 中aop. 就是包名部分;

      當然也可以用萬用字元

      • *:匹配任何數量字元,例如service.*.UserService 表示的是service的直接子包
      • ..:匹配任何數量字元的重複,如在型別模式中匹配任何數量子包,例如service..代表著匹配service及其包含的所有包;而在方法引數模式中匹配任何數量引數。
      • +:匹配指定型別的子型別;僅能作為字尾放在型別模式後邊,例如java.lang.Number+ 表示的是lang包下Numer的子類
    • 類名部分:在上例中aop.AspectDemo中aop.是包名部分,AspectDemo就是類名部分,可以用萬用字元來表示,*用的比較多

  • 引數模式

    • 寫法1:直接按照方法的引數列表寫具體型別,上例的方法中引數列表(int i,String s),就可以直接在表示式中寫(int,String)
    • 寫法2:使用萬用字元:
      • “()”表示方法沒有任何引數;
      • “(..)”表示匹配接受任意個引數的方法
      • “(..,java.lang.String)”表示匹配接受java.lang.String型別的引數結束,且其前邊可以接受有任意個引數的方法
      • “(java.lang.String,..)” 表示匹配接受java.lang.String型別的引數開始,且其後邊可以接受任意個引數的方法
      • “(*,java.lang.String)” 表示匹配接受java.lang.String型別的引數結束,且其前邊接受有一個任意型別引數的方法;
  • 異常模式(可省略)

    throws Exception1,Exception2.。。。

  • 傳入引數(可省略)

    ​ and args(arg-name),一般用於AfterAdvice和Around通知

四、前期準備

  1. 建立專案,匯入相關jar包,參考Spring——IOC,此外還需匯入aop和aspectj的jar包
  2. 建立applicationContext.xml檔案
<?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:context="http://www.springframework.org/schema/context"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xsi:schemaLocation="
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop.xsd
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd">
        <aop:aspectj-autoproxy/>
</beans>

​ 注意:新增了

xmlns:aop="http://www.springframework.org/schema/aop"

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/beans/spring-aop.xsd

<aop:aspectj-autoproxy/> 不加這個可能會報錯,可坑了

  1. 建立UserService這個類,內部有insert方法用來註冊使用者
public class UserService {  
    public void insert(){
        System.out.println("UserService正在註冊使用者……");
    }
}
  1. 建立MyAspect類
public class MyAspect {}

五、基於XML配置的AOP

1.BeforeAdvice

​ (1)在MyAspect類中建立方法beforeAdvice

    public void beforeAdvice(){
        System.out.println("【AOP】Before Advice正在執行……");
    }

​ (2)在applicationContext.xml中配置

關於pointcut和execution表示式見下文

    <!--首先要引入myAspect這個bean,備用-->
    <bean id="myAspect" class="aop.MyAspect"/>
    <bean id="userService" class="aop.UserService"/>

    <aop:config>
        <!--配置切面,一個aop:aspect標籤對應一個Aspect類-->
        <aop:aspect id="beforeAdvice" ref="myAspect">
            <!--配置通知 method對應MyAspect類中定義的方法,pointcut是切入點表示式用於篩選需要被                         切入的方法-->
            <aop:before method="beforeAdvice" pointcut="execution(* aop..*.*(..)))"/>
        </aop:aspect>
    </aop:config>

​ (3)編寫測試類

public class UserServiceTest {
    public static void main(String[] args) {
        ApplicationContext context=
                new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = context.getBean("userService", UserService.class);
        userService.insert();
    }
}

​ (4)輸出結果

【AOP】Before Advice正在執行……
 UserService正在註冊使用者……

​ 可以發現,BeforeAdvice就已經實現了

2.AfterAdvice(相當於異常裡面的finally語句)

​ (1)UserService類同上

​ (2)在MyAspect中建立方法afterAdvice

public void afterAdvice(){
    System.out.println("【AOP】after Advice…… 不管怎樣我都會執行");
}

​ (3)修改applicationContext.xml

<aop:config>
    <aop:aspect id="beforeAdvice" ref="myAspect">
        <aop:after method="afterAdvice"
                    pointcut="execution(* aop.*.insert(..)))" />
    </aop:aspect>
</aop:config>

​ (4)編寫測試類(同上)

​ (5)輸出結果

 UserService正在註冊使用者……
【AOP】after Advice…… 不管怎樣我都會執行   

3.AfterReturningAdvice

​ (1)修改UserService的insert方法,使其有返回值

public class UserService {

    public int insert(){
        System.out.println("UserService正在註冊使用者……");
        return 1;
    }
}

​ (2)在MyAspect中新增afterReturningAdvice方法

    public void afterReturningAdvice(int result) {
        System.out.println("【AOP】after advice……返回值為"+result);
    }

​ (3)在applicationContext.xml中配置

<aop:config>
    <aop:aspect id="beforeAdvice" ref="myAspect">
         <aop:after-returning method="afterReturningAdvice"
               pointcut="execution(* aop.*.insert(..)))" returning="result"/>
    </aop:aspect>
</aop:config>

​ 注意:這裡這個returning="result"與MyAspect類中對應方法的引數名必須保持一致,本例中都為result

​ (4)編寫測試類(程式碼同1.)

​ (5)輸出結果

    UserService正在註冊使用者……
 【AOP】after advice……返回值為1   

4.AfterThrowingAdvice

​ (1)修改UserService使其拋異常

public int insert() throws Exception {
    try {
        System.out.println("UserService開始註冊使用者……");
        int i=1/0;

    }catch (Exception e){
        throw new Exception("insert方法遇到異常……");
    }
    return 1;
}

​ (2)在MyAspect中新增方法 afterThrowingAdvice

//這裡傳入的這個Exception就是捕獲到的異常物件
public void afterThrowingAdvice(Exception e){
    System.out.println("【AOP】得到異常資訊:"+e.getMessage());
}

​ (3)修改applicationContext.xml

<aop:config>
    <aop:aspect id="beforeAdvice" ref="myAspect">
        <aop:after-throwing method="afterThrowingAdvice"
                    pointcut="execution(* aop.*.insert(..)))" throwing="e"/>
    </aop:aspect>
</aop:config>

​ 注意:這裡的throwing="e”就是跑出的異常物件的名字,要與MyAspect中afterThrowingAdvice方法中傳入的引數Exception e的名字保持一致。

​ (4)編寫測試類

public static void main(String[] args) throws Exception {
    ApplicationContext context=
            new ClassPathXmlApplicationContext("applicationContext.xml");
    UserService userService = context.getBean("userService", UserService.class);
    userService.insert();
}

​ (5)輸出結果

 UserService開始註冊使用者……
【AOP】得到異常資訊insert方法遇到異常……
Exception in thread "main" java.lang.Exception: insert方法遇到異常……
    at aop.UserService.insert(UserService.java:12)
    at aop.UserService$$FastClassBySpringCGLIB$$7e3b8e5e.invoke(<generated>)
    ...

5.AroundAdvice

​ (1)修改UserService中的insert方法

public int insert(int arg) throws Exception {
    try {
        int i = 1 / 0;

    } catch (Exception e) {
        throw new Exception("insert方法遇到異常……");
    }
    return 1;

}

​ (2)在MyAspect中新增方法AroundAdvice

//這裡這個ProceedingJointPoint可以理解為切入點物件,可以通過它獲取切入點(被切入的方法)的引數、返回值、丟擲的異常,並且可以通過pjp.proceed(args);為該切入點設定引數
public int aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
    Object[] args = pjp.getArgs();
    System.out.println("【AOP】before Advice,獲取到insert方法傳入的引數為:"+args[0]);
    Object result;
    try {
        result=pjp.proceed(args);//這裡是我們手動執行切入點,並傳入引數
        System.out.println("【AOP】after Returning Advice,返回值為:"+result);
    }catch (Exception e){
        //這裡捕獲的就是切入點執行時丟擲的異常
        System.out.println("【AOP】after Throwing Advice,錯誤資訊為:"+e.getMessage());
    }

    System.out.println("【AOP】after advice……不管異常不異常我都執行");

    //這個就跟著這樣寫吧。。如果不寫返回值的話會報 null return value does not match...
    return 1;
}

​ (3)修改applicationContext.xml檔案

<aop:config>
    <aop:aspect id="beforeAdvice" ref="myAspect">
        <aop:around method="aroundAdvice"
                    pointcut="execution(* aop.*.insert(..)))" />
    </aop:aspect>
</aop:config>

​ (4)編寫測試類

public static void main(String[] args) throws Exception {
    ApplicationContext context=
            new ClassPathXmlApplicationContext("applicationContext.xml");
    UserService userService = context.getBean("userService", UserService.class);
    userService.insert(2);
}

​ (5)輸出結果

【AOP】before Advice,獲取到insert方法傳入的引數為:2
【AOP】after Throwing Advice,錯誤資訊為:insert方法遇到異常……
【AOP】after advice……不管異常不異常我都執行

六、基於註解配置的AOP

​ 先把專案狀態恢復到 “四、前期準備”的狀態,然後在applicationContext.xml中新增下面的語句開啟註解和包掃描。

<context:annotation-config/>
<context:component-scan base-package="aop"/>

​ 注意,這個base-package可以配置多個包,以半形(英文)逗號隔開,例如“aop,mvc,dao,service”,當然,為了省事,可以直接配一個頂級包,他會自動遍歷掃描所有的子包及子包的子包等等。

​ 然後為MyAspect類和UserService類加上註解

@Component
@Aspect
public class MyAspect {}

@Service
public class UserService {}

1.BeforeAdvice

​ (1)在MyAspect類中建立beforeAdvice方法,並寫好註解

@Before(value = "execution(* aop..*.*(..)))")
public void beforeAdvice(){
    System.out.println("【AOP】Before Advice正在執行……");
}

​ 不需要配任何bean,是不是很爽

​ (2)編寫測試類

public static void main(String[] args) {
    ApplicationContext context=
            new ClassPathXmlApplicationContext("applicationContext.xml");
    UserService userService = context.getBean("userService", UserService.class);
    userService.insert();
}

​ (3)輸出結果

【AOP】Before Advice正在執行……
 UserService正在註冊使用者……    

2.AfterService(我就不寫測試了)

​ (1)在MyAspect類中建立afterAdvice方法,並寫好註解

@After(value = "execution(* aop..*.*(..)))")
public void afterAdvice(){
    System.out.println("【AOP】after Advice正在執行……");
}

3.AfterReturningAdvice

​ (1)修改UserService中的insert方法

public int insert(){
    System.out.println("UserService正在註冊使用者……");
    return 1;
}

​ (2)在MyAspect類中建立afterReturningAdvice方法,並寫好註解

@AfterReturning(value = "execution(* aop..*.*(..))&& args(result))")
public void afterReturningAdvice(int result){
    System.out.println("【AOP】after Returning Advice正在執行……返回值為:"+result);
}

​ (3)不寫測試了

4.AfterThrowingAdvice

​ (1)修改UserService中的insert方法

public int insert() throws Exception {
    try {
        int i=1/0;
    }catch (Exception e){
        throw new Exception("【UserService】的insert遇到了錯誤……");
    }
    return 1;
}

​ (2)在MyAspect類中建立afterThrowingAdvice方法,並寫好註解

@AfterThrowing(value = "execution(* aop..*.*(..)))",throwing = "e")
public void afterThrowingAdvice(Exception e){
    System.out.println("【AOP】after Throwing Advice正在執行……錯誤資訊為:"+e.getMessage());
}

​ (3)不測試了。。

5.AroundAdvice

​ (1)把MyAspect中之前寫的方法註釋掉,不然會影響觀察結果

​ (2)修改insert方法

public int insert(int arg) throws Exception {
    try {
        int i=1/0;
    }catch (Exception e){
        throw new Exception("【UserService】的insert遇到了錯誤……");
    }
    return 1;
}

​ (3)在MyAspect類中建立aroundAdvice方法,並寫好註解

@Around(value = "execution(* aop..*.*(..)))")
public int aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
    Object[] args = pjp.getArgs();
    System.out.println("【AOP】before Advice,獲取到insert方法傳入的引數為:"+args[0]);
    Object result;
    try {
        result=pjp.proceed(args);
        System.out.println("【AOP】after Returning Advice,返回值為:"+result);
    }catch (Exception e){
        System.out.println("【AOP】after Throwing Advice,錯誤資訊為:"+e.getMessage());
    }

    System.out.println("【AOP】after advice……不管異常不異常我都執行");

    return 1;
}