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¶2=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部分:設定切入點,編寫增強能力,織入