目錄
1.AOP概述
2.SpringAOP快速入門
2.1 引入AOP依賴
2.2 編寫AOP程序
3. Spring AOP詳解
3.1 Spring AOP 核心概念
3.1.1切點(Pointcut)
3.1.2 連接點 (Join Point)
3.1.3 通知(Advice)
3.1.4 切面(Aspect)
3.2 通知類型
3.3@PointCut
3.4 切面優先級
3.5 切點表達式
3.5.1 execution 表達式
3.5.2 @annotation
3.5.2.1 自定義注解 @MyAspect
3.5.2.2 切面類
3.5.2.3 添加自定義注解
4. Spring AOP的實現方式
1.AOP概述
AOP是Spring框架的一大核心(第一大核心是loC)
什么是AOP?
Aspect Oriented Programming(面向切面編程)
什么是面向切面編程呢?切面就是指某一類特定問題, 所以 AOP 也可以理解為面向特定方法編程.
什么是面向特定方法編程呢? 比如以前學習的"登錄校驗", 就是一類特定問題.? 登錄校驗攔截器, 就
是對 "登錄校驗" 這類問題的統一處理. 所以,攔截器也是 AOP 的一種應用. AOP 是一種思想,攔截器是 AOP 思想的一種實現. Spring 框架實現了這種思想,? 提供了攔截器技術的相關接口.
同樣的, 統一數據返回格式和統一異常處理,也是AOP思想的一種實現.
簡單來說:? AOP是一種思想,是對某一類事情的集中處理.
?
什么是 Spring AOP ?
AOP是一種思想,它的實現方法有很多,有Spring AOP, 也有AspectJ, CGLIB等.
Spring AOP是其中的一種實現方式.
學會了統一功能之后,是不是就學會了Spring AOP呢,當然不是.
截器作用的維度是URL(一次請求和響應), @ControllerAdvice 應用場景主要是全局異常處理
(配合自定義異常效果更佳), 數據綁定 , 數據預處理.? AOP 作用的維度更加細致(可以根據包、類、方法名、參數等進行攔截),? 能夠實現更加復雜的業務邏輯.
舉個例子:
我們現在有一個項目, 項目中開發了很多的業務功能
現在有一些業務的執行效率比較低, 耗時較長, 我們需要對接口進行優化.
第一步就需要定位出執行耗時比較長的業務方法, 在針對該業務方法來進行優化
如何定位呢? 我們就需要統計當前項目中每一個業務方法的執行耗時.
如何統計呢? 可以在業務方法運行前和運行后,? 記錄下方法的開始時間和結束時間, 兩者之差就是這個方法的耗時.?
這種方法是可以解決問題的,但一個項目中會包含很多業務模塊,每個業務模塊又有很多接口,一個接口又包含很多方法, 如果我們要在每個業務方法中都記錄方法的耗時,對于程序員而言, 會增加很多的工作量.
AOP 就可以做到在不改動這些原始方法的基礎上, 針對特定的方法進行功能的增強.
AOP 的作用: 在程序執行期間不修改源代碼的基礎上對已有方法的增強(無侵入性: 解耦)
接下來我們來看看 SpringAOP 如何來實現.
2.SpringAOP快速入門
學習完什么是 AOP 后, 我們先通過下面的程序體驗下 AOP 的開發, 并掌握 Spring 中 AOP 的開發步驟.
需求: 統計圖書系統中各個接口方法的執行時間.
2.1 引入AOP依賴
在 pom.xml 文件中添加配置
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2 編寫AOP程序
記錄 Controller 中每個方法的執行時間
先看看傳統的方法:
/*** 根據ID查詢圖書信息** @param bookId* @return*/@RequestMapping("/queryBookById")public BookInfo queryBookById(Integer bookId) {log.info("根據ID查詢圖書信息, id:{}", bookId);long start = System.currentTimeMillis();BookInfo bookInfo = bookService.queryBookById(bookId);long end = System.currentTimeMillis();log.info("[BookController] queryBookById 耗時: " + (end-start) + " ms");return bookInfo;}
使用這種方式, 當要測試多個接口的時候, 就需要對每個接口的代碼進行修改?, 工作量比較復雜, 很影響我們的時間和效率.
下面來看看使用AOP的代碼吧:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;@Component
@Slf4j
@Aspect
public class TimeRecordAspect {/*** 記錄耗時*/@Around("execution(* com.example.demo.controller.*.*(..))")public Object TimeRecord(ProceedingJoinPoint joinPoint) throws Throwable {//記錄開始事件long start = System.currentTimeMillis();//執行目標方法Object proceed = joinPoint.proceed();//記錄結束時間long end = System.currentTimeMillis();//日志打印耗時log.info(joinPoint.getSignature() + " 耗時時間: " + (end- start) + " ms");return proceed;}
}
運行程序, 觀察日志
對程序進行簡單的講解:
1. @Aspect: 標識這是一個切面類
2. @Around:?環繞通知, 在目標方法的前后都會被執行. 后?的表達式表示對哪些方法進行增強.
3. ProceedingJoinPoint.proceed(): 讓原始方法執行
整個代碼劃分為三部分
我們通過AOP入門程序完成了業務接口執行耗時的統計.
通過上面的程序,我們也可以感受到AOP面向切面編程的一些優勢:
- 代碼無侵入: 不修改原始的業務方法, 就可以對原始的業務方法進行了功能的增強或者是功能的改變
- 減少了重復代碼
- 提高開發效率
- 維護方便
?
3. Spring AOP詳解
下面我門再來詳細學習AOP, 主要是一下幾個部分
Spring AOP 中涉及的核心概念
Spring AOP 的通知類型
多個 AOP 的執行順序
3.1 Spring AOP 核心概念
3.1.1切點(Pointcut)
切點(Pointcut), 也稱之為 "切入點"
Pointcut 的作用就是提供一組規則(使用 AspectJ pointcut expression language 來描述), 告訴程序對哪些方法來進行功能增強.
上面的表達式??execution(* com.example.demo.controller.*.*(..)) 就是切點表達式.
3.1.2 連接點 (Join Point)
滿足切點表達式規則的方法, 就是連接點, 也就是可以被 AOP 控制的方法
以如門程序舉例, 所有?com.example.demo.controller 路徑下的方法, 都是連接點
package com.example.demo.controller;@RequestMapping("/book")
@RestController
public class BookController {@RequestMapping("/addBook")public Result addBook(BookInfo bookInfo) {//...代碼省略 }@RequestMapping("/queryBookById")public BookInfo queryBookById(Integer bookId) {//...代碼省略 }@RequestMapping("/updateBook")public Result updateBook(BookInfo bookInfo) {//...代碼省略 }
}
上述 BookController 中的方法都是連接點
切點和連接點的關系
連接點是滿足切點表達式的元素, 切點可以看作是保存了眾多連接點的一個集合.
比如:
切點表達式: 全體教師
連接點就是: 張三, 李四等各個教室
3.1.3 通知(Advice)
通知就是具體要做的事情, 指哪些重復的邏輯, 也就是共性功能(最終體現為一個方法)
比如上述程序中記錄業務方法的耗時時間, 就是通知
在AOP面向切面編程當中, 我門把這部分重復的代碼邏輯抽取出來單獨定義, 這部分代碼就是通知類容.
3.1.4 切面(Aspect)
切面(Aspect) = 切點(Pointcut) + 通知(Advice)
通過切面就能描述當前AOP程序需要針對于哪些方法, 在什么時候執行什么樣的操作.
切面既包含了通知邏輯的定義, 也包括了連接點的定義.
切面所在的類, 我們一般稱為切面類(被@Aspect注解標識的類)
3.2 通知類型
上面我們講了什么是通知, 接下來學習通知的類型. @Around 就是其中的一種通知類型, 表示環繞通知.
Spring 中 AOP 的通知類型有一下幾種:
@Around: 環繞通知, 次注解標注的通知方法在目標方法前, 后都被執行
@Before: 前置通知, 次注解表述的通知方法在目標方法前被執行
@After: 后置通知, 次注解標注的通知方法在目標方法后被執行, 無論是否有異常都會執行
@AfterReturning: 返回后通知, 次注解標注的通知方法在目標方法返回后被執行, 有異常不會執行
@AfterThrowing: 次注解標注的通知方法發生異常后執行
接下來我門通過代碼來加深對這幾個通知的理解:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Component
@Slf4j
@Aspect
public class AspectDemo {//前置通知@Before("execution(* com.example.aop.controller.*.*(..))")public void doBefore() {log.info("AspectDemo do before...");}//后置通知@After("execution(* com.example.aop.controller.*.*(..))")public void doAfter() {log.info("AspectDemo do after...");}//環繞通知@Around("execution(* com.example.aop.controller.*.*(..))")public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {log.info("AspectDemo do around before...");Object result = null;try {result = joinPoint.proceed();}catch (Exception e) {log.error("do around 執行目標函數, 內部發生異常");}log.info("AspectDemo do around after...");return result;}//返回后通知@AfterReturning("execution(* com.example.aop.controller.*.*(..))")public void doAfterReturning() {log.info("AspectDemo do AfterReturning...");}//拋出異常后通知@AfterThrowing("execution(* com.example.aop.controller.*.*(..))")public void doAfterThrowing() {log.info("AspectDemo do AfterThrowing...");}
}
寫一些測試程序:
import com.example.aop.config.MyAspect;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RequestMapping("/test")
@RestController
@Slf4j
public class TestController {@MyAspect@RequestMapping("/t1")public String t1() {log.info("執行t1方法....");return "t1";}@RequestMapping("/t2")public String t2() {log.info("執行t2方法....");int a = 10/0;return "t2";}
}
運行程序, 觀察日志:
1.正常運行的情況
觀察日志
程序正常運行的情況下, @AfterThrowing 標識的通知方法不會執行
從上圖也可以看出來, @Around 標識的通知方法包含兩個部分, 一個 "前置邏輯" , 一個 "后置邏輯" .其中 "前置邏輯" 會先于 @Before 標識的通知方法執行. "后置邏輯" 會晚于 @After 標識的通知方法執行
2. 異常時的情況
觀察日志
程序發生異常的情況下:
@AfterReturning 標識的通知方法不會執行, @AfterThrowing?標識的通知方法執行了
@Around 環繞通知中原始方法調用時有異常, 通知中的環繞后的代碼也不會在執行了(因為原始方法調用出異常了)
注意事項:
@Around 環繞通知需要調用 ProceedingJoinPoint.proceed() 來讓原始方法執行, 其他通知不需要考慮目標方法執行.
@Around環繞通知方法的返回值, 必須指定為Object , 來接收原始方法的返回值, 否則原始方法執行完畢, 是獲取不到返回值的.
一個切面類可以有多個切點
3.3@PointCut
上面代碼存在一個問題, 就是存在大量重復的切點表達式 execution(* com.example.demo.controller.*.*(..)) , Spring 提供了 @Pointcut 注解, 把公共的切點表達式提取出來, 需要用到時引入該切點表達式即可.
上述代碼就可以修改為:
@Component
@Slf4j
@Aspect
public class AspectDemo {@Pointcut("execution(* com.example.aop.controller.*.*(..))")public void pt(){};//前置通知@Before("pt()")public void doBefore() {//...代碼省略}//后置通知@After("pt()")public void doAfter() {//...代碼省略}//添加環繞通知@Around("pt()")public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {//...代碼省略}//返回后通知@AfterReturning("pt()")public void doAfterReturning() {//...代碼省略}//拋出異常后通知@AfterThrowing("pt()")public void doAfterThrowing() {//...代碼省略}
}
當請切點使用 private 修飾時, 僅能在當前切面類中使用, 當其他切面類也要使用當前切點定義時, 就需要把 private 改為 public, 引用方式為: 全限定類名.方法名()
@Component
@Slf4j
@Aspect
public class AspectDemo2 {@Before("com.example.aop.aspect.AspectDemo.pt()")public void doBefore() {log.info("AspectDemo2 do before...");}@After("com.example.aop.aspect.AspectDemo.pt()")public void doAfter() {log.info("AspectDemo2 do after...");}
}
3.4 切面優先級
當我們在一個項目中,定義了多個切面類時,并且這些切面類的多個切入點都匹配到了同一個目標方法. 當目標方法運行的時候,這些切面類中的通知方法都會執行,那么這幾個通知方法的執行順序是什么樣的呢?
我們還是通過程序來驗證:
定義多個切?類:
為了防止干擾, 我們把AspectDemo這個切面先去掉(把@Component注解去掉就可以)
為了簡單化, 只寫了@Before 和 @After 兩個通知
@Component
@Slf4j
@Aspect
public class AspectDemo2 {@Before("com.example.aop.aspect.AspectDemo.pt()")public void doBefore() {log.info("AspectDemo2 do before...");}@After("com.example.aop.aspect.AspectDemo.pt()")public void doAfter() {log.info("AspectDemo2 do after...");}
}
@Component
@Slf4j
@Aspect
public class AspectDemo3 {@Before("com.example.aop.aspect.AspectDemo.pt()")public void doBefore() {log.info("AspectDemo3 do before...");}@After("com.example.aop.aspect.AspectDemo.pt()")public void doAfter() {log.info("AspectDemo3 do after...");}
}
@Component
@Slf4j
@Aspect
public class AspectDemo4 {@Before("com.example.aop.aspect.AspectDemo.pt()")public void doBefore() {log.info("AspectDemo4 do before...");}@After("com.example.aop.aspect.AspectDemo.pt()")public void doAfter() {log.info("AspectDemo4 do after...");}
}
運行程序, 訪問接口:
?觀察日志:
通過上述程序的運行結果, 可以看出:
存在多個切面類時, 默認按照切面類的名字母排序:
- @Before 通知: 字母名字靠前的先執行
- @After 通知: 字母排名靠前的后執行
但這種方式不方便管理,我們的類名更多還是具備一定含義的.
Spring給我們提供了一個新的注解,來控制這些切面通知的執行順序: @Order
使用方式如下:
重新運行程序, 觀察日志:
通過上述程序的運行結果, 得出結論:
@Order 注解標識的切面類, 執行順序如下:
@Before 通知: 數字越小先執行
@After 通知: 數字越大先執行
@Order 控制切面的優先級, 先執行優先級高的切面, 在執行優先級較低的切面, 最終執行目標方法.
3.5 切點表達式
上面的代碼中,我們一直在使用切點表達式來描述切點. 下面我們來介紹一下切點表達式的語法.
切點表達式常見有兩種表達方式
1. execution(......): 根據方法的簽名來匹配
2. @annotation(......): 根據注解匹配
3.5.1 execution 表達式
execution() 是最常用的切點表達式, 用來匹配方法, 語法為:
其中: 訪問修飾符和異常可以省略
切點表達式支持通配符表達:
1. *?: 匹配任意字符, 只匹配一個元素(返回類型, 包, 類名, 方法或者方法參數)
a. 包名使用?*?表示任意包(一層包使用一個 * )
b. 類名使用?*?表示任意類
c. 返回值使用?*?表示任意返回值類型
d. 方法名使用 *?表示任意方法
e. 參數使用 *?表示一個任意類型的參數
2. .. : 匹配多個連續的任意符號, 可以通配任意層級的包, 或任意類型, 任意個數的參數
a. 使用 .. 配置包名, 標識次包以及此包下的所有子包
b. 可以使用 .. 配置參數, 任意個任意類型的參數
切點表達式示例
TestController 下的 public 修飾, 返回類型為 String 方法名為 t1, 無參方法
execution(public String com.example.demo.controller.TestController.t1())
省略訪問修飾符
execution(String com.example.demo.controller.TestController.t1())
匹配所有返回類型
execution(* com.example.demo.controller.TestController.t1())
匹配 TestController 下的所有無參方法
execution(* com.example.demo.controller.TestController.*())
匹配 TestController 下的所有方法
execution(* com.example.demo.controller.TestController.*(..))
匹配 controller 包下所有的類的所有方法
execution(* com.example.demo.controller.*.*(..))
匹配所有包下面的 TestController
execution(* com..TestController.*(..))
匹配 com.example.demo 包下, 子孫包下的所有類的所有方法
execution(* com.example.demo..*(..))
匹配特定方法名且有特定參數的方法:
execution(* myMethod(String, int))
?匹配特定方法名且有特定參數, 并且拋出特定異常的方法
execution(* myMethod(String, int) throws IOExeception)
3.5.2 @annotation
execution 表達式更適合有規則的, 如果我門要匹配多個無規則的方法呢, 比如: TestController中的 t1() 和 UserController 中的 u1() 這兩個方法.
這個時候我們使用 execution 這種切點表達式來描述就不是很方便了.
我們可以借助自定義注解的方法以及另一種切點表達式 @annotation 來描述這一類的切點
實現步驟:
1. 編寫自定義注解
2. 使用 @annotation 表達式來描述切點
3. 在連接點的方法上添加自定義注解
準備測試代碼
@RequestMapping("/test")
@RestController
@Slf4j
public class TestController {@RequestMapping("/t1")public String t1() {log.info("執行t1方法....");return "t1";}@RequestMapping("/t2")public String t2() {log.info("執行t2方法....");int a = 10/0;return "t2";}
}
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController{@RequestMapping("/u1")public String u1() {log.info("執行u1方法...");return "u1";}@RequestMapping("/u2")public String u2() {log.info("執行u2方法...");return "u2";}
}
3.5.2.1 自定義注解 @MyAspect
創建一個注解類(和創建Class文件一樣的流程, 選擇Annotation就可以了)
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {}
代碼簡單說明, 了解即可, 不做過多的解釋
1. @Target 標識了 Annotation 所修飾的對象范圍, 及該注解可以用在什么地方.
常用取值:?
ElementType.TYPE: 用于描述類, 接口(包括注解類型)或 enum 聲明
ElementType.METHOD: 描述方法
ElementType.PARAMETER: 描述參數
ElementType.TYPE_USE: 可以標注任意類型
2. @Retention 指 Annotation 被保留的時間長短, 標明注解的聲明周期
@Retention的取值有三種
1. RetentionPolicy.SOURCE: 表示注解僅存在于源代碼中,編譯成字節碼后會被丟棄. 這意味著在運行時無法獲取到該注解的信息,? 只能在編譯時使用.? 比如@SuppressWarnings,? 以及lombok? 提供的注解?@Data, @Slf4j
2.?RetentionPolicy.CLASS: 編譯時注解. 表示注解存在于源代碼和字節碼中,但在運行時會被丟棄. 這意味著在編譯時和字節碼中可以通過反射獲取到該注解的信息,? 但在實際運行時無法獲取. 通常用于一些框架和工具的注解.
3.?RetentionPolicy.RUNTIME: 運行時注解. 表示注解存在于源代碼,字節碼和運行時中. 這意味著在編譯時,字節碼中和實際運行時都可以通過反射獲取到該注解的信息. 通常用于一些需要在運行時處理的注解,如Spring的 @Controller @ResponseBody
3.5.2.2 切面類
使用 @annotation 切點表達式定義切點, 只對 @MyAspect 生效
切面類代碼如下:
@Component
@Slf4j
@Aspect
public class MyAspectDemo {//前置通知@Before("@annotation(com.example.aop.config.MyAspect)")public void before() {log.info("MyAspect -> before...");}//后置通知@After("@annotation(com.example.aop.config.MyAspect)")public void after() {log.info("MyAspect -> after...");}
}
3.5.2.3 添加自定義注解
在 TestController 中 t1() 和 UserController 中的 u1() 這兩個方法上添加自定義注解 @MyAspect, 其他方法不加
運行程序, 測試接口:
觀察日志:
繼續測試 t2, 觀察日志:
切面通知未執行
4. Spring AOP的實現方式
1.基于注解 @Aspect
2. 基于自定義注解(參考上面自定義注解 @annotation 部分的內容)
3. 基于Spring API (通過xml配置的方法, 自從SpringBoot廣泛使用之后, 這種方法幾乎看不到了, 稍作了解即可)
4. 基于代理來實現(更加久遠的一種方式, 寫法笨重, 不建議使用)