1. 程式人生 > 其它 >【Spring】AOP實現原理(一):AOP基礎知識

【Spring】AOP實現原理(一):AOP基礎知識

AOP相關概念

在學習AOP實現原理之前,先了解下AOP相關基礎知識。

AOP面向切面程式設計,它可以通過預編譯方式或者基於動態代理對我們編寫的程式碼進行攔截(也叫增強處理),在方法執行前後可以做一些操作,一般我們會看到以下幾個概念:

連線點(JointPoint): AOP進行切入的位置稱為連線點,一般指程式中的某個方法,對該方法進行攔截

通知(Advice): 在某個連線點執行的操作稱為通知,也就是被攔截方法執行前後需要執行的操作稱為通知,一共有五種

  • 前置通知:作用於被攔截方法執行之前
  • 後置通知:作用於被攔截方法執行之後進行的操作,無論被攔截方法是否丟擲異常都會執行
  • 環繞通知:作用於被攔截方法執行之前和執行之後
  • 返回通知:作用於被攔截方法正常執行完畢返回時,如果丟擲異常將不會執行
  • 異常通知:作用於被攔截方法丟擲異常時

切點(Pointcut): 切點作用在於讓程式知道需要在哪個連線點(方法)上執行通知,所以它也可以是一個表示式,匹配所有需要攔截的方法。

切面(Aspect): 切點通知共同組成了切面,其中切點定義了需要在哪些連線點上執行通知,通知裡面定義了具體需要進行的操作

織入(Weaving):將切面連線到應用程式型別或者物件上,建立一個被通知的物件(advised object)的過程稱為織入,換句話說織入就是將切面應用到目標物件的過程,它可以在編譯期時(使用AspectJ)、載入時或者在執行時實現

,Spring AOP是在執行時基於動態代理實現的。

Spring AOP和AspectJ區別

Spring AOP

Spring AOP是基於動態代理實現攔截功能的,預設使用JDK動態代理實現,當然這需要目標物件實現介面,如果目標物件沒有實現介面,則使用CGLIB生成代理物件。

AspectJ

AspectJ提供了三種方式實現AOP:

  • 編譯時織入:在編譯期間將程式碼進行織入到目標類的class檔案中。

  • 編譯後織入:在編譯後將程式碼織入到目標類的class檔案中。

  • 載入時織入:在JVM載入class檔案的時候進行織入。

Spring AOP的應用

瞭解了AOP相關知識後我們來實現一個需求:

  1. 自定義一個日誌註解MyLogger
  2. 對使用了MyLogger註解的方法進行攔截,在方法的執行前後分別進行一些操作(環繞通知):
    • 方法執行前列印方法傳入的引數
    • 方法執行後列印方法的返回值

自定義註解

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLogger {

}
定義切面Aspect

這裡使用註解@Aspect來標記這是一個切面,切面是切點和通知的集合,分別使用註解@Pointcut和@Around實現。

@Slf4j
@Aspect // 使用註解定義切面
@Component
@EnableAspectJAutoProxy
public class MyLogAspect {

}
切點Pointcut

使用表示式@annotation(com.demo.mybatis.annotation.MyLogger)匹配所有使用了@MyLogger註解的方法。

    /**
     * 定義切點,匹配所有使用了@MyLogger註解的方法
     */
    @Pointcut("@annotation(com.example.annotation.MyLogger)") // 這裡傳入MyLogger的全路徑
    public void logPoiontcut() {
      
    }
通知Advice

定義一個logAroudAdvice方法,使用@Around註解標記這是一個環繞通知,logPoiontcut()引用了切點,表示通知要作用於哪些連線點上,該方法需要傳入一個ProceedingJoinPoint型別引數(連線點):

    /**
     * 通知Advice,這裡使用了環繞通知
     * @param joinPoint 連線點
     * @return
     * @throws Throwable
     */
    @Around("logPoiontcut()") // 引用切點
    public Object logAroudAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        // 方法執行前的日誌列印
        printBeforeLog(joinPoint);
        // 執行方法
        Object returnValue = joinPoint.proceed();
        // 方法執行後的日誌列印
        printAfterLog(returnValue);
        return returnValue;
    }

完整的切面如下:

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Slf4j
@Aspect // 使用註解定義切面
@Component
@EnableAspectJAutoProxy
public class MyLogAspect {

    /**
     * 定義切點,匹配所有使用了@MyLogger註解的方法
     */
    @Pointcut("@annotation(com.example.annotation.MyLogger)") 
    public void logPoiontcut() {
    }

    /**
     * 通知Advice,這裡使用了環繞通知
     * @param joinPoint 連線點
     * @return
     * @throws Throwable
     */
    @Around("logPoiontcut()") // 引用切點
    public Object logAroudAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        // 方法執行前的日誌列印
        printBeforeLog(joinPoint);
        // 執行方法
        Object returnValue = joinPoint.proceed();
        // 方法執行後的日誌列印
        printAfterLog(returnValue);
        return returnValue;
    }

    /**
     * 方法執行前的日誌列印
     * @param joinPoint
     */
    public void printBeforeLog(ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 獲取方法
        Method method = methodSignature.getMethod();
        log.info("開始執行方法:{}", method.getName());
        // 獲取引數
        Object[] args = joinPoint.getArgs();
        if (args == null || args.length == 0) {
            return;
        }
        // 獲取引數名稱
        String[] parameterNames = methodSignature.getParameterNames();
        StringBuilder parameterBuilder = new StringBuilder();
        for (int i = 0; i < args.length; i++) {
            parameterBuilder.append(parameterNames[i]).append(":").append(args[i]);
            if (i < parameterNames.length - 1) {
                parameterBuilder.append(",");
            }
        }
        log.info("方法引數【{}】", parameterBuilder.toString());
    }

    /**
     * 方法執行後的日誌列印
     * @param returnValue
     */
    public void printAfterLog(Object returnValue) {
        log.info("方法返回值【{}】", returnValue == null ? null : returnValue.toString());
    }
}

測試

定義一個用於計算的Service,實現一個兩數相加的方法addTwoNum,並使用@MyLogger註解,對方法進行攔截,在方法執行前後列印相關日誌

@Slf4j
@Service
public class ComputeService {

    // 使用自定義日誌註解對方法進行攔截
    @MyLogger
    public Integer addTwoNum(Integer value1, Integer value2) {
        log.info("執行addTwoNum方法");
        return value1 + value2;
    }
}

編寫單元測試:

	@Autowired
	private ComputeService computeService;
	
	@Test
	public void testAddTwoNum {
		computeService.addTwoNum(1, 2);
	}

由於ComputeService沒有實現介面,可以看到Spring預設使用了CGLIB生成代理物件:

日誌輸出如下,可以看到方法執行前後列印了相關日誌:

開始執行方法:addTwoNum
方法引數【value1:1,value2:2】
執行addTwoNum方法
方法返回值【3】

參考

Spring官方文件

【 FatalFlower】AspectJ 簡介