超實用的SpringAOP實戰之日誌記錄

渊渟岳發表於2024-11-17

本文主要以日誌記錄作為切入點,來講解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();
    }

}

測試結果:

image

這是最簡單的日誌記錄,主要目的是理解整個程式碼是怎樣的。

案例二:交易日誌記錄

本案例完成切面類根據外部傳進來的引數實現動態日誌的記錄。

切面獲取外部資訊的一些方法:

  • 獲取目標方法(連線點)的引數: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");
    }

}

測試結果

成功的情況

image

失敗的情況

image

總結

使用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 的核心優勢在於將這些橫切功能從核心業務邏輯中提取出來,從而實現程式碼的解耦複用,提升系統的可維護性擴充套件性

image

軟考中級--軟體設計師毫無保留的備考分享

單例模式及其思想

2023年下半年軟考考試重磅訊息

透過軟考後卻領取不到實體證書?

計算機演算法設計與分析(第5版)

Java全棧學習路線、學習資源和麵試題一條龍

軟考證書=職稱證書?

什麼是設計模式?

相關文章