深度解析Spring AOP @Aspect 原理、實戰(zhàn)與最佳實踐教程
1. @Aspect 核心概念
1.1 AOP 編程范式
- 核心思想:將橫切關(guān)注點(如日志、事務(wù)、安全)與業(yè)務(wù)邏輯分離
- 解決的問題:避免代碼中出現(xiàn)大量重復(fù)的"模板代碼"(如每個Service方法都寫事務(wù)控制)
1.2 @Aspect 關(guān)鍵特性
| 特性 | 說明 |
|---|---|
| 基于注解 | 比傳統(tǒng)XML配置更簡潔 |
| 代理機制 | 運行時生成代理對象(JDK動態(tài)代理/CGLIB) |
| 連接點模型 | 支持方法執(zhí)行、異常處理等多種切入點 |
2. 完整代碼實現(xiàn)解析
2.1 基礎(chǔ)切面結(jié)構(gòu)
@Aspect
@Component
@Order(1) // 控制多個切面的執(zhí)行順序
@Slf4j
public class LoggingAspect {
// 定義可重用的切入點表達式
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
@Before("serviceLayer()")
public void logMethodStart(JoinPoint jp) {
log.info("?? 調(diào)用 {}.{} 參數(shù): {}",
jp.getTarget().getClass().getSimpleName(),
jp.getSignature().getName(),
Arrays.toString(jp.getArgs()));
}
@Around("@annotation(com.example.audit.AuditLog)")
public Object auditLog(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed();
log.info("?? 審計日志 - 操作: {}, 耗時: {}ms",
pjp.getSignature().getName(),
System.currentTimeMillis() - start);
return result;
}
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void logException(JoinPoint jp, Exception ex) {
log.error("?? 方法 {} 拋出異常: {}",
jp.getSignature(), ex.getMessage());
}
}2.2 高級切面示例:接口限流
@Aspect
@Component
public class RateLimitAspect {
private final RateLimiter limiter = RateLimiter.create(100); // 100 QPS
@Around("@annotation(rateLimit)")
public Object limit(ProceedingJoinPoint pjp, RateLimit rateLimit)
throws Throwable {
if (!limiter.tryAcquire(rateLimit.timeout(), rateLimit.timeUnit())) {
throw new RateLimitException("請求過于頻繁");
}
return pjp.proceed();
}
}
// 自定義注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimit {
long timeout() default 1;
TimeUnit timeUnit() default TimeUnit.SECONDS;
}3. 核心原理深度剖析
3.1 代理機制對比
| 代理類型 | 條件 | 性能 | 限制 |
|---|---|---|---|
| JDK動態(tài)代理 | 目標實現(xiàn)接口 | 較高 | 只能代理接口方法 |
| CGLIB代理 | 無接口類 | 略低 | 無法代理final方法 |
代理選擇邏輯:
// Spring AbstractAutoProxyCreator 的核心邏輯
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return JdkDynamicAopProxy();
}
return ObjenesisCglibAopProxy();4. 生產(chǎn)環(huán)境最佳實踐
4.1 性能優(yōu)化方案
精確切入點匹配
// 不推薦(掃描范圍過大)
@Pointcut("execution(* *(..))")
// 推薦(限定包路徑+注解)
@Pointcut("execution(* com.yourpackage..service.*.*(..)) && " +
"@annotation(org.springframework.transaction.annotation.Transactional)")避免切面內(nèi)部耗時操作
@Around("serviceLayer()")
public Object profile(ProceedingJoinPoint pjp) throws Throwable {
// 錯誤示范:在切面內(nèi)進行數(shù)據(jù)庫操作
// auditRepository.save(...);
// 正確做法:只做輕量級記錄
long start = System.nanoTime();
Object result = pjp.proceed();
long duration = System.nanoTime() - start;
metrics.record(duration);
return result;
}4.2 事務(wù)切面特殊處理
@Aspect
@Component
public class TransactionRetryAspect {
@Around("@annotation(retry)")
public Object retry(ProceedingJoinPoint pjp, RetryOnConflict retry)
throws Throwable {
int attempts = 0;
do {
try {
return pjp.proceed();
} catch (OptimisticLockingFailureException ex) {
if (++attempts >= retry.maxAttempts()) throw ex;
Thread.sleep(retry.backoff());
}
} while (true);
}
}5. 常見陷阱與解決方案
5.1 自調(diào)用問題
public class OrderService {
public void createOrder() {
this.updateStock(); // 自調(diào)用不走代理!
}
@Transactional
public void updateStock() {...}
}解決方案:
通過ApplicationContext獲取代理對象
((OrderService) context.getBean("orderService")).updateStock();使用AopContext(需開啟exposeProxy)
((OrderService) AopContext.currentProxy()).updateStock();
5.2 循環(huán)依賴問題
現(xiàn)象:A切面依賴B服務(wù),B服務(wù)又需要被A切面代理
解決:
@DependsOn("bService") // 強制初始化順序
@Aspect
@Component
public class AAspect {
@Autowired
private BService bService;
}6. 監(jiān)控與調(diào)試技巧
6.1 查看生成的代理類
# application.properties spring.aop.proxy-target-class=true # 強制使用CGLIB logging.level.org.springframework.aop=DEBUG
6.2 切面執(zhí)行監(jiān)控
@Aspect
@Component
public class AopMonitorAspect {
@Around("within(@org.aspectj.lang.annotation.Aspect *)")
public Object monitorAspect(ProceedingJoinPoint pjp) throws Throwable {
String aspectName = pjp.getTarget().getClass().getSimpleName();
Monitor.start("aop.aspect." + aspectName);
try {
return pjp.proceed();
} finally {
Monitor.end();
}
}
}7. 進階:編譯時織入(AspectJ)
7.1 與Spring AOP對比
| 特性 | Spring AOP | AspectJ |
|---|---|---|
| 織入時機 | 運行時 | 編譯期/類加載期 |
| 性能 | 有代理開銷 | 無運行時損耗 |
| 能力 | 僅方法級別 | 支持字段、構(gòu)造器等 |
7.2 配置示例(Maven插件)
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>總結(jié)
- 正確使用場景:日志、監(jiān)控、緩存等非核心但全局的需求
- 避免濫用:業(yè)務(wù)規(guī)則校驗等核心邏輯應(yīng)放在Service層
- 性能關(guān)鍵點:切入點表達式精度、切面內(nèi)部復(fù)雜度
- 推薦組合:Spring AOP + 編譯時織入(關(guān)鍵路徑)
到此這篇關(guān)于深度解析Spring AOP @Aspect 原理、實戰(zhàn)與最佳實踐教程的文章就介紹到這了,更多相關(guān)Spring AOP @Aspect 原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javacv-ffmpeg ProcessBuilder批量旋轉(zhuǎn)圖片方式
為了批量處理大量圖片的旋轉(zhuǎn),可以使用javacv-ffmpeg結(jié)合ProcessBuilder,首先在maven配置文件中添加ffmpeg及javacpp依賴,javacpp支持調(diào)用C/C++方法,而ffmpeg基于C語言,使用ProcessBuilder創(chuàng)建進程調(diào)用ffmpeg方法2024-09-09
Java使用itext5實現(xiàn)生成多個PDF并合并
這篇文章主要為大家詳細介紹了Java如何使用itext5實現(xiàn)生成多個PDF并合并,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學習一下2025-04-04
java基于QuartzJobBean實現(xiàn)定時功能的示例代碼
QuartzJobBean是Quartz框架中的一個抽象類,用于定義和實現(xiàn)可由Quartz調(diào)度的作業(yè),本文主要介紹了java基于QuartzJobBean實現(xiàn)定時功能的示例代碼,具有一定的參考價值,感興趣可以了解一下2023-09-09
SpringBoot+BootStrap多文件上傳到本地實例
這篇文章主要介紹了SpringBoot+BootStrap多文件上傳到本地實例,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03
jquery對輸入框內(nèi)容的數(shù)字校驗代碼實例
這篇文章主要介紹了jquery對輸入框內(nèi)容的數(shù)字校驗代碼實例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2019-09-09
Java MyBatis返回兩個字段作為Map的key和value問題
使用MyBatis查詢兩個字段并返回Map時,需要注意數(shù)據(jù)量和值的類型,直接返回Map會導致報錯,使用@MapKey注解可以生成Map,但值是對象而不是直接值,為了解決這個問題,可以自定義一個Map結(jié)果處理器MapResultHandler2024-12-12
java實現(xiàn)圖像轉(zhuǎn)碼為字符畫的方法
這篇文章主要為大家詳細介紹了java實現(xiàn)圖像轉(zhuǎn)碼為字符畫的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-03-03

