這篇文章我將通過一個最常用的AOP場景-方法調用日志記錄,帶你徹底理解AOP的使用。例子使用Spring Boot+Spring AOP實現。
如果對你有幫助可以點個贊和關注。謝謝大家的支持!!
一、Demo實操步驟:
1.首先添加Maven依賴
<!-- Spring AOP支持 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.創建日志切面類
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;@Aspect // 標識這是一個切面類
@Component // 讓Spring能夠掃描到這個組件
public class LoggingAspect {// 創建日志記錄器private final Logger logger = LoggerFactory.getLogger(this.getClass());/*** 定義切點:攔截service包下的所有方法* execution(返回值類型 包名.類名.方法名(參數列表))*/@Pointcut("execution(* com.example.demo.service.*.*(..))")public void serviceLayer() {}/*** 前置通知:在目標方法執行前執行*/@Before("serviceLayer()")public void logBefore(JoinPoint joinPoint) {// joinPoint包含目標方法的信息logger.info("準備執行 {} 方法", joinPoint.getSignature().getName());logger.info("參數: {}", Arrays.toString(joinPoint.getArgs()));}/*** 后置通知:在目標方法正常返回后執行*/@AfterReturning(pointcut = "serviceLayer()", returning = "result")public void logAfterReturning(JoinPoint joinPoint, Object result) {logger.info("方法 {} 執行成功,返回值: {}", joinPoint.getSignature().getName(), result);}/*** 異常通知:在目標方法拋出異常后執行*/@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {logger.error("方法 {} 執行異常: {}", joinPoint.getSignature().getName(), ex.getMessage());}/*** 環繞通知:最強大的通知類型,可以控制方法執行*/@Around("serviceLayer()")public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {long start = System.currentTimeMillis();logger.info("進入方法: {}", joinPoint.getSignature().getName());try {// 執行目標方法Object result = joinPoint.proceed();long elapsedTime = System.currentTimeMillis() - start;logger.info("方法執行完成,耗時: {} ms", elapsedTime);return result;} catch (Exception e) {long elapsedTime = System.currentTimeMillis() - start;logger.error("方法執行異常,耗時: {} ms,異常: {}", elapsedTime, e.getMessage());throw e;}}
}
3.創建測試服務類
@Service
public class UserService {public String getUserById(Long id) {// 模擬數據庫查詢if (id == 1) {return "用戶張三";} else if (id == 2) {return "用戶李四";}throw new RuntimeException("用戶不存在");}public void updateUser(String user) {// 模擬更新操作System.out.println("更新用戶: " + user);}
}
4.測試Controller
@RestController
public class UserController {@Autowiredprivate UserService userService;@GetMapping("/user/{id}")public String getUser(@PathVariable Long id) {return userService.getUserById(id);}@PostMapping("/user")public void updateUser(@RequestBody String user) {userService.updateUser(user);}
}
二、代碼講解:
1.切面(Aspect):
LoggingAspect類 就是一個切面,它封裝了橫切關注點(這里是日志記錄)。
2.切點(Pointcut):
@Pointcut("execution(* com.example.demo.service.*.*(..))")
- 定義了"哪些方法需要被攔截"
- execution是匹配方法執行的連接點
- *表示任意返回值
- com.example.demo.service指定包名
- 第一個*表示所有類
- 第二個*表示所有方法
- (. .)表示任意參數
3.通知(Advice):
- @Before:方法執行前
- @AfterReturning:方法正常返回后
- @AfterThrowing:方法拋出異常后
- @Around:最強大,可以控制方法是否執行
三、執行流程演示:
場景1:調用GET/user/1
1.請求進入UserController.getUser(1)
2.調用userService.getUserById(1)時被AOP攔截
3.執行順序:
- @Around的開始部分(記錄開始時間)
- @Before(記錄方法準備執行)
- 實際執行getUserById方法
- @AfterReturning(記錄成功返回)
- @Around的結束部分(計算耗時)
控制臺輸出 執行結果:
進入方法: getUserById
準備執行 getUserById 方法
參數: [1]
方法 getUserById 執行成功,返回值: 用戶張三
方法執行完成,耗時: 12 ms
場景2.調用GET/user/3(會拋出異常)
1.請求進入UserController.getUser(3)
2.調用userService.getUserById(3)時被AOP攔截
3.執行順序:
- @Around的開始部分
- @Before
- 執行getUserById拋出異常
- @AfterThrowing(記錄異常信息)
- @Around的異常處理部分
控制臺輸出 執行結果:
進入方法: getUserById
準備執行 getUserById 方法
參數: [3]
方法 getUserById 執行異常: 用戶不存在
方法執行異常,耗時: 5 ms,異常: 用戶不存在
四、為什么要用AOP?
對比傳統寫法,如果不用AOP,我們需要在每個方法中寫日志代碼,重復代碼太多,顯得冗雜。
@Service
public class UserService {private final Logger logger = LoggerFactory.getLogger(this.getClass());public String getUserById(Long id) {long start = System.currentTimeMillis();logger.info("準備執行 getUserById 方法");logger.info("參數: " + id);try {if (id == 1) {String result = "用戶張三";logger.info("方法執行成功,返回值: " + result);return result;}throw new RuntimeException("用戶不存在");} catch (Exception e) {logger.error("方法執行異常: " + e.getMessage());throw e;} finally {long elapsedTime = System.currentTimeMillis() - start;logger.info("方法耗時: " + elapsedTime + " ms");}}
}
此時就可以使用AOP來解決。
AOP的優勢:
1.消除重復代碼:日志邏輯只寫一次,應用到所有方法。
2.業務更純凈:業務方法只關注核心邏輯。
3.維護方便:修改日志格式只需改切面類。
4.靈活擴展:可以隨時添加新的橫切邏輯(如權限檢查)。
五、應用場景:
1. 統一日志記錄(如上面的例子)
2.性能監控:統計方法執行時間
@Around("serviceLayer()")
public Object monitorPerformance(ProceedingJoinPoint jp) throws Throwable {long start = System.currentTimeMillis();Object result = jp.proceed();long elapsed = System.currentTimeMillis() - start;if (elapsed > 500) { // 超過500ms記錄警告logger.warn("方法 {} 執行耗時 {} ms", jp.getSignature(), elapsed);}return result;
}
3.事務管理(Spring的@Transactional就是基于AOP實現的)
4.權限控制:
@Before("@annotation(requiresAuth)")
public void checkAuth(JoinPoint jp, RequiresAuth requiresAuth) {if (!SecurityUtils.hasRole(requiresAuth.value())) {throw new AccessDeniedException("無權限訪問");}
}
5.緩存處理:
@Around("@annotation(cacheable)")
public Object handleCache(ProceedingJoinPoint jp, Cacheable cacheable) throws Throwable {String key = generateKey(jp, cacheable);Object cached = cache.get(key);if (cached != null) return cached;Object result = jp.proceed();cache.put(key, result);return result;
}
六、小白常見問題:
1.切面不生效?
- 確保切面類有 @Component 注解。
- 確保啟動類有 @EnableAspectJAutoProxy(Spring Boot自動配置)。
- 檢查切點表達式是否正確匹配到目標方法。
2.執行順序問題:
- 多個切面同一個切點,可以用 @Order注解指定順序。
- 單個切面中通知執行順序:Around前 → Before → 方法執行 → Around后 → AfterReturning/AfterThrowing → After
3.自調用問題:
- 同一個類內部方法調用不會觸發AOP(因為不是代理對象調用)
- 解決方案:從Spring容器獲取代理對象調用
七、總結:
通過這篇文章,你應該理解了:
1.AOP如何通過切面、切點、通知等概念工作。
2.為什么AOP能解決代碼重復的問題。
3.如何在Spring項目中實際使用AOP。
4.AOP的各種實際應用場景。
AOP就像是一個"方法攔截器",在不修改原有代碼的情況下,給方法添加各種增強功能。這是Spring框架的核心特性之一,掌握后能大幅提高代碼質量和開發效率。