輕松上手Spring AOP,掌握切面編程的核心技巧
Spring框架是我們使用比較多的一個框架,而AOP又是Spring的核心特性之一,本篇文章將介紹一下AOP的切點表達式、通知等特性及如何使用Spring AOP。
AOP 是什么
AOP(Aspect-Oriented Programming,面向切面編程) 是一種編程范式,旨在將橫切關注點與核心業(yè)務邏輯相分離,以提高代碼的模塊化性、可維護性和復用性。
在傳統(tǒng)的面向?qū)ο缶幊讨?,程序的功能被模塊化為類和方法,但某些功能可能會跨越多個類和方法,如日志記錄、事務管理、安全控制等,這些功能不屬于核心業(yè)務邏輯,但又必須在多個地方重復使用,導致代碼重復和耦合性增加。
AOP提供了一種機制,可以將這些橫切關注點單獨定義,并在需要的地方插入到應用程序中,而不必修改核心業(yè)務邏輯。
AspectJ
AspectJ是一個面向切面的框架,它擴展了Java語言。AspectJ定義了AOP(面向切面編程) 語法,并擁有一個專門的編譯器,用于生成遵守Java字節(jié)編碼規(guī)范的Class文件。
AspectJ可以單獨使用,也可以整合到其他框架中。當單獨使用AspectJ時,需要使用專門的編譯器ajc。AspectJ屬于靜態(tài)織入,通過修改代碼來實現(xiàn),包括編譯期織入等多種織入時機。
Spring集成AspectJ,可以在Spring中方便的使用AOP。
Spring AOP
Spring AOP核心概念主要包括以下幾個方面:
- 切面(Aspect):切面是模塊化橫切關注點的機制,由切入點和通知組成。在Spring AOP中,一個切面可以定義在什么時候、什么地方以及如何應用某種特定的行為到目標對象上。
- 連接點(Joinpoint):連接點是程序執(zhí)行過程中的一個點,例如方法的調(diào)用、字段的訪問等。在Spring AOP中,一個連接點總是代表一個方法的執(zhí)行。連接點是AOP框架可以在其上 “織入” 切面的點。
- 通知(Advice):通知定義了在切入點執(zhí)行時要執(zhí)行的代碼,它是增強應用到連接點上的行為。通知有多種類型,包括前置通知(Before Advice)、后置通知(After Advice) 、環(huán)繞通知(Around Advice) 、異常通知(After Throwing Advice) 和 返回通知(After Returning Advice) 。這些通知類型決定了增強在連接點上的執(zhí)行順序和方式。
- 切點(Pointcut):切點用于定義通知應該應用到哪些連接點上。它是一組連接點的集合,這些連接點共享相同的特性。切點表達式用于匹配連接點,從而確定哪些連接點應該接收通知。
- 目標對象(Target Object) :被一個或多個切面所通知的對象。也被稱為被通知(advised)對象。由于Spring AOP是通過代理模式實現(xiàn)的,因此在運行時,目標對象總是被代理對象所包裹。
- 織入(Weaving):織入是將切面應用到目標對象并創(chuàng)建代理對象的過程。這是AOP框架在運行時或編譯時完成的核心任務。
- AOP代理(AOP Proxy):AOP框架創(chuàng)建的對象,用于實現(xiàn)切面編程。在Spring中,AOP代理可以是JDK動態(tài)代理或CGLIB代理。
- 引入(Introduction):用于向現(xiàn)有的類添加新的接口和實現(xiàn),而不需要修改原始類的代碼。Introduction允許在不修改現(xiàn)有類結構的情況下,向類引入新的功能和行為。在 AspectJ 社區(qū)中,引入稱為類型間聲明(inter-type declaration)。
這些核心概念共同構成了AOP的基礎,使得我們能夠模塊化地處理橫切關注點,從而提高代碼的可維護性和可重用性。
切點表達式
Pointcut 表達式 是用來定義切入點的規(guī)則,它決定了哪些連接點(方法調(diào)用或方法執(zhí)行)將會被通知所影響。在 Spring AOP 中,Pointcut 表達式通常由以下幾種規(guī)則和通配符組成:
- execution(): 用于匹配方法執(zhí)行的連接點,它是最常用的切點指示器。它基于方法簽名進行匹配,可以指定方法的返回類型、包名、類名、方法名以及參數(shù)列表等。比如: @Pointcut("execution(* com.example.myapp.service.*.*(..))") 表示匹配com.example.myapp.service包下所有類的所有方法執(zhí)行。
- within(): 匹配指定類型內(nèi)的方法執(zhí)行連接點。它通常用于匹配特定包或類中的所有方法。示例:@Pointcut("within(com.example.myapp.service.*)") 表示表示匹配com.example.myapp.service包下所有類的所有方法的執(zhí)行。
- this(): 匹配當前代理對象為指定類型的連接點。這用于限制切點只匹配特定類型的代理對象。示例:@Pointcut("this(com.example.myapp.service.MyService)") 表示匹配當前代理對象類型為com.example.myapp.service.MyService的所有方法的執(zhí)行。
- target(): 匹配目標對象為制定類型的連接點。與this()不同,target()是基于目標對象類型,而不是代理類型。示例:@Pointcut("target(com.example.myapp.service.MyServiceImpl)") 表示匹配目標對象類型為com.example.myapp.service.MyServiceImpl的所有方法的執(zhí)行。
- args(): 匹配方法執(zhí)行時參數(shù)為特定類型的連接點。示例:@Pointcut("args(java.io.Serializable)") 表示匹配方法執(zhí)行時至少有一個參數(shù)是java.io.Serializable類型的連接點。
- @annotation(): 匹配執(zhí)行的方法上帶有指定注解的連接點。示例:@Pointcut("@annotation(com.example.myapp.annotation.MyAnnotation)") 表示匹配執(zhí)行的方法上帶有com.example.myapp.annotation.MyAnnotation注解的連接點。
- @target:用于匹配所有帶有特定注解的類或接口。 這個指示器通常與execution表達式結合使用,以進一步細化匹配條件。示例:@Pointcut("@target(com.example.annotation.MyAnnotation)") 表示匹配目標對象類型上帶有com.example.myapp.annotation.MyAnnotation注解的方法執(zhí)行。
- @within:匹配指定類型帶有指定注解的連接點。與within()類似,但它是基于注解而不是包或類。示例: @Pointcut("@within(com.example.myapp.annotation.MyAnnotation)") 表示匹配帶有MyAnnotation注解的類的方法執(zhí)行。
- bean():匹配Spring容器中特定名稱的bean的方法的執(zhí)行。示例: @Pointcut("bean(myServiceImpl)") 表示匹配Spring容器中名稱為myServiceImplbean的方法的執(zhí)行。
- @args():用于限制匹配的方法的參數(shù)必須有指定的注解。
帶有 @ 符的切點表達式都是需要指定注解的連接點。
這些規(guī)則可以通過邏輯運算符(如 &&、||、! )進行組合,以實現(xiàn)更復雜的 Pointcut 匹配規(guī)則。我們可以根據(jù)自己的需求,靈活地使用這些規(guī)則來定義切入點表達式,實現(xiàn)對目標方法的精確匹配和監(jiān)控。
execution()
execution() 表達式使用的比較多,最復雜的一個表達式,這里重點介紹一下。
語法結構
execution() 表達式的語法結構如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
其中,各部分的含義如下:
- modifiers-pattern: 方法的訪問修飾符,如 public、protected 等,可以省略。
- ret-type-pattern: 方法的返回類型,如 void、int 等。
- declaring-type-pattern: 方法所屬的類的類型模式,可以使用通配符 * 匹配任意字符。
- name-pattern: 方法的名稱模式,可以使用通配符 * 匹配任意字符。
- param-pattern: 方法的參數(shù)模式,包括參數(shù)類型和個數(shù)。
- throws-pattern: 方法拋出的異常類型。
示例
- 所有公共方法的執(zhí)行
execution(public * *(..))
- 名稱以 set 開頭的所有方法的執(zhí)行
execution(* set*(..))
- AccountService 接口定義的任何方法的執(zhí)行
execution(* com.xyz.service.AccountService.*(..))
- service 包中定義的任何方法的執(zhí)行
execution(* com.xyz.service.*.*(..))
- service 包或其子包之一中定義的任何方法的執(zhí)行
execution(* com.xyz.service..*.*(..))
- 執(zhí)行指定類型參數(shù)的方法
execution(* com.example.service.MyService.myMethod(String, int))
注意事項
- 在 execution() 表達式中,通配符 * 可以用來匹配任意字符或任意個數(shù)的字符。
- 使用 execution() 表達式時,需要注意合理地組織表達式,以確保精準地匹配目標方法。
- 可以通過組合多個條件來更加靈活地定義切點,例如同時匹配方法的訪問修飾符、返回類型、類名、方法名等。
總的來說,execution() 方法提供了一種靈活且強大的方式來定義切點表達式,從而精確定位需要添加通知的目標方法。
通知(Advice)類型
圖片
在 Spring AOP 中,通知(Advice)是在切入點(Pointcut)上執(zhí)行的代碼。Spring 提供了幾種類型的通知,每種類型都對應著在連接點執(zhí)行前、執(zhí)行后或拋出異常時執(zhí)行的不同代碼邏輯。這些通知對應著不同的注解,常用的通知注解包括:
- @Before: 在方法執(zhí)行之前執(zhí)行的通知。它有以下屬性:
value:要綁定的切點或者切點表達式。
argNames: 用于指定連接點表達式中方法參數(shù)的名稱,以便在通知方法中通過參數(shù)名來獲取方法參數(shù)的值。這樣可以在前置通知中訪問和處理方法參數(shù)的具體數(shù)值。該屬性即使不指定也能獲取參數(shù)。
- @AfterReturning: 在方法執(zhí)行成功返回結果后執(zhí)行的通知。它比 @Before注解多了2個屬性:
pointcut:作用和value屬性一樣,當指定pointcut時,會覆蓋value屬性的值。
returning:方法返回的結果將被綁定到此參數(shù)名,可以在通知中訪問方法的返回值。
@AfterThrowing: 在方法拋出異常后執(zhí)行的通知。它的屬性前3個和 @AfterReturning注解一樣,多了1個屬性:
throwing:指定方法拋出的異常將被綁定到此參數(shù)名,可以在通知中訪問方法拋出的異常。
@After: 在方法執(zhí)行后(無論成功或失?。﹫?zhí)行的通知。屬性同 @Before 注解。
@Around: 環(huán)繞通知,能夠在方法執(zhí)行前后都可以進行操作,具有最大的靈活性。屬性同 @Before 注解。
通知的執(zhí)行順序為: @Around -> @Before -> @AfterReturning(不拋異常情況) 或者 @AfterThrowing(拋異常情況) -> @After
這些通知注解可以與 Pointcut 表達式結合使用,實現(xiàn)對目標方法的攔截和處理。通過選擇合適的通知類型,開發(fā)者可以根據(jù)需求在不同的時間點插入自定義的邏輯,實現(xiàn)對方法調(diào)用的控制和增強。
如何使用
講了那么多概念性的東西,下面來看怎么使用Spring AOP。
在Spring 中使用AOP也很簡單,主要分3步:
- 定義切面
- 定義切點
- 在具體通知上使用切點
準備階段
我這里使用的是Springboot 3.1.5、jdk 17,如果是Springboot低版本的可能需要引入 spring-boot-starter-aop 依賴,高版本的AOP已經(jīng)包含在spring-boot-starter-web依賴中了:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Spring官網(wǎng)中介紹,使用Spring AOP要在啟動類或者配置類中加上 @EnableAspectJAutoProxy 注解開啟 AspectJ 注解的支持,在我使用的這個版本中并不需要,如果你的項目中切面未生效可以嘗試使用該注解。
定義一個接口,下面用于對這個接口及其實現(xiàn)類進行攔截:
public interface AopService {
/**
* 兩數(shù)除法
* @param a
* @param b
* @return
*/
BigDecimal divide(BigDecimal a, BigDecimal b);
/**
* 兩數(shù)加法
* @param a
* @param b
* @return
*/
BigDecimal add(BigDecimal a, BigDecimal b);
}
@Service
public class MyAopServiceImpl implements AopService{
/**
* 兩數(shù)除法
*
* @param a
* @param b
* @return
*/
@Override
public BigDecimal divide(BigDecimal a, BigDecimal b) {
return a.divide(b , RoundingMode.UP);
}
/**
* 兩數(shù)加法
*
* @param a
* @param b
* @return
*/
@Override
public BigDecimal add(BigDecimal a, BigDecimal b) {
return a.add(b);
}
}
定義切面
新建一個類,在類上加上@Aspect 注解,標記該類為切面。
@Component
@Aspect
public class AspectComponent {
}
定義并使用切點
在切面中使用@Pointcut注解定義切點表達式,然后在通知注解中使用定義好的切點。在該示例中主要對AopService#divide()方法進行攔截。
@Component
@Aspect
public class AspectComponent {
/**
* 匹配AopService接口的divide方法
*/
@Pointcut("execution(* site.suncodernote.aop.AopService.divide(..))")
void dividePointCut(){
}
/**
* 匹配AopService接口的divide方法
*/
@Pointcut("within(site.suncodernote.aop.AopService+)")
void withinPointCut(){
}
/**
* 匹配AopService接口的add方法 或者 divide方法
*/
@Pointcut("execution(* site.suncodernote.aop.AopService.add(..)) || execution(* site.suncodernote.aop.AopService.divide(..))")
void addOrDividePointCut(){
}
@Before("dividePointCut()")
public void beforeDivide(JoinPoint joinPoint){
System.out.println("---------------------@Before----------------");
printJoinPoint(joinPoint);
}
@After("dividePointCut()")
public void afterDivide(JoinPoint joinPoint){
System.out.println("---------------------@After----------------");
printJoinPoint(joinPoint);
}
@AfterReturning(pointcut = "dividePointCut()" , returning = "result")
public void afterReturningDivide(JoinPoint joinPoint , BigDecimal result){
System.out.println("---------------------@AfterReturning----------------");
System.out.println("返回結果="+result);
printJoinPoint(joinPoint);
}
@AfterThrowing(pointcut = "dividePointCut()" , throwing = "e")
public void afterThrowingDivide(JoinPoint joinPoint ,Exception e){
System.out.println("---------------------@AfterThrowing----------------");
System.out.println("異常:"+e.getMessage());
printJoinPoint(joinPoint);
}
@Around("dividePointCut()")
public Object aroundDivide(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("---------------------@Around----------------");
printJoinPoint(joinPoint);
Object[] args = joinPoint.getArgs();
Object result = null;
try {
//執(zhí)行方法
result = joinPoint.proceed(args);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("返回值:"+result);
return result;
}
private void printJoinPoint(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
Signature signature = joinPoint.getSignature();
System.out.println("方法名:"+signature.getName());
System.out.println("方法參數(shù):"+ Arrays.toString(args));
System.out.println();
}
}
測試
寫個簡單的單元測試,調(diào)用AopService#divide()方法,然后看一下輸出結果。
@SpringBootTest
public class AOPTest {
@Resource
private AopService aopService;
@Test
public void testAOP() {
BigDecimal a = new BigDecimal(1);
BigDecimal b = new BigDecimal(2);
// aopService.add(a, b);
aopService.divide(a, b);
}
}
測試結果:
---------------------@Around----------------
方法名:divide
方法參數(shù):[1, 2]
---------------------@Before----------------
方法名:divide
方法參數(shù):[1, 2]
---------------------@AfterReturning----------------
返回結果=1
方法名:divide
方法參數(shù):[1, 2]
---------------------@After----------------
方法名:divide
方法參數(shù):[1, 2]
返回值:1
從測試結果中通知執(zhí)行的順序是按照我們上面所說的執(zhí)行順序執(zhí)行的。
總結
本文介紹了Spring AOP的常用的切點表達式、通知注解等,我們可以利用AOP對業(yè)務邏輯的各個部分進行隔離,使得業(yè)務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高開發(fā)的效率。