本文主要以日誌記錄
作為切入點,來講解Spring AOP在實際專案開發中怎樣更好的使專案業務程式碼更加簡潔、開發更加高效。
日誌處理只是AOP其中一種應用場景,當你掌握了這一種場景,對於其它應用場景也可以遊刃有餘的應對。
AOP常見的應用場景有:日誌記錄、異常處理、效能監控、事務管理、安全控制、自定義驗證、快取處理等等。文末會簡單列舉一下。
在看完本文(日誌記錄場景)後,可以練習其它場景如何實現。應用場景萬般種,萬變不離其中,掌握其本質最為重要。
使用場景的本質是:在一個方法
的執行前、執行後、執行異常和執行完成狀態下,都可以做一些統一的操作
。AOP 的核心優勢在於將這些橫切功能從核心業務邏輯中提取出來,從而實現程式碼的解耦和複用,提升系統的可維護性和擴充套件性。
案例一:簡單日誌記錄
本案例主要目的是為了理解整個AOP程式碼是怎樣編寫的。
引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
自定義一個註解類
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 日誌記錄註解:作用於方法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RecordLog {
String value() default "";
}
編寫一個切面類Aspect
AOP切面有多種通知方式:@Before、@AfterReturning、@AfterThrowing、@After、@Around。因為@Around包含前面四種情況,本文案例都只使用@Around,其它可自行了解。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class RecordLogAspect {
// 指定自定義註解為切入點
@Around("@annotation(org.example.annotations.RecordLog)")
public void around(ProceedingJoinPoint proceedingJoinPoint){
try {
System.out.println("日誌記錄--執行前");
proceedingJoinPoint.proceed();
System.out.println("日誌記錄--執行後");
} catch (Throwable e) {
// e.printStackTrace();
System.out.println("日誌記錄--執行異常");
}
System.out.println("日誌記錄--執行完成");
}
}
編寫一個Demo方法
import org.example.annotations.RecordLog;
import org.springframework.stereotype.Component;
@Component
public class RecordLogDemo {
@RecordLog
public void simpleRecordLog(){
System.out.println("執行當前方法:"+Thread.currentThread().getStackTrace()[1].getMethodName());
// 測試異常情況
// int a = 1/0;
}
}
進行單元測試
import org.example.demo.RecordLogDemo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringDemoAOPApplicationTests {
@Autowired
private RecordLogDemo recordLogDemo;
@Test
void contextLoads() {
System.out.println("Test...");
recordLogDemo.simpleRecordLog();
}
}
測試結果:
這是最簡單的日誌記錄,主要目的是理解整個程式碼是怎樣的。
案例二:交易日誌記錄
本案例完成切面類根據外部傳進來的引數實現動態日誌的記錄。
切面獲取外部資訊的一些方法:
-
獲取目標方法(連線點)的引數:
JoinPoint
類下的getArgs()
方法,或ProceedingJoinPoint
類下的getArgs()
方法 -
獲取自定義註解中的引數:自定義註解可以定義多個引數、必填引數、預設引數等
-
透過丟擲異常來傳遞業務處理情況,切面透過捕獲異常來記錄異常資訊
也可以根據方法引數的名稱去校驗是否是你想要的引數 String[] paramNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
切面獲取內部資訊:比如獲取執行前後的時間。
調整後的程式碼如下
新增了列舉類
public enum TransType {
// 轉賬交易型別
TRANSFER,
// 查詢交易型別
QUERYING;
}
自定義註解類
註解多了必填的交易型別和選填的交易說明
import org.example.enums.TransType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 日誌記錄註解:作用於方法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RecordLog {
// 必填的交易型別
TransType transType() ;
// 選填的交易說明
String description() default "";
}
切面類
新增了開頭所說的三種獲取外部資訊的程式碼
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.example.annotations.RecordLog;
import org.example.enums.TransType;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Component
public class RecordLogAspect {
// 指定自定義註解為切入點
@Around("@annotation(org.example.annotations.RecordLog)")
public Object around(ProceedingJoinPoint proceedingJoinPoint){
// 1.獲取目標方法(連線點)的引數資訊
Object[] args = proceedingJoinPoint.getArgs();
// 獲取特定型別的引數:根據具體情況而定
for (Object arg : args) {
if (arg instanceof String) {
System.out.println("日誌記錄--執行前:方法請求引數資訊記錄: " + arg);
}
}
// 2.獲取自定義註解中的引數
MethodSignature methodSignature = (MethodSignature)proceedingJoinPoint.getSignature();
Method method = methodSignature.getMethod();
RecordLog annotation = method.getAnnotation(RecordLog.class);
// 交易型別
TransType transType = annotation.transType();
// 交易描述資訊
String description = annotation.description();
try {
System.out.println("日誌記錄--執行前:註解引數資訊記錄:"+transType+"|"+description+"|");
Object proceed = proceedingJoinPoint.proceed();
System.out.println("日誌記錄--執行後:"+proceed.toString());
// 只要沒異常,那就是執行成功
System.out.println("日誌記錄--執行成功:200");
return proceed;
} catch (Throwable e) {
// e.printStackTrace();
// 3.捕獲異常來記錄異常資訊
String errorMessage = e.getMessage();
System.out.println("日誌記錄--執行異常: "+errorMessage);
throw new Exception("日誌記錄--執行異常: ").initCause(e);
} finally {
System.out.println("日誌記錄--執行完成");
};
}
}
新增了業務相關類
public class TransInfoBean {
private String transStatusCode;
private String transResultInfo;
private String account;
private BigDecimal transAmt;
public String getTransStatusCode() {
return transStatusCode;
}
public void setTransStatusCode(String transStatusCode) {
this.transStatusCode = transStatusCode;
}
public String getTransResultInfo() {
return transResultInfo;
}
public void setTransResultInfo(String transResultInfo) {
this.transResultInfo = transResultInfo;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public BigDecimal getTransAmt() {
return transAmt;
}
public void setTransAmt(BigDecimal transAmt) {
this.transAmt = transAmt;
}
}
業務Demo類
加入了外部引數註解、方法引數和異常
,只要交易失敗都要丟擲異常。
import org.example.annotations.RecordLog;
import org.example.enums.TransType;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
@Component
public class RecordLogDemo {
@RecordLog(transType = TransType.QUERYING,description = "查詢賬戶剩餘多少金額")
public BigDecimal queryRecordLog(String account) throws Exception {
System.out.println("--執行當前方法:"+Thread.currentThread().getStackTrace()[1].getMethodName());
try{
// 執行查詢操作:這裡只是模擬
TransInfoBean transInfoBean = this.queryAccountAmt(account);
BigDecimal accountAmt = transInfoBean.getTransAmt();
System.out.println("--查詢到的賬戶餘額為:"+accountAmt);
return accountAmt;
}catch (Exception e){
throw new Exception("查詢賬戶餘額異常:"+e.getMessage());
}
}
/**
* 呼叫查詢交易
* @param account
* @return TransInfoBean
*/
private TransInfoBean queryAccountAmt(String account) throws Exception {
TransInfoBean transInfoBean = new TransInfoBean();
transInfoBean.setAccount(account);
try{
// 呼叫查詢交易
// int n = 1/0;
transInfoBean.setTransAmt(new BigDecimal("1.25"));
//交易成功:模擬交易介面返回來的狀態
transInfoBean.setTransStatusCode("200");
transInfoBean.setTransResultInfo("成功");
}catch (Exception e){
//交易成功:模擬交易介面返回來的狀態
transInfoBean.setTransStatusCode("500");
transInfoBean.setTransResultInfo("失敗");
throw new Exception(transInfoBean.getTransStatusCode()+"|"+transInfoBean.getTransResultInfo());
}
return transInfoBean;
}
}
單元測試
@SpringBootTest
class SpringDemoAOPApplicationTests {
@Autowired
private RecordLogDemo recordLogDemo;
@Test
void contextLoads() throws Exception {
System.out.println("Test...");
recordLogDemo.queryRecordLog("123567890");
}
}
測試結果
成功的情況
失敗的情況
總結
使用AOP後,交易處理的日誌就不需要和業務程式碼交織在一起,起到解耦作用,提高程式碼可讀性和可維護性。
其次是程式碼的擴充套件性問題,比如後面要開發轉賬交易
,後面只管寫業務程式碼,打上日誌記錄註解即可完成日誌相關程式碼@RecordLog(transType = TransType.TRANSFER,description = "轉賬交易")
。如果一個專案幾十個交易介面需要編寫,那這日誌記錄就少寫了幾十次,大大的提高了開發效率。
這個案例只是講解了日誌記錄,如果將輸出的日誌資訊存到一個物件,並儲存到資料庫
,那就可以記錄所有交易的處理情況了。把日誌記錄處理換成你所需要做的操作即可。
常用場景簡述
事務管理
Spring AOP 提供了 @Transactional
註解來簡化事務管理,底層是透過 AOP 實現的。透過宣告式事務管理,可以根據方法的執行情況自動提交或回滾事務。
例如:在方法執行之前開啟事務,執行後提交事務,出現異常時回滾事務。
@Transactional
public void transferMoney(String fromAccount, String toAccount, double amount) {
accountService.debit(fromAccount, amount);
accountService.credit(toAccount, amount);
}
可以自定義事務管理切面,也可以同時相容@Transactional
事務管理註解。執行目標方法前開啟事務,執行異常時回滾事務,執行正常時可以不用處理。
@Aspect
@Component
public class TransactionAspect {
@Before("execution(* com.example.service.*.*(..)) && @annotation(org.springframework.transaction.annotation.Transactional)")
public void beforeTransaction(JoinPoint joinPoint) {
System.out.println("Starting transaction for method: " + joinPoint.getSignature().getName());
}
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..)) && @annotation(org.springframework.transaction.annotation.Transactional)", throwing = "exception")
public void transactionFailure(Exception exception) {
System.out.println("Transaction failed: " + exception.getMessage());
}
}
效能監控
AOP 可以用來監控方法的執行效能(如執行時間、頻率等),特別適合用於系統的效能分析和最佳化。
示例:
- 計算方法執行時間,並記錄日誌。
- 監控方法的呼叫頻率。
@Aspect
@Component
public class PerformanceMonitorAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
// 執行目標方法
Object result = joinPoint.proceed();
long elapsedTime = System.currentTimeMillis() - start;
System.out.println("Method " + joinPoint.getSignature().getName() + " executed in " + elapsedTime + " ms");
return result;
}
}
安全控制
AOP 適用於實現方法級別的安全控制。例如,你可以在方法呼叫之前檢查使用者的許可權,決定是否允許訪問。
示例:
- 校驗使用者是否具有某個操作的許可權。
- 使用註解
@Secured
或自定義註解,基於角色或許可權進行安全驗證。
@Aspect
@Component
public class SecurityAspect {
@Before("@annotation(com.example.security.Secured)")
public void checkSecurity(Secured secured) {
// 獲取當前使用者的許可權
String currentUserRole = getCurrentUserRole();
if (!Arrays.asList(secured.roles()).contains(currentUserRole)) {
throw new SecurityException("Insufficient permissions");
}
}
}
快取管理
AOP 可以用於方法結果的快取。對於一些耗時較長的方法,可以使用 AOP 來在第一次呼叫時執行計算,後續的呼叫則直接從快取中獲取結果,從而提高效能。
示例:使用 AOP 實現方法結果快取,避免重複計算。
@Aspect
@Component
public class CachingAspect {
private Map<String, Object> cache = new HashMap<>();
@Around("execution(* com.example.service.*.get*(..))")
public Object cacheResult(ProceedingJoinPoint joinPoint) throws Throwable {
String key = joinPoint.getSignature().toShortString();
if (cache.containsKey(key)) {
return cache.get(key); // 從快取中獲取
}
// 執行目標方法並快取結果
Object result = joinPoint.proceed();
cache.put(key, result);
return result;
}
}
異常處理
AOP 可以統一處理方法中的異常,比如記錄日誌、傳送警報或執行其他處理。可以透過 @AfterThrowing
或 @Around
註解來實現異常的捕獲和處理。
示例:統一捕獲異常並記錄日誌或傳送通知。
@Aspect
@Component
public class ExceptionHandlingAspect {
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "exception")
public void handleException(Exception exception) {
System.out.println("Exception caught: " + exception.getMessage());
// 傳送郵件或日誌記錄
}
}
自定義驗證
AOP 可以用於方法引數的驗證,尤其在輸入資料的校驗上。在方法呼叫之前進行引數驗證,避免無效資料的傳入。
示例:校驗方法引數是否為空或符合特定規則,比如密碼格式校驗
@Aspect
@Component
public class ValidationAspect {
// 正規表示式
private static final String PASSWORD_PATTERN =
"^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)(?=.*[!@#$%^&*(),.?:{}|<>_]).{8,16}$";
@Before("execution(* org.example.demo.UserService.createUser(..))")
public void validateUserInput(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
for(Object arg : args){
// 型別檢查
if(arg instanceof UserService){
UserService userService = (UserService) arg;
// 然後再校驗物件屬性的值:是否為空、是否不符合格式要求等等,
// 比如密碼校驗
if(!validatePassword(userService.getPassword())){
// 不符合就丟擲異常即可
throw new IllegalArgumentException("Invalid user input");
}
}
}
}
/**
* 密碼校驗:8~16為,要有大小學字母+數字+特殊字元
* @param password String
* @return boolean
*/
public static boolean validatePassword(String password) {
if(password == null)
return false;
return Pattern.matches(PASSWORD_PATTERN, password);
}
}
總結
應用場景萬般種,萬變不離其中,掌握其本質最為重要。
使用場景的本質是:在一個方法
的執行前、執行後、執行異常和執行完成狀態下,都可以做一些統一的操作
。AOP 的核心優勢在於將這些橫切功能從核心業務邏輯中提取出來,從而實現程式碼的解耦
和複用
,提升系統的可維護性
和擴充套件性
。
軟考中級--軟體設計師毫無保留的備考分享
單例模式及其思想
2023年下半年軟考考試重磅訊息
透過軟考後卻領取不到實體證書?
計算機演算法設計與分析(第5版)
Java全棧學習路線、學習資源和麵試題一條龍
軟考證書=職稱證書?
什麼是設計模式?