如何使用Spring AOP優(yōu)雅實現(xiàn)橫切邏輯復(fù)用
前言
在OOP的思想中,一切皆以對象為核心,將數(shù)據(jù)與操作數(shù)據(jù)的方法封裝在一起,通過封裝、繼承、多態(tài)等特性實現(xiàn)高內(nèi)聚、低耦合的代碼結(jié)構(gòu)。高內(nèi)聚和低耦合正是我們開發(fā)程序所要遵循的兩大終極目標。
試想一下,如果有一項業(yè)務(wù)開發(fā)的邏輯,此時我們實現(xiàn)它需要發(fā)現(xiàn)需要修改核心源碼,這顯然不利于程序的維護。那么有什么辦法能實現(xiàn)不修改源碼的情況下動態(tài)擴展功能呢,答案顯而易見,通過代理模式,建立代理對象來代替對真實對象(real object)的訪問,這樣就可以在不修改原目標對象的前提下,提供額外的功能操作。
當然在Spring中已經(jīng)提供了一套成熟的解決方案,無需我們手動實現(xiàn)代理模式,它就是大名鼎鼎的Spring AOP。本篇文章我將和大家一起梳理AOP這種編程范式,討論在Spring Boot中應(yīng)用AOP編程。
一、什么是AOP
AOP全稱Aspect Oriented Programming,直譯過來是 面向切面編程。這其實是一種編程范式,它允許我們不修改源代碼的情況下,給程序中動態(tài)得添加擴展功能,是作為面向?qū)ο蟮囊粋€補充,方便我們?nèi)ゾS護程序,提高模塊的內(nèi)聚性,降低模塊間的耦合。通過預(yù)編譯方式和運行期動態(tài)代理實現(xiàn)程序功能的統(tǒng)一維護。通過AOP,我們就能實現(xiàn)給特定的方法添加特點的邏輯,比如記錄方法的執(zhí)行時間,操作日志。事實上Spring中的事務(wù)也是Spring通過AOP實現(xiàn)的。
二、AOP的核心概念
2.1 連接點(Join Point)
連接點指的是程序執(zhí)行過程中可以插入切面代碼的特定位置,在Spring中簡單來說就是可以被AOP控制的方法,即方法調(diào)用。
比方說下面這個StudentServiceImpl里的findAll方法,這個findAll方法的每一次調(diào)用就是一個連接點。Spring AOP中,默認情況下,IOC容器中Bean的 public方法可作為連接點。
@Service
public class StudentServiceImpl implements StudentService {
private final StudentMapper studentMapper;
@Override
public List<Student> findAll() {
return studentMapper.findAll();
}
}
2.2 通知(Advice)
通知被定義為在特定連接點上執(zhí)行的動作,像我們要織入的橫切自定義邏輯,比如記錄方法的執(zhí)行時間,操作日志這種。Spring AOP中通知分為以下幾類:
- 前置通知(Before):在方法執(zhí)行前運行。
- 后置通知(After):無論方法是否異常,都會在方法結(jié)束后運行。
- 返回通知(AfterReturning):方法成功返回后運行。
- 異常通知(AfterThrowing):方法拋出異常時運行。
- 環(huán)繞通知(Around):包圍整個方法調(diào)用,可控制是否執(zhí)行原方法
通知在Spring AOP中的作用是用來告訴定義的自定義邏輯應(yīng)該放在什么時候執(zhí)行。
2.3 切入點(Pointcut)
切入點是一個表達式,用來定義匹配連接點,也就是指定哪些連接點(Join Point)需要應(yīng)用通知(Advice)。一般情況下,切入點的設(shè)置的越局限,通知的應(yīng)用范圍就越小,AOP執(zhí)行的效率就越大。所以在實際生產(chǎn)環(huán)境下,最好避免對所有方法都織入邏輯。切入點其實就是指定自定義邏輯在哪里執(zhí)行。
在Spring Boot中定義切入點是通過在空方法上通過@Pointcut注解來定義匹配連接點。
定義一個名為servicePointCut的切入點,匹配所有public修飾、返回值為任意類型的org.araby.blognovelink.service.impl.StudentServiceImpl類下的所有方法
@Pointcut("execution(public * org.araby.blognovelink.service.impl.StudentServiceImpl.*(..))")
public void servicePointCut(){}
2.3.1 execution定義匹配連接點
通過execution定義匹配連接點格式是
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
- modifiers-pattern:訪問修飾符public、private這種,可選;
- ret-type-pattern:返回值類型,如void、String、*表示任意返回值,必選;
- declaring-type-pattern:全類名,…表示當前包及子包,可選;
- name-pattern:方法名稱,*表示任意方法,必選;
- param-pattern:方法參數(shù)列表,…表示任意參數(shù)),必選;
- throws-pattern:異常類型,可選。
2.3.2 annotation定義匹配連接點
還有一種方式是通過注解來定義匹配連接點
自定義@Log注解:
import java.lang.annotation.*;
@Target(ElementType.METHOD) // 僅作用于方法
@Retention(RetentionPolicy.RUNTIME) // 運行時保留,可通過反射獲取
@Documented
public @interface Log {
String value() default ""; // 注解屬性,用于描述日志內(nèi)容
}定義切入點,匹配被@Log注解標記的方法
@Before("@annotation(org.araby.blognovelink.annotation.Log)")
public void beforeAdvice(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("【前置通知】方法名:" + methodName + ",參數(shù):" + Arrays.toString(args));
}
最后在方法上添加注解就能織入自定義的邏輯
@Log
@Override
public Boolean update(Student student) {
student.setUpdateTime(java.time.LocalDate.now());
return studentMapper.update(student) > 0;
}
2.4 切面
通知加切入點的組合就是切面。前面提到’通知’是設(shè)置我們自定義邏輯應(yīng)該放在什么時候執(zhí)行,切入點是設(shè)置自定義邏輯在哪里執(zhí)行。這兩者組合起來就是切面,即在哪些地方做什么,這就是切面
在Spring Boot中,切面類是由@Aspect 修飾,我們在里面定義切入點和通知。
比方說這個被@Aspect修飾的LogAspect類,它包含一個servicePointCut切入點,和一個beforeAdvice()前置通知方法。
@Slf4j
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(public * org.araby.blognovelink.service.impl.StudentServiceImpl.*(..))")
public void servicePointCut(){}
@Before("servicePointCut()")
public void beforeAdvice(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("【前置通知】方法名:" + methodName + ",參數(shù):" + Arrays.toString(args));
}
}2.5 目標對象(Target Object)
目標對象既被一個或多個切面通知的對象,在Spring AOP中,目標對象必須是Bean對象,它和切面對象一樣是交由Spring IOC容器管理,目標對象總是被代理包裝,使用JDK動態(tài)代理或CGLIB。
Spring AOP正是通過代理模式來增強原始對象的能力。
2.6 小結(jié)
總的來說,在Spiring AOP中,通過切面(Aspect) 通過 切入點(Pointcut) 找到 連接點(Join Point),然后在目標對象(Target Object) 的方法中執(zhí)行時織入通知(Advice),這樣就能實現(xiàn)原始對象的功能增強。
三、Spring Boot中使用AOP
3.1 引入Spring Boot AOP依賴
dependencies層級下添加Spring Boot AOP依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.2 編寫切面類
在編寫切面類之前,我們假設(shè)有個名為StudentServiceImpl的業(yè)務(wù)類作為目標對象,全類名為org.araby.blognovelink.service.impl.StudentServiceImpl。以下的切入點都匹配的是StudentServiceImpl的findById方法。業(yè)務(wù)類
@Service
public class StudentServiceImpl implements StudentService {
private final StudentMapper studentMapper;
@Autowired
public StudentServiceImpl(StudentMapper studentMapper) {
this.studentMapper = studentMapper;
}
@Override
public Student findById(Integer id) {
return studentMapper.findById(id);
}
}然后我們編寫一個切面類來實現(xiàn)前置通知,返回通知,異常通知,后置通知和環(huán)繞通知。
3.2.1 前置通知-@Before
如果我們想在目標對象的方法前執(zhí)行邏輯,可以選用@Before注解修飾的前置通知,前置通知方法的參數(shù)包括JoinPoint ,這個正是連接點,包含目標對象方法的元數(shù)據(jù)。比方說可以通過joinPoint的getSignature()獲取方法信息。
/**
* 前置通知
* @param joinPoint
*/
@Before("servicePointCut()")
public void beforeAdvice(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("【前置通知】方法名:" + methodName + ",參數(shù):" + Arrays.toString(args));
}[INFO ] [2025-12-10 21:34:59.688] [http-nio-9090-exec-2] [REQ-7eaebf76034c4b0aa3ef5d8bc6da1536] o.araby.blognovelink.aop.LogAspect - 【前置通知】方法名:findById,參數(shù):[1]
3.2.2 返回通知-@AfterReturning
標記返回通知的@AfterReturning注解包括一個額外的屬性returning 。在下面的示例代碼中相當于告訴 Spring,把目標方法的返回值,賦值給通知方法中名為Student的那個參數(shù)。也就是returning的值和通知方法的參數(shù)名稱要一致,且參數(shù)類型需兼容目標方法的返回值類型。
/**
* 返回通知
* @param joinPoint
* @param Student
*/
@AfterReturning(value = "servicePointCut()",returning = "Student")
public void afterReturning(JoinPoint joinPoint, Object Student){
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("【返回通知】方法名:" + methodName + ",返回值:" + Student);
}
[INFO ] [2025-12-10 21:55:53.748] [http-nio-9090-exec-7] [REQ-2ac8142014ae4f53af7a0d71652071cf] o.araby.blognovelink.aop.LogAspect - 【返回通知】方法名:findById,返回值:Student(id=1, name=巢治文, age=28, sex=男, classNo=eu, createTime=2025-11-08, updateTime=2025-11-08)
3.2.3 異常通知-@AfterThrowing
AfterThrowing注解多了一個throwing屬性,當目標方法執(zhí)行異常觸發(fā)。
/**
* 異常通知
* @param joinPoint
* @param e
*/
@AfterThrowing(value = "servicePointCut()",throwing = "e")
public void afterThrowingAdvice(JoinPoint joinPoint,Exception e){
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("【異常通知】方法名:" + methodName + ",異常信息:" + e.getMessage());
}
[INFO ] [2025-12-10 22:01:26.235] [http-nio-9090-exec-3] [REQ-33bd8436c6f5495bad13d924c7be1887] o.araby.blognovelink.aop.LogAspect - 【異常通知】方法名:findById,異常信息:發(fā)生異常
3.2.4 后置通知-@After
后置通知的方法內(nèi)部包含joinPoint屬性,用于獲取連接點信息。該邏輯是在方法執(zhí)行后觸發(fā)。
/**
* 后置通知
* @param joinPoint
*/
@After("servicePointCut()")
public void afterAdvice(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("【后置通知】方法名:" + methodName + ",執(zhí)行結(jié)束");
}
3.2.5 環(huán)繞通知-@Around
環(huán)繞通知是這幾個通知中最為強大的,因為它可以在方法執(zhí)行的全部生命周期中應(yīng)用。環(huán)繞通知方法有一個核心的ProceedingJoinPoint參數(shù),在我們調(diào)用proceedingJoinPoint.proceed()的時候才實際執(zhí)行目標方法,獲取返回值就相當于實現(xiàn)了返回通知的效果。在此之前是前置通知邏輯,再次之后是后置通知邏輯。如果對proceedingJoinPoint.proceed()編寫異常捕捉,那就是實現(xiàn)了異常通知。
比方說這里我們通過環(huán)繞通知實現(xiàn)了一個記錄方法執(zhí)行時間的邏輯。
/**
* 環(huán)繞通知:記錄方法執(zhí)行時間
* @param proceedingJoinPoint 連接點(目標方法)
* @return 目標方法的返回值
* @throws Throwable 目標方法拋出的異常
*/
@Around("servicePointCut()")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
String methodName = proceedingJoinPoint.getSignature().getName();
String className = proceedingJoinPoint.getTarget().getClass().getName();
Object[] args = proceedingJoinPoint.getArgs();
Object result = null;
// 開始時間
long startTime = System.nanoTime();
try {
// 執(zhí)行目標方法
result = proceedingJoinPoint.proceed();
} catch (Exception e) {
log.info("【方法執(zhí)行異?!款悾簕},方法:{},參數(shù):{},異常信息:{}",
className, methodName, Arrays.toString(args), e.getMessage());
throw e;
} finally {
// 結(jié)束時間
long endTime = System.nanoTime();
long durationNano = endTime - startTime; // 納秒耗時
double durationMs = durationNano / 1_000_000.0; // 轉(zhuǎn)換為毫秒
log.info("【方法執(zhí)行時間】類:{},方法:{},參數(shù):{},執(zhí)行耗時:{}毫秒",
className, methodName, Arrays.toString(args),durationMs);
}
return result;[INFO ] [2025-12-10 22:17:25.036] [http-nio-9090-exec-4] [REQ-1e52d539f11344509218706010f1e829] o.araby.blognovelink.aop.LogAspect - 【方法執(zhí)行時間】類:org.araby.blognovelink.service.impl.StudentServiceImpl,方法:findById,參數(shù):[1],執(zhí)行耗時:50.0671毫秒
四、Spring AOP的實現(xiàn)原理
Spring AOP的底層實現(xiàn)依賴于動態(tài)代理技術(shù),主要有兩種代理方式:JDK動態(tài)代理和CGLIB動態(tài)代理。Spring會根據(jù)目標對象的類型自動選擇合適的代理方式。
若目標對象實現(xiàn)了接口,Spring默認使用JDK動態(tài)代理,若目標對象沒有實現(xiàn)接口,Spring會使用CGLIB動態(tài)代理。
application.yml添加配置spring.aop.proxy-target-class=true,可以強制使用CGLIB代理
4.1 JDK動態(tài)代理
JDK動態(tài)代理是基于Java反射機制實現(xiàn)的,它要求目標對象必須實現(xiàn)一個或多個接口。Spring通過java.lang.reflect.Proxy類創(chuàng)建代理對象,代理對象會實現(xiàn)目標對象的所有接口,并重寫接口中的方法,在重寫的方法中織入切面邏輯。也就是目標對象必須實現(xiàn)接口,否則無法使用。
4.2 CGLIB動態(tài)代理
CGLIB(Code Generation Library)是一個第三方字節(jié)碼生成庫,它通過繼承目標對象的方式創(chuàng)建代理對象,生成目標對象的子類,并重寫子類中的方法,織入切面邏輯。因此,CGLIB不要求目標對象實現(xiàn)接口,即使是沒有實現(xiàn)任何接口的類,也能通過CGLIB生成代理。由于是基于繼承實現(xiàn)的,所以目標對象的final方法無法被代理。
五、多個Spring AOP的執(zhí)行順序
多個AOP組合在一起,若是匹配到了相同的目標方法,可以通過@Order注解指定切面的執(zhí)行順序,值越小,優(yōu)先級越高。我們要把這種優(yōu)先級當作先進后出的棧結(jié)構(gòu)。假設(shè)c的優(yōu)先級高于b,b的優(yōu)先級高于a。那么c的前置邏輯最先,后置邏輯最后。
前置通知、環(huán)繞通知的前置邏輯按@Order值從小到大執(zhí)行,Order值越小越先執(zhí)行;后置通知、返回通知、異常通知、環(huán)繞通知的后置邏輯按@Order值從大到小執(zhí)行,Order值越小越后執(zhí)行.

[INFO ] [2025-12-10 22:29:21.568] [http-nio-9090-exec-1] [REQ-ebd5296f7aaa4225bcea5f87a4008007] o.a.blognovelink.aop.AnotherAspect - 【另一個】【前置通知】方法名:findById,參數(shù):[1] [INFO ] [2025-12-10 22:29:21.568] [http-nio-9090-exec-1] [REQ-ebd5296f7aaa4225bcea5f87a4008007] o.araby.blognovelink.aop.LogAspect - 【前置通知】方法名:findById,參數(shù):[1] [INFO ] [2025-12-10 22:29:21.618] [http-nio-9090-exec-1] [REQ-ebd5296f7aaa4225bcea5f87a4008007] o.araby.blognovelink.aop.LogAspect - 【返回通知】方法名:findById,返回值:Student(id=1, name=巢治文, age=28, sex=男, classNo=eu, createTime=2025-11-08, updateTime=2025-11-08) [INFO ] [2025-12-10 22:29:21.618] [http-nio-9090-exec-1] [REQ-ebd5296f7aaa4225bcea5f87a4008007] o.araby.blognovelink.aop.LogAspect - 【后置通知】方法名:findById,執(zhí)行結(jié)束 [INFO ] [2025-12-10 22:29:21.618] [http-nio-9090-exec-1] [REQ-ebd5296f7aaa4225bcea5f87a4008007] o.araby.blognovelink.aop.LogAspect - 【方法執(zhí)行時間】類:org.araby.blognovelink.service.impl.StudentServiceImpl,方法:findById,參數(shù):[1],執(zhí)行耗時:49.6057毫秒 [INFO ] [2025-12-10 22:29:21.618] [http-nio-9090-exec-1] [REQ-ebd5296f7aaa4225bcea5f87a4008007] o.a.blognovelink.aop.AnotherAspect - 【另一個】【返回通知】方法名:findById,返回值:Student(id=1, name=巢治文, age=28, sex=男, classNo=eu, createTime=2025-11-08, updateTime=2025-11-08) [INFO ] [2025-12-10 22:29:21.618] [http-nio-9090-exec-1] [REQ-ebd5296f7aaa4225bcea5f87a4008007] o.a.blognovelink.aop.AnotherAspect - 【另一個】【后置通知】方法名:findById,執(zhí)行結(jié)束 [INFO ] [2025-12-10 22:29:21.618] [http-nio-9090-exec-1] [REQ-ebd5296f7aaa4225bcea5f87a4008007] o.a.blognovelink.aop.AnotherAspect - 【另一個】【方法執(zhí)行時間】類:org.araby.blognovelink.service.impl.StudentServiceImpl,方法:findById,參數(shù):[1],執(zhí)行耗時:50.2602毫秒
總結(jié)
本文詳細介紹了Spring AOP的核心概念、在Spring Boot中的使用方式、實現(xiàn)原理及多切面執(zhí)行順序,AOP作為面向?qū)ο缶幊痰难a充,通過橫切邏輯的分離,能有效簡化日志、事務(wù)、權(quán)限等功能的開發(fā),提升代碼的可維護性和復(fù)用性。在實際項目中,合理使用 AOP可以讓業(yè)務(wù)邏輯更專注于核心功能,同時實現(xiàn)橫切需求的統(tǒng)一管理。
到此這篇關(guān)于如何使用Spring AOP優(yōu)雅實現(xiàn)橫切邏輯復(fù)用的文章就介紹到這了,更多相關(guān)Spring AOP橫切邏輯復(fù)用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實現(xiàn)經(jīng)緯度坐標轉(zhuǎn)換的示例代碼
坐標系統(tǒng)是描述物質(zhì)存在的空間位置(坐標)的參照系,通過定義特定基準及其參數(shù)形式來實現(xiàn),下面我們就來看看如何使用Java實現(xiàn)經(jīng)緯度坐標轉(zhuǎn)換功能吧2025-07-07
基于SpringBoot和Leaflet的行政區(qū)劃地圖掩膜效果實戰(zhàn)教程
本文講解的是一種圖層級的掩膜,即使用行政區(qū)劃圖層來進行掩膜,使用場景為,用戶只需要在地圖頁面中展示目標行政區(qū)劃內(nèi)的影像信息,對于行政邊界外的影像,這篇文章主要介紹了基于SpringBoot和Leaflet的行政區(qū)劃地圖掩膜效果實戰(zhàn),需要的朋友可以參考下2024-05-05
Spring MVC中日期格式轉(zhuǎn)換的兩種實用方法
在開發(fā)基于 Spring MVC 的 Web 應(yīng)用時,日期格式的轉(zhuǎn)換是一個常見的需求,本文將詳細介紹 Spring MVC 中兩種日期格式轉(zhuǎn)換的方法,包括創(chuàng)建過程和最終的運行結(jié)果,需要的朋友可以參考下2025-08-08

