Spring AOP 的組成和實現(xiàn)
1. Spring AOP 簡介
AOP 是一種思想,Spring AOP 是這種思想的具體實現(xiàn)。
OOP:面向對象編程
AOP:面向切面編程
AOP 面向切面編程,就是對某一類事情的集中處理。
比如,我們需要在 CSDN 上進行編輯博客、發(fā)布博客、刪除博客等操作,這些功能都是需要進行權限校驗的,判斷是否登錄
開發(fā)三階段
對于公共方法的處理:
- (初級階段)每個方法都去實現(xiàn)
- (中級階段)把同一類功能抽取成公共方法
- (高級階段)采用 AOP 的方式,對代碼無侵入實現(xiàn)
除了統(tǒng)?的用戶登錄判斷之外,AOP 還可以實現(xiàn):
- 統(tǒng)?日志記錄
- 統(tǒng)一方法執(zhí)行時間統(tǒng)計
- 統(tǒng)一的返回格式設置
- 統(tǒng)一的異常處理
- 事務的開啟和提交等
統(tǒng)一方法執(zhí)行時間統(tǒng)計 | 項目監(jiān)控:監(jiān)控項目請求流量、監(jiān)控接口的響應時間甚至每個方法的響應時間 |
統(tǒng)一的返回格式設置 | httpstatus: HTTP狀態(tài)碼 code: 業(yè)務狀態(tài)碼(后端響應成功不代表業(yè)務辦理成功) msg: 業(yè)務處理失敗返回的信息 data: |
也就是說使用 AOP 可以擴充多個對象的某個能力,所以 AOP 可以說是 OOP(Object Oriented Programming,面向對象編程)的補充和完善。
2. AOP 的組成
2.1 切面(Aspect)
切面(Aspect)由切點(Pointcut)和通知(Advice)組成,它既包含了橫切邏輯的定義,也包括了連接點的定義。
切面是包含了:通知、切點和切面的類,相當于 AOP 實現(xiàn)的某個功能的集合。
2.2 連接點(Join Point)
應?執(zhí)行過程中能夠插入切面的?個點,這個點可以是方法調用時、拋出異常時,甚至修改字段 時。切面代碼可以利用這些點插入到應用的正常流程之中,并添加新的行為。
連接點相當于需要被增強的某個 AOP 功能的所有方法。
2.3 切點(Pointcut)
Pointcut 是匹配 Join Point 的謂詞。 Pointcut 的作用就是提供?組規(guī)則(使用 AspectJ pointcut expression language 來描述)來匹配 Join Point,給滿足規(guī)則的 Join Point 添加 Advice。
切點相當于保存了眾多連接點的一個集合(如果把切點看成一個表,而連接點就是表中?條?條 的數(shù)據(jù))。
2.4 通知(Advice)
切面也是有目標的 ——它必須完成的工作。在 AOP 術語中,切面的工作被稱之為通知。
Spring 切面類中,可以在方法上使用以下注解,會設置方法為通知方法,在滿足條件后會通知本 方法進行調用:
- 前置通知使用 @Before:通知方法會在目標方法調用之前執(zhí)行。
- 后置通知使用 @After:通知方法會在目標方法返回或者拋出異常后調用。
- 返回之后通知使用 @AfterReturning:通知方法會在目標方法返回后調用。
- 拋異常后通知使用 @AfterThrowing:通知方法會在目標方法拋出異常后調用。
- 環(huán)繞通知使用戶 @Around:通知包裹了被通知的方法,在被通知的方法通知之前和調用之后執(zhí)行自定義的行為。
切點相當于要增強的方法。
AOP 整個組成部分的概念如下圖所示,以多個頁面都要訪問用戶登錄權限為例:
既然說 AOP 是對一類事情的集中處理,那么我們就需要明確兩點:
- 一類事情:處理對象的一個范圍
- 集中處理:處理的內容是什么
我們通過生活中的一個例子來看一下:
比如,我們乘坐高鐵需要安檢
那么,我們需要處理的內容就是安檢;處理的范圍就是需要乘坐高鐵的人。
此處乘坐高鐵需要安檢這件事情就是切面,處理的內容安檢就是通知,處理的范圍乘坐高鐵的人就是切點,具體有哪些人就是連接點。
切點是一個規(guī)則,事情的處理,最終作用在方法上。
3. Spring AOP的實現(xiàn)
3.1 新建項目
3.2 添加 AOP 框架支持
在 pom.xml 中添加如下配置:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
3.3 定義切面、切點和通知
我們先定義 UserController 類:
@RequestMapping("/user") @RestController public class UserController { // 獲取用戶信息 @RequestMapping("/getInfo") public String getInfo(){ return "get info..."; } // 注冊 @RequestMapping("/reg") public String reg(){ return "reg..."; } // Login @RequestMapping("/login") public String login(){ return "login..."; } }
運行后,成功訪問:
接下來,我們在 UserController 類中定義切面和切點:
@Slf4j @RequestMapping("/user") @RestController public class UserController { // 獲取用戶信息 @RequestMapping("/getInfo") public String getInfo(){ log.info("get info..."); return "get info..."; } // 注冊 @RequestMapping("/reg") public String reg(){ log.info("reg..."); return "reg..."; } // Login @RequestMapping("/login") public String login(){ log.info("login..."); return "login..."; } }
在 LoginAspect 類中使用 @Before 注解(通知方法會在目標方法調用之前執(zhí)行):
@Slf4j @Component @Aspect public class LoginAspect { @Pointcut("execution(* com.example.demo.controller.UserController.* (..))") public void pointcut(){} @Before("pointcut()") public void doBefore(){ log.info("do berore..."); } }
我們接著新建一個 TestController 類:
@Slf4j @RequestMapping("/test") @RestController public class TestController { @RequestMapping("/hi") public String hi(){ log.info("hi~"); return "hi~"; } }
可以看到運行的結果中,并沒有在控制臺打印 @Before 中的內容:
那么為什么沒有執(zhí)行呢?
我們再來看一下其他注解,@After(通知方法會在目標方法返回或者拋出異常后調用):
@Slf4j @Component @Aspect public class LoginAspect { @Pointcut("execution(* com.example.demo.controller.UserController.* (..))") public void pointcut(){} @Before("pointcut()") public void doBefore(){ log.info("do berore..."); } @After("pointcut()") public void doAfter(){ log.info("do after..."); } }
運行結果如下:
@AfterReturning(通知方法會在目標方法返回后調用):
@Slf4j @Component @Aspect public class LoginAspect { @Pointcut("execution(* com.example.demo.controller.UserController.* (..))") public void pointcut(){} @Before("pointcut()") public void doBefore(){ log.info("do berore..."); } @After("pointcut()") public void doAfter(){ log.info("do after..."); } @AfterReturning("pointcut()") public void doAfterReturning(){ log.info("do after returning..."); } }
運行以上代碼后:
可以看到 :@AfterReturning 在 @After 之前被調用。
@AfterThrowing(通知方法會在目標方法拋出異常后調用):
我們首先在 UserController 類中加入異常:
@Slf4j @RequestMapping("/user") @RestController public class UserController { // 獲取用戶信息 @RequestMapping("/getInfo") public String getInfo(){ log.info("get info..."); return "get info..."; } // 注冊 @RequestMapping("/reg") public String reg(){ log.info("reg..."); int a = 10/0; return "reg..."; } // Login @RequestMapping("/login") public String login(){ log.info("login..."); return "login..."; } }
添加 @AfterThrowing 注解:
@Slf4j @Component @Aspect public class LoginAspect { @Pointcut("execution(* com.example.demo.controller.UserController.* (..))") public void pointcut(){} @Before("pointcut()") public void doBefore(){ log.info("do berore..."); } @After("pointcut()") public void doAfter(){ log.info("do after..."); } @AfterReturning("pointcut()") public void doAfterReturning(){ log.info("do after returning..."); } @AfterThrowing("pointcut()") public void doAfterThrowing(){ log.info("do after throwing..."); } }
運行后可以看到:
當正常返回時,執(zhí)行 @AfterReturning 注解,當出現(xiàn)異常時,不會執(zhí)行 @AfterReturning 注解;
當出現(xiàn)異常時,才會執(zhí)行 @AfterThrowing 注解,當正常返回時,不會執(zhí)行 @AfterThrowing 注解。
@Around(通知包裹了被通知的方法,在被通知的方法通知之前和調用之后執(zhí)行自定義的行為):
添加 @Around 注解:
@Slf4j @Component @Aspect public class LoginAspect { @Pointcut("execution(* com.example.demo.controller.UserController.* (..))") public void pointcut(){} @Before("pointcut()") public void doBefore(){ log.info("do berore..."); } @After("pointcut()") public void doAfter(){ log.info("do after..."); } @AfterReturning("pointcut()") public void doAfterReturning(){ log.info("do after returning..."); } @AfterThrowing("pointcut()") public void doAfterThrowing(){ log.info("do after throwing..."); } @Around("pointcut()") public void doAround(ProceedingJoinPoint joinPoint){ log.info("環(huán)繞通知執(zhí)行之前..."); try { joinPoint.proceed(); // 調用目標方法 } catch (Throwable e) { throw new RuntimeException(e); } log.info("環(huán)繞通知執(zhí)行之后..."); } }
運行后界面顯示如下:
可以看到此時界面中不再有返回值,因此修改代碼如下:
@Around("pointcut()") public Object doAround(ProceedingJoinPoint joinPoint){ Object oj = null; log.info("環(huán)繞通知執(zhí)行之前..."); try { oj = joinPoint.proceed(); // 調用目標方法 } catch (Throwable e) { throw new RuntimeException(e); } log.info("環(huán)繞通知執(zhí)行之后..."); return oj; }
此時可以看到成功返回并打印了值:
我們再來看一下這段代碼:
4. 切點表達式說明
AspectJ 支持三種通配符
- * :匹配任意字符,只匹配一個元素(包,類,或方法,方法參數(shù))
- .. :匹配任意字符,可以匹配多個元素 ,在表示類時,必須和 * 聯(lián)合使用。
- + :表示按照類型匹配指定類的所有類,必須跟在類名后面,如 com.cad.Car+ ,表示繼承該類的 所有子類包括本身
切點表達式由切點函數(shù)組成,其中 execution() 是最常用的切點函數(shù),用來匹配方法,語法為:
execution(<修飾符><返回類型><包.類.方法(參數(shù))><異常>)
5. 練習:使用 AOP 統(tǒng)計 UserController 每個方法的執(zhí)行時間
@Slf4j @Component @Aspect public class LoginAspect { @Pointcut("execution(* com.example.demo.controller.UserController.* (..))") public void pointcut(){} @Before("pointcut()") public void doBefore(){ log.info("do berore..."); } @After("pointcut()") public void doAfter(){ log.info("do after..."); } @AfterReturning("pointcut()") public void doAfterReturning(){ log.info("do after returning..."); } @AfterThrowing("pointcut()") public void doAfterThrowing(){ log.info("do after throwing..."); } @Around("pointcut()") public Object doAround(ProceedingJoinPoint joinPoint){ Object oj = null; log.info("環(huán)繞通知執(zhí)行之前..."); try { oj = joinPoint.proceed(); // 調用目標方法 } catch (Throwable e) { throw new RuntimeException(e); } log.info("環(huán)繞通知執(zhí)行之后..."); return oj; } /** * * @param joinPoint 使用 AOP 統(tǒng)計 UserController 每個方法的執(zhí)行時間 * @return */ @Around("pointcut()") public Object doAroundCount(ProceedingJoinPoint joinPoint){ Object oj = null; long start = System.currentTimeMillis(); try { oj = joinPoint.proceed(); // 調用目標方法 } catch (Throwable e) { throw new RuntimeException(e); } log.info(joinPoint.getSignature().toString()+"耗時:"+(System.currentTimeMillis()-start)); return oj; } }
可以看到不同的方法直接在 url 中進行更改重新運行界面即可獲得:
到此這篇關于Spring AOP 的組成和實現(xiàn)的文章就介紹到這了,更多相關Spring AOP實現(xiàn)內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringBoot+SseEmitter和Vue3+EventSource實現(xiàn)實時數(shù)據(jù)推送
本文主要介紹了SpringBoot+SseEmitter和Vue3+EventSource實現(xiàn)實時數(shù)據(jù)推送,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2025-03-03解決idea出現(xiàn)的java.lang.OutOfMemoryError:?Java?heap?space的問題
我們在使用idea的時候經(jīng)常會遇到一些問題,本文介紹了如何解決idea出現(xiàn)的java.lang.OutOfMemoryError:?Java?heap?space的問題,文中有相關的圖文示例,需要的朋友們下面隨著小編來一起學習學習吧2023-06-06Mybatis?Plus插入數(shù)據(jù)后獲取新數(shù)據(jù)id值的踩坑記錄
在某些情況下,需要在執(zhí)行新增后,需要獲取到新增行的id,這篇文章主要給大家介紹了關于Mybatis?Plus插入數(shù)據(jù)后獲取新數(shù)據(jù)id值的相關資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-08-08使用SpringBoot+nmap4j獲取端口信息的代碼詳解
這篇文章主要介紹了使用 SpringBoot + nmap4j 獲取端口信息,包括需求背景、nmap4j 的相關介紹、代碼說明(含測試代碼、改造后的代碼及參數(shù)說明),還提到了文件讀取方式和依賴引入方式,最終請求能獲取到數(shù)據(jù),需要的朋友可以參考下2025-01-01