Spring AOP快速入門及開發(fā)步驟
Spring 框架有兩大核心 IoC,AOP。在前面我們已經學習過了 IoC 的相關知識,今天就讓我們開始 AOP 的學習。
一、AOP 概述
Aspect Oriented Programming(面向切面編程)。
切面就是指某一類特定問題,所以 AOP 也可以理解為面向特定方法編程。
**AOP 是一種思想,是對某一類事情的集中處理。**Spring AOP 是其中的一種實現方式。
AOP 的作用:在程序運行期間,在不修改源代碼的基礎上,對已有方法進行增強(無侵入性:解耦)。
二、Spring AOP 快速入門
我們先通過下面的程序體驗下 AOP 的開發(fā),并掌握 Spring 中 AOP 的開發(fā)步驟。
2.1 引入 AOP 依賴:
在 pom.xml 文件中添加配置:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
2.2 編寫 AOP 程序:
@Aspect @Slf4j @Component public class TestAspect { @Around("execution(* com.example.demo.controller.*.*(..))") public Object demo(ProceedingJoinPoint joinPoint) throws Throwable { log.info("方法執(zhí)行前執(zhí)行"); Object result = joinPoint.proceed(); log.info("方法執(zhí)行后執(zhí)行"); return result; } }
controller 類:
@RequestMapping("/test") @RestController @Slf4j public class TestController { @RequestMapping("/t1") public void test1(){ log.info("我是 test1"); } }
調用 controller 中的 test1 方法。
結果如下:
對程序進行簡單的講解:
- @Aspect:標識這是一個切面類。
- @Around:環(huán)繞通知,在目標方法的前后都會被執(zhí)行。后面的表達式表示對哪些方法進行增強。
- ProceedingJoinPoint.proceed()讓原始方法執(zhí)行。
整個代碼劃分為三部分。
通過上面的程序,我們也可以感受到 AOP 面向切面編程的一些優(yōu)勢:
- 代碼無侵入:不修改原始的業(yè)務方法,就可以對原始的業(yè)務方法進行了功能的增強或者是功能的改變。
- 減少了重復代碼。
- 提高開發(fā)效率。
- 維護方便。
三、Spring AOP 詳解
3.1 Spring AOP 核心概念:
3.1.1 切點(Pointcut):
切點(Pointcut),也稱之為"切入點"。
Pointcut 的作用就是提供一組規(guī)則(使用 AspectJ pointcut expression language 來描述),告訴程序對哪些方法來進行功能增強。
上面的表達式 execution(* com.example.demo.controller..(…)) 就是切點表達式。
3.1.2 連接點(Join Point):
滿足切點表達式規(guī)則的方法,就是連接點。也就是可以被 AOP 控制的方法。
切點和連接點的關系:
連接點是滿足切點表達式的元素。切點可以看做是保存了眾多連接點的一個集合。
3.1.3 通知(Advice):
通知就是具體要做的工作,指哪些重復的邏輯,也就是共性功能(最終體現為一個方法)。
在 AOP 面向切面編程當中,我們把這部分重復的代碼邏輯抽取出來單獨定義,這部分代碼就是通知的內容。
3.1.4 切面(Aspect):
切面(Aspect)= 切點(Pointcut)+ 通知(Advice)。
通過切面就能夠描述當前 AOP 程序需要針對于哪些方法,在什么時候執(zhí)行什么樣的操作。
切面既包含了通知邏輯的定義,也包括了連接點的定義。
切面所在的類,我們一般稱為切面類(被 @Aspect 注解標識的類)。
3.2 通知類型:
上面我們講了什么是通知,接下來學習通知的類型。@Around 就是其中一種通知類型,表示環(huán)繞通知。Spring 中 AOP 的通知類型有以下幾種:
- @Around:環(huán)繞通知,此注解標注的通知方法在目標方法前后都被執(zhí)行。
- @Before:前置通知,此注解標注的通知方法在目標方法前被執(zhí)行。
- @After:后置通知,此注解標注的通知方法在目標方法后被執(zhí)行,無論是否有異常都會執(zhí)行。
- @AfterReturning:返回后通知,此注解標注的通知方法在目標方法返回后被執(zhí)行,有異常不會執(zhí)行。
- @AfterThrowing:異常后通知,此注解標注的通知方法發(fā)生異常后執(zhí)行。
沒有異常的運行順序:
程序正常運行的情況下,@AfterThrowing 標識的通知方法不會執(zhí)行。
出現異常的運行順序:
@AfterReturning 標識的通知方法不會執(zhí)行,@AfterThrowing 標識的通知方法執(zhí)行了。
@Around 環(huán)繞通知中原始方法調用時有異常,通知中的環(huán)繞后的代碼邏輯也不會再執(zhí)行了(因為原始方法調用出異常了)。
注意:
- @Around 環(huán)繞通知需要調用 ProceedingJoinPoint.proceed() 來讓原始方法執(zhí)行,其他通知不需要考慮目標方法執(zhí)行。
- @Around 環(huán)繞通知方法的返回值,必須指定為 Object,來接收原始方法的返回值,否則原始方法執(zhí)行完畢,是獲取不到返回值的。
- 一個切面類可以有多個切點。
3.3 @PointCut:
Spring 提供了 @PointCut 注解,把公共的切點表達式提取出來,需要用到時引用該切入點表達式即可,便于后續(xù)代碼的維護。
@Aspect @Slf4j @Component public class TestAspect { @Pointcut("execution(* com.example.demo.controller.*.*(..))") public void pt(){} @Around("pt()") public Object demo(ProceedingJoinPoint joinPoint) throws Throwable { log.info("方法執(zhí)行前執(zhí)行"); Object result = joinPoint.proceed(); log.info("方法執(zhí)行后執(zhí)行"); return result; } }
當切點定義使用 private 修飾時,僅能在當前切面類中使用,當其他切面類也要使用當前切點定義時,就需要把 private 改為 public。引用方式為:全限定類名.方法名()。
@Slf4j @Component @Aspect public class TestAspect2 { @Before("com.example.demo.aspect.TestAspect.pt()") public void doBefore() { log.info("執(zhí)? TestAspect2 -> Before ?法"); } }
3.4 切面優(yōu)先級 @Order:
當我們在一個項目中,定義了多個切面類時,并且這些切面類的多個切入點都匹配到了同一個目標方法。當目標方法運行的時候,這些切面類中的通知方法都會執(zhí)行,那么這幾個通知方法的執(zhí)行順序是什么樣的呢?
我們通過程序來進行驗證。
@Slf4j @Component @Aspect public class TestAspect2 { @Pointcut("execution(* com.example.demo.controller.*.*(..))") private void pt(){} //前置通知 @Before("pt()") public void doBefore() { log.info("執(zhí)行 TestAspect2 -> Before 方法"); } //后置通知 @After("pt()") public void doAfter() { log.info("執(zhí)行 TestAspect2 -> After 方法"); } } @Aspect @Component @Slf4j public class TestAspect3 { @Pointcut("execution(* com.example.demo.controller.*.*(..))") private void pt(){} //前置通知 @Before("pt()") public void doBefore() { log.info("執(zhí)行 TestAspect3 -> Before 方法"); } //后置通知 @After("pt()") public void doAfter() { log.info("執(zhí)行 TestAspect3 -> After 方法"); } } @Aspect @Component @Slf4j public class TestAspect4 { @Pointcut("execution(* com.example.demo.controller.*.*(..))") private void pt(){} //前置通知 @Before("pt()") public void doBefore() { log.info("執(zhí)行 TestAspect4 -> Before 方法"); } //后置通知 @After("pt()") public void doAfter() { log.info("執(zhí)行 TestAspect4 -> After 方法"); } }
運行上面程序:
通過上述程序的運行結果,可以看出:
存在多個切面類時,默認按照切面類的類名字母排序:
- @Before 通知:字母排名靠前的先執(zhí)行。
- @After 通知:字母排名靠前的后執(zhí)行。
但這種方式不方便管理,我們的類名更多還是具備一定含義的。
Spring 給我們提供了一個新的注解,來控制這些切面通知的執(zhí)行順序:@Order。
@Slf4j @Component @Aspect @Order(10) public class TestAspect2 { //代碼省略 } @Aspect @Component @Slf4j @Order(5) public class TestAspect3 { //代碼省略 } @Aspect @Component @Slf4j @Order(1) public class TestAspect4 { //代碼省略 }
運行程序:
通過上述程序的運行結果,得出結論:
@Order 注解標識的切面類,執(zhí)行順序如下:
- @Before 通知:數字越小先執(zhí)行。
- @After 通知:數字越大先執(zhí)行。
@Order 的執(zhí)行順序可以抽象成下面這張圖:
3.5 切點表達式:
上面的代碼中,我們一直在使用切點表達式來描述切點。下面我們來介紹一下切點表達式的語法。
切點表達式常見有兩種表達方式:
- execution:根據方法的簽名來匹配。
- @annotation:根據注解匹配。
3.5.1 execution 表達式:
execution() 是最常用的切點表達式,用來匹配方法,語法為:
execution (<訪問修飾符> <返回類型> <包名.類名.方法(方法參數)> <異常>)
其中:訪問修飾符和異??梢允÷浴?/p>
切點表達式支持通配符表達:
*
:匹配任意字符,只匹配一個元素(返回類型,包,類名,方法或者方法參數)。- 包名使用 * 表示任意包(一層包使用一個 * )。類名使用 * 表示任意類。返回值使用 * 表示任意返回值類型。
- 方法名使用 * 表示任意方法(參數可能有限制)。
- 參數使用 * 表示一個任意類型的參數。
..
:匹配多個連續(xù)的任意符號,可以通配任意層級的包,或任意類型,任意個數的參數。
- 使用
..
配置包名,標識此包以及此包下的所有子包。 - 可以使用
..
配置參數,任意個任意類型的參數。
3.5.2 @annotation:
execution 表達式更適用有規(guī)則的,如果我們要匹配多個無規(guī)則的方法呢,比如:TestController 中的 t1() 和 UserController 中的 u1() 這兩個方法。這個時候我們使用 execution 這種切點表達式來描述就不是很方便了。我們可以借助自定義注解的方式以及另一種切點表達式 @annotation 來描述這一類的切點。
實現步驟:
- 編寫自定義注解。
- 使用 @annotation 表達式來描述切點。
- 在方法上添加自定義注解。
創(chuàng)建一個注解類(和創(chuàng)建 Class 文件一樣的流程,選擇 Annotation 就可以了)
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyAspect { }
使用 @annotation 表達式來描述切點。
@Component @Slf4j @Aspect public class MyAspectDemo { @Before("@annotation(com.example.demo.aspect.MyAspect)") public void doBefore(){ log.info("我是 MyAspectDemo"); } }
在方法上添加自定義注解。
@RequestMapping("/test") @RestController @Slf4j public class TestController { @MyAspect @RequestMapping("/t1") public void test1(){ log.info("我是 test1"); } }
運行程序,訪問 test1 方法。
3.6 Spring AOP 的實現方式(常見面試題):
- 基于注解 @Aspect。
- 基于自定義注解(@annotation)。
- 基于 Spring API(通過 xml 配置的方式,自從 SpringBoot 廣泛使用之后,這種方法幾乎看不到了)。
- 基于代理來實現(更加久遠的一種實現方式,寫法笨重,不建議使用)。
四、代理模式
Spring AOP 是基于動態(tài)代理來實現 AOP 的。
代理模式,也叫委托模式。
定義:
為其他對象提供一種代理,以控制對這個對象的訪問。它的作用就是通過提供一個代理類,讓我們在調用目標方法的時候,不再是直接對目標方法進行調用,而是通過代理類間接調用。
代理模式可以在不修改被代理對象的基礎上,通過擴展代理類,進行一些功能的附加與增強。
根據代理的創(chuàng)建時期,代理模式分為靜態(tài)代理和動態(tài)代理。
- 靜態(tài)代理:由程序員創(chuàng)建代理類或特定工具自動生成源代碼再對其編譯,在程序運行前代理類的 .class 文件就已經存在了。
- 動態(tài)代理:在程序運行時,運用反射機制動態(tài)創(chuàng)建而成。
到此這篇關于一文掌握Spring AOP的文章就介紹到這了,更多相關Spring AOP內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Springboot整合hibernate validator 全局異常處理步驟詳解
本文分步驟給大家介紹Springboot整合hibernate validator 全局異常處理,補呢文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2024-01-01