Spring AOP核心概念

鏈接點 - Joinpoint切點 - Pointcut加強/通知 - Advice切面 - Aspect織入 - Weaving實例總結參考文獻html

在上一章節中咱們初步瞭解 Spring AOP,包括 Spring AOP 的基本概念以及使用,本文將對 AOP 核心概念進行解讀。java

鏈接點 - Joinpoint

鏈接點是指程序執行過程當中的一些點,好比方法調用,異常處理等。在 Spring AOP 中,僅支持方法級別的鏈接點。以上是官方說明,通俗地講就是可以被攔截的地方,每一個成員方法均可以稱之爲鏈接點。咱們仍是借用租房的案例作分析。web

Rent 接口:spring

public interface Rent {
    //租房
    public void rent(String address);
    //買傢俱
    public void buyFurniture();
}
複製代碼

該接口的實現類是 Host,即房東類,具體實現以下:express

@Component
public class Host implements Rent {

    @Override
    public void rent(String address) {
        System.out.println("出租位於"+address+"處的房屋");
    }

    @Override
    public void buyFurniture() {
        System.out.println("添置傢俱");
    }
}
複製代碼

其中 rent()方法即爲一個鏈接點。segmentfault

接下來咱們看看鏈接點的定義:api

public interface JoinPoint {
    String METHOD_EXECUTION = "method-execution";
    String METHOD_CALL = "method-call";
    String CONSTRUCTOR_EXECUTION = "constructor-execution";
    String CONSTRUCTOR_CALL = "constructor-call";
    String FIELD_GET = "field-get";
    String FIELD_SET = "field-set";
    String STATICINITIALIZATION = "staticinitialization";
    String PREINITIALIZATION = "preinitialization";
    String INITIALIZATION = "initialization";
    String EXCEPTION_HANDLER = "exception-handler";
    String SYNCHRONIZATION_LOCK = "lock";
    String SYNCHRONIZATION_UNLOCK = "unlock";
    String ADVICE_EXECUTION = "adviceexecution";

    String toString();

    String toShortString();

    String toLongString();

    //獲取代理對象
    Object getThis();

    /**
    返回目標對象。該對象將始終與target切入點指示符匹配的對象相同。除非您特別須要此反射訪問,不然應使用        target切入點指示符到達此對象,以得到更好的靜態類型和性能。

    若是沒有目標對象,則返回null。
    **/

    Object getTarget();

    //獲取傳入目標方法的參數對象
    Object[] getArgs();

    //獲取封裝了署名信息的對象,在該對象中能夠獲取到目標方法名,所屬類的Class等信息
    Signature getSignature();

    /**
    返回與鏈接點對應的源位置。
    若是沒有可用的源位置,則返回null。
    返回默認構造函數的定義類的SourceLocation。
    **/

    SourceLocation getSourceLocation();

    String getKind();

    JoinPoint.StaticPart getStaticPart();

    public interface EnclosingStaticPart extends JoinPoint.StaticPart {
    }

    //該幫助對象僅包含有關鏈接點的靜態信息。它能夠從JoinPoint.getStaticPart()方法中得到,也可使用特殊形式在建議中單獨訪問 thisJoinPointStaticPart。
    public interface StaticPart {
        Signature getSignature();

        SourceLocation getSourceLocation();

        String getKind();

        int getId();

        String toString();

        String toShortString();

        String toLongString();
    }
}
複製代碼

JoinPoint 接口中經常使用 api 有:getSignature()、 getArgs()、 getTarget() 、 getThis() 。可是咱們平時使用並不直接使用 JoinPoint 的實現類,中間還有一個接口實現,叫作 ProceedingJoinPoint,其定義以下:app

public interface ProceedingJoinPoint extends JoinPoint {
    void set$AroundClosure(AroundClosure var1);

    //執行目標方法
    Object proceed() throws Throwable;
    //傳入的新的參數去執行目標方法
    Object proceed(Object[] var1) throws Throwable;
}
複製代碼

ProceedingJoinPoint 對象是 JoinPoint 的子接口,該對象只用在@Around 的切面方法中。在該接口中,proceed 方法是核心,該方法用於執行攔截器邏輯。關於攔截器這裏說一下,之前置通知攔截器爲例,在執行目標方法前,該攔截器首先會執行前置通知邏輯,若是攔截器鏈中還有其餘的攔截器,則繼續調用下一個攔截器邏輯。直到攔截器鏈中沒有其餘的攔截器後,再去調用目標方法。ide

proceed() 方法的具體實如今 MethodInvocationProceedingJoinPoint 類中,其定義以下:函數

public Object proceed() throws Throwable {
    return this.methodInvocation.invocableClone().proceed();
}

public Object proceed(Object[] arguments) throws Throwable {
    Assert.notNull(arguments, "Argument array passed to proceed cannot be null");
    if (arguments.length != this.methodInvocation.getArguments().length) {
        throw new IllegalArgumentException("Expecting " + this.methodInvocation.getArguments().length + " arguments to proceed, but was passed " + arguments.length + " arguments");
    } else {
        this.methodInvocation.setArguments(arguments);
        return this.methodInvocation.invocableClone(arguments).proceed();
    }
}
複製代碼

查看代碼可知,arguments 參數被傳入到 ProxyMethodInvocation 對象中,並調用自身的 proceed()方法,接着咱們定位到此處進行查看相關代碼:

public MethodInvocation invocableClone(Object... arguments) {
    if (this.userAttributes == null) {
        this.userAttributes = new HashMap();
    }

    try {
        ReflectiveMethodInvocation clone = (ReflectiveMethodInvocation)this.clone();
        clone.arguments = arguments;
        return clone;
    } catch (CloneNotSupportedException var3) {
        throw new IllegalStateException("Should be able to clone object of type [" + this.getClass() + "]: " + var3);
    }
}

public Object proceed() throws Throwable {
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        return this.invokeJoinpoint();
    } else {
        Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
        if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
            InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher)interceptorOrInterceptionAdvice;
            Class<?> targetClass = this.targetClass != null ? this.targetClass : this.method.getDeclaringClass();
            return dm.methodMatcher.matches(this.method, targetClass, this.arguments) ? dm.interceptor.invoke(this) : this.proceed();
        } else {
            return ((MethodInterceptor)interceptorOrInterceptionAdvice).invoke(this);
        }
    }
}
複製代碼

如上所示,MethodInvocation 的實現類 ReflectiveMethodInvocation 獲取到傳入的參數以後,執行 proceed 方法,獲取到前置通知攔截器邏輯,而後經過反射進行調用。關於 ReflectiveMethodInvocation 類,又繼承自 JoinPoint 接口,因此咱們看一下這些定義之間的繼承關係:

關於鏈接點的相關知識,咱們先了解到這裏。有了這些鏈接點,咱們才能進行一些橫切操做,可是在操做以前,咱們須要定位選擇鏈接點,怎麼選擇的呢?這就是切點 Pointcut 要作的事情了,繼續往下看。

切點 - Pointcut

在上述定義的接口中的全部方法均可以認爲是 JoinPoint,可是有時咱們並不但願在全部的方法上都添加 Advice(這個後續會講到),而 Pointcut 的做用就是提供一組規則(使用 AspectJ pointcut expression language 來描述 )來匹配 JoinPoint,給知足規則的 JoinPoint 添加 Advice。

在上一節中咱們基於 XML 和註解實現了 AOP 功能,總結髮現切點的定義也分爲兩種。當基於 XML 文件時,能夠經過在配置文件中進行定義,具體以下:

<!--Spring基於Xml的切面-->
<aop:config>
    <!--定義切點函數-->
    <aop:pointcut id="rentPointCut" expression="execution(* com.msdn.bean.Host.rent())"/>

    <!-- 定義切面 order 定義優先級,值越小優先級越大-->
    <aop:aspect ref="proxy" order="0">
        <!--前置通知-->
        <aop:before method="seeHouse" pointcut-ref="rentPointCut" />
        <!--環繞通知-->
        <aop:around method="getMoney" pointcut-ref="rentPointCut" />
        <!--後置通知-->
        <aop:after method="fare" pointcut-ref="rentPointCut" />
    </aop:aspect>
</aop:config>
複製代碼

當基於註解進行配置時,定義切點須要兩個步驟:

  1. 定義一個空方法,無需參數,不能有返回值
  2. 使用 @Pointcut 標註,填入切點表達式

代碼定義以下:

    //定義一個切入點表達式,用來肯定哪些類須要代理
    @Pointcut("execution(* com.msdn.bean.Host.*(..))")
    public void rentPointCut(){

    }
複製代碼

這裏你們也都看到了,關於切點的定義要麼是經過來定義,又或者使用@Pointcut 來定義,並無其餘的地方出現過,可是經過以前在 Spring IoC 自定義標籤解析一文能夠知道,若是聲明瞭註解,那麼就必定會在程序中的某個地方註冊了對應的解析器。這裏就不從頭找起了,咱們先查看一下 Pointcut 定義:

package org.springframework.aop;

public interface Pointcut {
    Pointcut TRUE = TruePointcut.INSTANCE;

    /** 返回一個類型過濾器 */
    ClassFilter getClassFilter();

    /** 返回一個方法匹配器 */
    MethodMatcher getMethodMatcher();
}
複製代碼

Pointcut 接口中定義了兩個接口,分別用於返回類型過濾器和方法匹配器。用於對定義的切點函數進行解析,關於切點函數的講解,你們能夠閱讀Spring AOP : AspectJ Pointcut 切點。下面咱們再來看一下類型過濾器和方法匹配器接口的定義:

@FunctionalInterface
public interface ClassFilter {
    ClassFilter TRUE = TrueClassFilter.INSTANCE;

    boolean matches(Class<?> var1);
}

public interface MethodMatcher {
    MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;

    boolean matches(Method var1, Class<?> var2);

    boolean isRuntime();

    boolean matches(Method var1, Class<?> var2, Object... var3);
}
複製代碼

上面的兩個接口均定義了 matches 方法,咱們定義的切點函數就是經過 matches 方法進行解析的,而後選擇知足規則的鏈接點。在 Spring 中提供了一個 AspectJ 表達式切點類 AspectJExpressionPointcut,下面咱們來看一下這個類的繼承關係:

如上所示,這個類最終實現了 Pointcut、ClassFilter 和 MethodMatcher 接口,其中就包括 matches 方法的實現,具體代碼以下:

   public boolean matches(Class<?> targetClass) {
        PointcutExpression pointcutExpression = this.obtainPointcutExpression();

        try {
            try {
                return pointcutExpression.couldMatchJoinPointsInType(targetClass);
            } catch (ReflectionWorldException var5) {
                logger.debug("PointcutExpression matching rejected target class - trying fallback expression", var5);
                PointcutExpression fallbackExpression = this.getFallbackPointcutExpression(targetClass);
                if (fallbackExpression != null) {
                    return fallbackExpression.couldMatchJoinPointsInType(targetClass);
                }
            }
        } catch (Throwable var6) {
            logger.debug("PointcutExpression matching rejected target class", var6);
        }

        return false;
    }
複製代碼

經過該方法,對切點函數進行解析,該類也就具有了經過 AspectJ 表達式對鏈接點進行選擇的能力。

經過切點選擇出鏈接點以後,就要進行接下來的處理——通知(Advice)。

加強/通知 - Advice

通知 Advice 即咱們定義的橫切邏輯,好比咱們能夠定義一個用於監控方法性能的通知,也能夠定義一個事務處理的通知等。若是說切點解決了通知在哪裏調用的問題,那麼如今還須要考慮了一個問題,即通知在什麼時候被調用?是在目標方法執行前被調用,仍是在目標方法執行結束後被調用,還在二者兼備呢?Spring 幫咱們解答了這個問題,Spring 中定義瞭如下幾種通知類型:

上面是五種通知的介紹,下面咱們來看一下通知的源碼,以下:

package org.aopalliance.aop;

public interface Advice {
}
複製代碼

如上,通知接口裏好像什麼都沒定義。不過別慌,咱們再去到它的子類接口中一探究竟。

//前置通知
public interface BeforeAdvice extends Advice {
}

//返回通知
public interface AfterReturningAdvice extends AfterAdvice {
    void afterReturning(@Nullable Object var1, Method var2, Object[] var3, @Nullable Object var4) throws Throwable;
}

//後置通知
public interface AfterAdvice extends Advice {
}

//環繞通知
@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
    Object invoke(MethodInvocation var1) throws Throwable;
}

//異常通知
public interface ThrowsAdvice extends AfterAdvice {
}
複製代碼

以上通知的定義很簡單,咱們找一下它們的具體實現類,這裏先看一下前置通知的實現類 AspectJMethodBeforeAdvice。

public class AspectJMethodBeforeAdvice extends AbstractAspectJAdvice implements MethodBeforeAdviceSerializable {
    public AspectJMethodBeforeAdvice(Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) {
        super(aspectJBeforeAdviceMethod, pointcut, aif);
    }

    public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {
        this.invokeAdviceMethod(this.getJoinPointMatch(), (Object)null, (Throwable)null);
    }

    public boolean isBeforeAdvice() {
        return true;
    }

    public boolean isAfterAdvice() {
        return false;
    }
}
複製代碼

上面的核心代碼是 before()方法,用於執行咱們定義的前置通知函數。

因爲環繞通知比較重要,因此再來看一下它的具體實現類代碼。

public class AspectJAroundAdvice extends AbstractAspectJAdvice implements MethodInterceptorSerializable {
    public AspectJAroundAdvice(Method aspectJAroundAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) {
        super(aspectJAroundAdviceMethod, pointcut, aif);
    }

    public boolean isBeforeAdvice() {
        return false;
    }

    public boolean isAfterAdvice() {
        return false;
    }

    protected boolean supportsProceedingJoinPoint() {
        return true;
    }

    public Object invoke(MethodInvocation mi) throws Throwable {
        if (!(mi instanceof ProxyMethodInvocation)) {
            throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
        } else {
            ProxyMethodInvocation pmi = (ProxyMethodInvocation)mi;
            ProceedingJoinPoint pjp = this.lazyGetProceedingJoinPoint(pmi);
            JoinPointMatch jpm = this.getJoinPointMatch(pmi);
            return this.invokeAdviceMethod(pjp, jpm, (Object)null, (Throwable)null);
        }
    }

    protected ProceedingJoinPoint lazyGetProceedingJoinPoint(ProxyMethodInvocation rmi) {
        return new MethodInvocationProceedingJoinPoint(rmi);
    }
}
複製代碼

核心代碼爲 invoke 方法,當執行目標方法時,會首先來到這裏,而後再進入到自定義的環繞方法中。

如今咱們有了切點 Pointcut 和通知 Advice,因爲這兩個模塊目前仍是分離的,咱們須要把它們整合在一塊兒。這樣切點就能夠爲通知進行導航,而後由通知邏輯實施精確打擊。那怎麼整合兩個模塊呢?答案是,切面。好的,是時候來介紹切面 Aspect 這個概念了。

切面 - Aspect

切面 Aspect 整合了切點和通知兩個模塊,切點解決了 where 問題,通知解決了 when 和 how 問題。切面把二者整合起來,就能夠解決 對什麼方法(where)在什麼時候(when - 前置仍是後置,或者環繞)執行什麼樣的橫切邏輯(how)的三連發問題。在 AOP 中,切面只是一個概念,並無一個具體的接口或類與此對應。

切面類型主要分紅了三種

  • 通常切面
  • 切點切面
  • 引介/引入切面

通常切面,切點切面,引介/引入切面介紹:

public interface Advisor {
    Advice EMPTY_ADVICE = new Advice() {
    };

    Advice getAdvice();

    boolean isPerInstance();
}
複製代碼

咱們重點看一下 PointcutAdvisor ,關於該接口的定義以下:

public interface PointcutAdvisor extends Advisor {
    Pointcut getPointcut();
}
複製代碼

Advisor 中有一個 getAdvice 方法,用於返回通知。PointcutAdvisor 在 Advisor 基礎上,新增了 getPointcut 方法,用於返回切點對象。所以 PointcutAdvisor 的實現類便可以返回切點,也能夠返回通知,因此說 PointcutAdvisor 和切面的功能類似。因此說 PointcutAdvisor 和切面的功能類似。不過他們之間仍是有一些差別的,好比看下面的配置:

<!--Spring基於Xml的切面-->
    <aop:config>
        <!--定義切點函數-->
        <aop:pointcut id="rentPointCut" expression="execution(* com.msdn.bean.Host.rent())"/>

            <!-- 定義切面 order 定義優先級,值越小優先級越大-->
            <aop:aspect ref="proxy" order="0">
                <!--前置通知-->
                <aop:before method="seeHouse" pointcut-ref="rentPointCut" />
                    <!--環繞通知-->
                    <aop:around method="aroundMethod" pointcut-ref="rentPointCut" />
                        <!--後置通知-->
                        <aop:after method="fare" pointcut-ref="rentPointCut" />
                            </aop:aspect>
                                </aop:config>
複製代碼

如上,一個切面中配置了一個切點和三個通知,三個通知均引用了同一個切點,即 pointcut-ref="helloPointcut"。這裏在一個切面中,一個切點對應多個通知,是一對多的關係(能夠配置多個 pointcut,造成多對多的關係)。而在 PointcutAdvisor 的實現類 AspectJPointcutAdvisor 中,切點和通知是一一對應的關係。

    public AspectJPointcutAdvisor(AbstractAspectJAdvice advice) {
        Assert.notNull(advice, "Advice must not be null");
        this.advice = advice;
        this.pointcut = advice.buildSafePointcut();
    }
複製代碼

上面的通知最終會被轉換成三個 PointcutAdvisor,這裏我把在 AbstractAdvisorAutoProxyCreator 源碼調試的結果貼在下面:

織入 - Weaving

織入是把切面應用到目標對象並建立新的代理對象的過程。切面在指定的鏈接點被織入到目標對象中。在目標對象的生命週期裏有不少個點能夠織入:

  • 編譯期:切面在目標類編譯時被織入。這種方式須要特殊的編譯器。AspectJ 的織入編譯器就是以這種方式織入切面的 。
  • 類加載期:切面在目標類加載到 JVM 時被織入。這種方式須要特殊的類加載器(ClassLoader),它能夠在目標類被引入應用以前 加強該目標類的字節碼。
  • 運行期:切面在應用運行的某個時刻被織入。通常狀況下,在織入切面時,AOP 容器會爲目標對象動態地建立一個代理對象。Spring AOP 就是以這種方式織入切面的。

Spring AOP 既然是在目標對象運行期織入切面的,那它是經過什麼方式織入的呢?先來講說以何種方式進行織入,首先仍是從 LoadTimeWeaverAwareProcessor 開始,該類是後置處理器 BeanPostProcessor 的一個實現類,咱們都知道 BeanPostProcessor 有兩個核心方法,用於在 bean 初始化以前和以後被調用。具體是在 bean 對象初始化完成後,Spring經過切點對 bean 類中的方法進行匹配。若匹配成功,則會爲該 bean 生成代理對象,並將代理對象返回給容器。容器向後置處理器輸入 bean 對象,獲得 bean 對象的代理,這樣就完成了織入過程。 關於後置處理器的細節,這裏就很少說了,你們如有興趣,能夠參考以前寫的Spring之BeanFactoryPostProcessor和BeanPostProcessor

實例

結合上述分析的內容,咱們對上一章節中的案例進行擴展,修改切面定義。

@Order(0)
@Aspect
@Component
public class JoinPointDemo {

    //定義一個切入點表達式,用來肯定哪些類須要代理
    @Pointcut("execution(* com.msdn.bean.Host.*(..))")
    public void rentPointCut(){
    }

    /**
     * 前置方法,在目標方法執行前執行
     * @param joinPoint 封裝了代理方法信息的對象,若用不到則能夠忽略不寫
     */

    @Before("rentPointCut()")
    public void seeHouse(JoinPoint joinPoint){

        Signature oo = joinPoint.getSignature();
        System.out.println("前置方法準備執行......");
        System.out.println("目標方法名爲:" + joinPoint.getSignature().getName());
        System.out.println("目標方法所屬類的簡單類名:" +        joinPoint.getSignature().getDeclaringType().getSimpleName());
        System.out.println("目標方法所屬類的類名:" + joinPoint.getSignature().getDeclaringTypeName());
        System.out.println("目標方法聲明類型:" + Modifier.toString(joinPoint.getSignature().getModifiers()));

        //獲取傳入目標方法的參數
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            System.out.println("第" + (i+1) + "個參數爲:" + args[i]);
        }

        System.out.println("被代理的對象:" + joinPoint.getTarget());
        System.out.println("代理對象本身:" + joinPoint.getThis());
        System.out.println("前置方法執行結束......");
    }

    /**
     * 環繞方法,可自定義目標方法執行的時機
     * @param point
     * @return  該方法須要返回值,返回值視爲目標方法的返回值
     */

    @Around("rentPointCut()")
    public Object aroundMethod(ProceedingJoinPoint point){
        Object result = null;

        try {
            System.out.println("目標方法執行前...");
            //獲取目標方法的參數,判斷執行哪一個proceed方法
            Object[] args = point.getArgs();
            if (args.length > 0){
                //用新的參數值執行目標方法
                result = point.proceed(new Object[]{"上海市黃浦區中華路某某公寓19樓2號"});
            }else{
                //執行目標方法
                result = point.proceed();
            }
        } catch (Throwable e) {
            //異常通知
            System.out.println("執行目標方法異常後...");
            throw new RuntimeException(e);
        }
        System.out.println("目標方法執行後...");
        return result;
    }

    @After("rentPointCut()")
    public void fare(JoinPoint joinPoint){
        System.out.println("執行後置方法");
    }

}
複製代碼

配置文件以下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">


    <context:component-scan base-package="com.msdn.bean,com.msdn.aop" />
    <aop:aspectj-autoproxy />

</beans>
複製代碼

測試代碼爲:

@Test
public void aopTest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("spring-aop.xml");

    Object host = context.getBean("host");
    Rent o = (Rent) host;
    o.rent("新東方");
    o.buyFurniture();
}
複製代碼

執行結果爲:

目標方法執行前...
前置方法準備執行......
目標方法名爲:rent
目標方法所屬類的簡單類名:Rent
目標方法所屬類的類名:com.msdn.bean.Rent
目標方法聲明類型:public abstract
1個參數爲:上海市黃浦區中華路某某公寓192
被代理的對象:com.msdn.bean.Host@524d6d96
代理對象本身:com.msdn.bean.Host@524d6d96
前置方法執行結束......
出租位於上海市黃浦區中華路某某公寓192號處的房屋
目標方法執行後...
執行後置方法
********************
目標方法執行前...
前置方法準備執行......
目標方法名爲:buyFurniture
目標方法所屬類的簡單類名:Rent
目標方法所屬類的類名:com.msdn.bean.Rent
目標方法聲明類型:public abstract
被代理的對象:com.msdn.bean.Host@524d6d96
代理對象本身:com.msdn.bean.Host@524d6d96
前置方法執行結束......
添置傢俱
目標方法執行後...
執行後置方法
複製代碼

總結

前面三篇文章只是在介紹 Spring AOP 的原理和基本使用,從本文開始準備深刻學習 AOP,其中就先了解 AOP 的核心概念,對於後續源碼的學習很是有必要。若是文中有什麼不對的地方,歡迎指正。

參考文獻

SpringAop中JoinPoint對象的使用方法

《Spring實戰(第4版)》高清PDF

Spring AOP 源碼分析系列文章導讀

相關文章
相關標籤/搜索