什麼是AOP,以及在Springboot中自定義AOP

王若伊_恩赐解脱發表於2024-08-17

AOP (Aspect Oriented Programming)一般譯為面向切面程式設計

Aspect [ˈæspekt] n.方面;層面;(動詞的)體
那麼AOP 面相切面程式設計具體是指什麼,它和之前的OOP 面相物件程式設計又有什麼區別和聯絡。
先說OOP,面相物件程式設計簡單來說,萬物皆可視為物件,我們要做的就是將萬物(業務邏輯中的虛擬物體),抽象為一個個物件,進而為這些抽象的物體豐富各種能力和特性(方法和屬性)。從而抽象出一整段的業務邏輯,作為我們的系統。

但是在OOP的開發過程中,我們發現儘管我們已經抽象出很多物件了,但是物件之間的某些方法是有一些共性的,如果進一步抽象,則整體的抽象粒度過於小,抽象粒度過於複雜。在這種情況下,我們需要換一個角度,將這些共性的點,作為一個切入點,將我們的業務邏輯注入到裡邊去,直接去增強這些切入點。而這種增強的物件某個同性點的程式設計方式,我們就稱之為AOP,即面相切面程式設計。(防盜連線:本文首發自http://www.cnblogs.com/jilodream/ )

舉個例子:

學校的老師,每天都需要統計上課工時,政府的公務員,每天都需要統計辦公工時,同時辦公室的電腦也要統計每天的開機時長。

他們本質上都是物件,如果統計每天的執行時間,我們現在有兩個辦法來實現:

1,各自實現各自的統計辦法,實現簡單,但是修改複雜,而且有大量重複邏輯。

2,定義統一的介面,老師、公務員、電腦實現同樣的介面,這樣減少了重複邏輯,但是又實現複雜,整個抽象粒度太細了。

此時我們就可以透過AOP程式設計的方式,將老師、公務員、機器的辦公,作為一個切入點,在這個切入點作一些物件之外的處理工作。這就是所謂的面向切面程式設計。這看著有點像作弊,所以很多人將aop視為物件導向程式設計的一個補充。是從第三方的視角,來看待物件導向程式設計的,如下圖:

下面我們來看看,如何在當下最流行的java框架springboot框架中,實現面向切面程式設計:
面相切面程式設計主要實現三個基本操作:
1、設定切入面 Aspect (放到哪裡)
2、編寫增加能力,即注入到業務邏輯中的新特性 (放什麼)
3、織入,即將新特性注入到原有的業務邏輯中。(怎麼放進去)
假設我們現在已經有一個簡易的Springboot工程,實現兩個字串的連線:

首先我們新增相關的pom依賴:

1         <dependency>
2             <groupId>org.springframework.boot</groupId>
3             <artifactId>spring-boot-starter-aop</artifactId>
4         </dependency>
5         <dependency>
6             <groupId>org.aspectj</groupId>
7             <artifactId>aspectjweaver</artifactId>
8             <version>1.9.7</version>
9         </dependency>

接著我們按照3個基本操作來新增aop能力:

1、設定切入面
設定切面的常用方式有兩種,我們依次來看
(1)使用註解的形式

 1 package com.example.demo.learnaop;
 2 
 3 
 4 import java.lang.annotation.ElementType;
 5 import java.lang.annotation.Retention;
 6 import java.lang.annotation.RetentionPolicy;
 7 import java.lang.annotation.Target;
 8 
 9 @Target(ElementType.METHOD)
10 @Retention(RetentionPolicy.RUNTIME)
11 public @interface LogAop {
12 }

如上先定義一個註解:@Target,我們設定為method,@Retention,我們設定為runtime,該註解可被標記到方法中,同時執行時期要使用該註解。(關於java的註解,屬於java的基礎知識,但是在新興的框架中,他的作用越來越大,我抽時間會寫一篇相關的文章)

定義如下的切面類:
隨意定義一個切面方法,方法的註解@PointCut,標記好要增加的註解的全限定類名。
然後我們就可以在我們想要設定的切面出設定切入點了,如下

1 public class AopAdvice {
2 
3     @Pointcut("execution(* com.example.demo.learnaop.DoService.learnMinus(..))")
4     public void logAopCut() {
5         int a=1;
6         System.out.println("point cut 123 " );
7         //  log.warn("ex advice1");
8     }
9 }

業務程式碼像這樣新增定義好的註解,如紅色字型:

1     @LogAop //像這樣
2     @Override
3     public String learnMinus(String para1, String para2) {
4         //   log.warn("start Minus");
5         System.out.println("service learn minus "+para1 +para2 );
6         return para1 + "-" + para2;
7     }

這種方式比較符合目前的程式設計思路,(防盜連線:本文首發自http://www.cnblogs.com/jilodream/ )儘可能的使用各種註解來代替原有的各種配置,降低配置的維護難度。

(2)使用execution 表示式
我們可以不定義註解,直接在切面方法上設定,要切入的點,如下:

1 public class AopAdvice {
2 
3     @Pointcut("execution(* com.example.demo.learnaop.DoService.learnMinus(..))")
4     public void logAopCut() {
5         int a=1;
6         System.out.println("point cut 123 " );
7         //  log.warn("ex advice1");
8     }
9 }

execution後邊的部分,我們使用的表示式稱之為 execution表示式

這是一種類似於正則的表示式,總體的結構如下圖

問號部分我們可填也可以不填,同時我們可以使用*,..來實現模糊匹配,

* 可以模糊匹配,某一個層級的選項,或者某一層級一部分的選項,比如我們想省略某一層級包名,也可以省略方法名的某一部分。
.. 可以用來省略多級選項。
限於篇幅有限,這裡就不過多的介紹execution表示式了。
這樣我們就可以直接根據全限定路徑,直接指定某一層級方法作為切入點了。
這裡有兩點需要注意的是:
如果使用的註解表示式,則註解加入到介面中,是不能在實現類中新增切入點的,換句話說不會直接生效。
注意:使用execution表示式時,如果表示式匹配的是父類或介面,則對應子類的切入點是會生效的。這裡也和java中註解不會直接繼承,繼承類和介面實現類,卻可以替代類和介面中的方法是一個效果。

2、編寫增強能力
我們繼續在OPTAopAdvice類中新增如下方法:

方法的註解可以依次使用
@Before 切入面執行執行
@After 切入面返回之後
@Around 切入面環繞
@AfterReturning 切入面正常返回後
@AfterThrowing 切入面異常返回後
@After 是包含@AfterReturning @AfterThrowing兩種場景的。
像下面這樣,我們就可以定義幾個增強能力

 1 public class AopAdvice {
 2 
 3   
 4     @Before("logAopCut()")
 5     public Object logBefore() {
 6         System.out.println("log before !!!" );
 7         return "123456654rjdkkgjlkjg";
 8     }
 9 
10     @Before("optAopCut()")
11     public Object optBefore() {
12         System.out.println("opt before !!!" );
13         return "123456654rjdkkgjlkjg";
14     }
15 
16     @After("optAopCut()")
17     public void optAfter() {
18         System.out.println("opt after !!!" );
19 
20     }
21 
22     @After("logAopCut()")
23     public void logAfter() throws Throwable {
24         System.out.println("log after !!!" );
25 
26     }
27 
28     @Around("optAopCut()")
29     public Object optAround1(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
30         System.out.println("around start1 " );
31         Object proceed = proceedingJoinPoint.proceed();
32         System.out.println("around end1 " );
33         return proceed;
34     }
35 
36     @Around("optAopCut()")
37     public Object optAround2(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
38         System.out.println("around start2 " );
39         Object proceed = proceedingJoinPoint.proceed();
40         System.out.println("around end 2" );
41         return proceed;
42     }
43 }

3、織入
這一步理論上來說最複雜,但是和具體業務邏輯又距離最遠,(防盜連線:本文首發自http://www.cnblogs.com/jilodream/ )所以spring早已替我們封裝好了
我們只需要在OPTAopAdvice類上新增@Aspect @Component,分別表示要進行切入處理,和進行springboot的bean管理。

整體的程式碼如下:

controller層

 1 package com.example.demo.learnaop;
 2 
 3 import lombok.extern.slf4j.Slf4j;
 4 import org.springframework.beans.factory.annotation.Autowired;
 5 import org.springframework.web.bind.annotation.GetMapping;
 6 import org.springframework.web.bind.annotation.RequestParam;
 7 import org.springframework.web.bind.annotation.RestController;
 8 
 9 import java.util.Date;
10 
11 /**
12  * @discription
13  */
14 @Slf4j
15 @RestController
16 public class Controller {
17 
18     @Autowired
19     private DoService doService;
20 
21     @Autowired
22     private DoServiceImpl doServiceImpl;
23 
24     @Deprecated
25     @GetMapping("/learn/add")
26     public String learnAdd(@RequestParam("para1") String para1, @RequestParam("para2") String para2) {
27         //   log.debug("show plugin Profile {} ,{}", para1, para2);
28         System.out.println("controller learn add " + para1 + para2);
29         return doService.learnMinus(para1, para2) + doService.learnAdd(para1, para2);
30     }
31 
32     @Deprecated
33     @GetMapping("/learn/minus")
34     public String learnMinus(@RequestParam("para1") String para1, @RequestParam("para2") String para2) {
35         //   log.debug("show plugin Profile {} ,{}", para1, para2);
36         System.out.println("controller learn Minus " + para1 + para2);
37         Date date = new Date();
38         return doService.learnMinus(para1, para2) + date.getTime() + date.getSeconds() + "";
39     }
40 
41 }

service層

 1 package com.example.demo.learnaop;
 2 
 3 /**
 4  * @discription
 5  */
 6 public interface DoService {
 7 
 8     String learnAdd(String para1, String para2);
 9 
10 
11     String learnMinus(String para1,  String para2);
12 
13 }

 1 package com.example.demo.learnaop;
 2 
 3 
 4 import lombok.extern.slf4j.Slf4j;
 5 import org.springframework.stereotype.Service;
 6 
 7 /**
 8  * @discription
 9  */
10 @Slf4j
11 @Service
12 public class DoServiceImpl implements DoService {
13     @Override
14     public String learnAdd(String para1, String para2) {
15         System.out.println("service learn add "+para1 +para2 );
16         return para1 + "+" + para2;
17     }
18 
19     @OPTAop
20     @Override
21     public String learnMinus(String para1, String para2) {
22         //   log.warn("start Minus");
23         System.out.println("service learn minus "+para1 +para2 );
24         return para1 + "-" + para2;
25     }
26     
27     @Override
28     public void learnNothing() {
29         System.out.println("service learn do nothing  " );
30 
31     }
32 }

服務埠我們設定為8081,

請求如下url

http://127.0.0.1:8081/learn/minus?para1=1a&para2=2b

控制檯輸出如下

 1 controller learn Minus 1a2b
 2 around start1 
 3 around start2 
 4 log before !!!
 5 opt before !!!
 6 service learn minus 1a2b
 7 opt after !!!
 8 log after !!!
 9 around end 2
10 around end1 

注意看這裡有兩個細節

1、執行順序,

around先執行,然後才會執行 before、after 接著又跳轉回around,也就是說before 和after 更接近切面點,這一點我們在處理諸如分散式鎖的場景要考慮到。

2、可以在一個切入點加入多個方法,

切入順序一般是按照程式碼的載入順序(書寫順寫)來加入的,儘管是在一個切入點加入了多個增強方法,但是隻執行一遍切入面的程式碼。(他的原理是什麼呢?如何模仿或者實現呢,我後文會詳細介紹)

以上說的比較多,這裡我們總結一下,

1、aop是物件導向的補充,是針對多個物件的共同特性,我們統一增強能力的一個途徑。

2、自定義aop程式設計只要實現3部分:設定切入點,編寫增強能力,織入

相關文章