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


























