開發與維運

Spring 5 中文解析核心篇-IoC容器之AOP編程(下)

5.5 基於Schema的AOP支持

如果你更喜歡基於XML的格式,Spring還提供了使用新的aop名稱空間標籤定義切面的支持。支持與使用@AspectJ樣式時完全相同的切入點表達式和通知類型。因此,在本節中,我們將重點放在新語法上,並使讀者參考上一節中的討論(@AspectJ支持),以瞭解編寫切入點表達式和通知參數的綁定。

要使用本節中描述的aop名稱空間標籤,你需要導入spring-aop模式,如基於XML Schema的配置中所述。有關如何在aop名稱空間中導入標籤的信息,請參見AOP schema

在你的Spring配置中,所有切面和advisor元素都必須放在<aop:config>元素內(在應用程序上下文配置中可以有多個<aop:config>元素)。<aop:config>元素可以包含切入點,advisoraspect元素(請注意,必須按此順序聲明它們)。

<aop:config>的配置樣式大量使用了Spring的自動代理機制。如果你已經通過BeanNameAutoProxyCreator或類似的工具使用了顯式的自動代理,那麼這可能會導致一些問題(比如沒有編織通知)。推薦的用法模式是僅使用<aop:config>樣式或僅使用AutoProxyCreator樣式並且不要混合使用。

5.5.1 聲明切面

使用schema支持時,切面是在Spring應用程序上下文中定義為Bean的常規Java對象。狀態和行為在對象的字段和方法中捕獲,切入點和通知信息在XML中捕獲。

你可以通過使用<aop:aspect>元素來聲明一個切面,並通過使用ref屬性來引用後臺bean,如下面的示例所示:

<aop:config>
    <aop:aspect id="myAspect" ref="aBean">
        ...
    </aop:aspect>
</aop:config>

<bean id="aBean" class="...">
    ...
</bean>

支持切面的bean(在本例中為aBean)當然可以像配置其他Spring bean一樣進行配置並注入依賴項。

5.5.2 聲明切入點

你可以在<aop:config>元素內聲明一個命名的切入點,讓切入點定義多個切面和advisors之間共享。

可以定義代表服務層中任何業務服務的執行的切入點:

<aop:config>

    <aop:pointcut id="businessService"
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>

</aop:config>

請注意,切入點表達式本身使用的是@AspectJ支持中所述的AspectJ切入點表達式語言。如果使用基於schema的聲明樣式,則可以引用在切入點表達式中的類型(@Aspects)中定義的命名切入點。定義上述切入點的另一種方法如下:

<aop:config>

    <aop:pointcut id="businessService"
        expression="com.xyz.myapp.SystemArchitecture.businessService()"/>

</aop:config>

假定你具有“共享通用切入點定義”中所述的SystemArchitecture方面。

然後,在切面內聲明切入點與聲明頂級切入點非常相似,如以下示例所示:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service.*.*(..))"/>

        ...

    </aop:aspect>

</aop:config>

@AspectJ切面幾乎相同,通過使用基於schema的定義樣式聲明的切入點可以收集連接點上下文。例如,以下切入點收集此對象作為連接點上下文,並將其傳遞給通知:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service.*.*(..)) && this(service)"/>

        <aop:before pointcut-ref="businessService" method="monitor"/>

        ...

    </aop:aspect>

</aop:config>

必須聲明該通知以通過包含匹配名稱的參數來接收收集的連接點上下文,如下所示:

public void monitor(Object service) {
    // ...
}

在組合切入點子表達式時,&&在XML文檔中是不合適的,所以你可以分別使用andornot關鍵字來代替&&||!。例如,上一個切入點可以更好地編寫如下:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service.*.*(..)) and this(service)"/>

        <aop:before pointcut-ref="businessService" method="monitor"/>

        ...
    </aop:aspect>
</aop:config>

請注意,以這種方式定義的切入點由其XML ID引用,並且不能用作命名切入點以形成複合切入點。因此,基於schema的定義樣式中的命名切入點支持比@AspectJ樣式所提供的更受限制。

5.5.3 聲明通知

基於schema的AOP支持使用與@AspectJ樣式相同的五種通知,並且它們具有完全相同的語義。

前置通知

在執行匹配的方法之前,先運行通知。使用<aop:before>元素在<aop:aspect>中聲明它,如以下示例所示:

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>

    ...

</aop:aspect>

在這裡,dataAccessOperation是在最高(<aop:config>)級別定義的切入點ID。要定義內聯切入點,請使用以下方法將pointcut-ref屬性替換為pointcut屬性:

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before
        pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
        method="doAccessCheck"/>

    ...

</aop:aspect>

正如我們在@AspectJ樣式的討論中所指出的那樣,使用命名的切入點可以顯著提高代碼的可讀性。method屬性標識提供通知正文的方法(doAccessCheck)。必須為包含通知的Aspect元素所引用的bean定義此方法。在執行數據訪問操作(與切入點表達式匹配的方法執行連接點)之前,將調用方面bean上的doAccessCheck方法。

返回後通知

返回的通知在匹配的方法執行正常完成時運行。它以與前置通知相同的方式在<aop:aspect>中聲明。以下示例顯示瞭如何聲明它:

<aop:aspect id="afterReturningExample" ref="aBean">

    <aop:after-returning
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>

    ...

</aop:aspect>

@AspectJ樣式一樣,你可以在通知正文中獲取返回值。為此,使用returning屬性指定返回值應傳遞到的參數的名稱,如以下示例所示:

<aop:aspect id="afterReturningExample" ref="aBean">

    <aop:after-returning
        pointcut-ref="dataAccessOperation"
        returning="retVal"
        method="doAccessCheck"/>

    ...

</aop:aspect>

doAccessCheck方法必須聲明一個名為retVal的參數。該參數的類型以與@AfterReturning中所述相同的方式約束匹配。例如,你可以按以下方式聲明方法簽名:

public void doAccessCheck(Object retVal) {...

異常通知

當匹配的方法執行通過拋出異常退出時執行通知時,拋出通知。通過使用after-throwing 元素在<aop:aspect>中聲明它,如以下示例所示:

<aop:aspect id="afterThrowingExample" ref="aBean">

    <aop:after-throwing
        pointcut-ref="dataAccessOperation"
        method="doRecoveryActions"/>

    ...

</aop:aspect>

@AspectJ樣式一樣,你可以在通知正文中獲取引發的異常。為此,請使用throwing屬性指定異常應傳遞到的參數的名稱,如以下示例所示:

<aop:aspect id="afterThrowingExample" ref="aBean">

    <aop:after-throwing
        pointcut-ref="dataAccessOperation"
        throwing="dataAccessEx"
        method="doRecoveryActions"/>

    ...

</aop:aspect>

doRecoveryActions方法必須聲明一個名為dataAccessEx的參數。該參數的類型以與@AfterThrowing中所述相同的方式約束匹配。例如,方法簽名可以聲明如下:

public void doRecoveryActions(DataAccessException dataAccessEx) {...

最終通知

無論最終如何執行匹配的方法,最終通知都會運行。你可以使用after元素聲明它,如以下示例所示:

<aop:aspect id="afterFinallyExample" ref="aBean">

    <aop:after
        pointcut-ref="dataAccessOperation"
        method="doReleaseLock"/>

    ...

</aop:aspect>

環繞通知

最後一種通知是環繞通知。環繞通知在匹配的方法執行“環繞”運行。它有機會在方法執行之前和之後執行工作,並確定何時、如何執行,甚至是否真的執行方法。環繞通知通常用於以線程安全的方式(例如,啟動和停止計時器)在方法執行之前和之後共享狀態。總是使用最弱的形式的通知來滿足你的要求(備註:最小範圍使用)。不要使用環繞的通知,如果前置通知可以做的工作。

你可以使用<aop:around>元素聲明環繞通知。通知方法的第一個參數必須是ProceedingJoinPoint類型。在通知的正文中,在ProceedingJoinPoint上調用proceed()會使底層方法執行。還可以使用Object []調用proceed方法。數組中的值用作方法執行時的參數。有關調用Object []的注意事項,請參見“環繞通知”。以下示例顯示瞭如何在XML中環繞通知進行聲明:

<aop:aspect id="aroundExample" ref="aBean">

    <aop:around
        pointcut-ref="businessService"
        method="doBasicProfiling"/>

    ...

</aop:aspect>

doBasicProfiling通知的實現可以與@AspectJ示例完全相同(當然要去掉註解),像以下示例所示:

public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
    // start stopwatch
    Object retVal = pjp.proceed();
    // stop stopwatch
    return retVal;
}

通知參數

基於schema的聲明樣式以與@AspectJ支持相同的方式支持完全類型的通知,即通過名稱與通知方法參數匹配切入點參數來實現。有關詳細信息,請參見通知參數。如果你希望顯式指定通知方法的參數名稱(不依賴於先前描述的檢測策略,則可以通過使用advice元素的arg-names屬性來實現,該屬性的處理方式與argNames屬性相同在通知註解中(如確定參數名稱中所述)。以下示例顯示如何在XML中指定參數名稱:

<aop:before
    pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
    method="audit"
    arg-names="auditable"/>

arg-names屬性接受以逗號分隔的參數名稱列表。

以下基於XSD的方法中涉及程度稍高的示例顯示了一些與一些強類型參數結構使用的建議:

package x.y.service;

public interface PersonService {

    Person getPerson(String personName, int age);
}

public class DefaultFooService implements FooService {

    public Person getPerson(String name, int age) {
        return new Person(name, age);
    }
}

接下來是切面。請注意profile(..)方法接受許多強類型參數的事實,其中第一個恰好是用於進行方法調用的連接點。此參數的存在表明profile(..)將用作通知,如以下示例所示:

package x.y;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;

public class SimpleProfiler {

    public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
        StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
        try {
            clock.start(call.toShortString());
            return call.proceed();
        } finally {
            clock.stop();
            System.out.println(clock.prettyPrint());
        }
    }
}

最後,以下示例XML配置影響了特定連接點的上述通知的執行:

<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"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- this is the object that will be proxied by Spring's AOP infrastructure -->
    <bean id="personService" class="x.y.service.DefaultPersonService"/>

    <!-- this is the actual advice itself -->
    <bean id="profiler" class="x.y.SimpleProfiler"/>

    <aop:config>
        <aop:aspect ref="profiler">

            <aop:pointcut id="theExecutionOfSomePersonServiceMethod"
                expression="execution(* x.y.service.PersonService.getPerson(String,int))
                and args(name, age)"/>

            <aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
                method="profile"/>

        </aop:aspect>
    </aop:config>

</beans>

考慮以下驅動程序腳本:

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.service.PersonService;

public final class Boot {

    public static void main(final String[] args) throws Exception {
        BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
        PersonService person = (PersonService) ctx.getBean("personService");
        person.getPerson("Pengo", 12);
    }
}

有了這樣的Boot類,我們將在標準輸出上獲得類似於以下內容的輸出:

StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0
-----------------------------------------
ms     %     Task name
-----------------------------------------
00000  ?  execution(getFoo)

通知順序

當需要在同一連接點(執行方法)上執行多個通知時,排序規則如“通知順序”中所述。切面之間的優先級是通過將Order註解添加到支持切面的Bean或通過使Bean實現Ordered接口來確定的。

5.5.4 引入

簡介(在AspectJ中稱為類型間聲明)使切面可以聲明通知的對象實現給定的接口,並代表那些對象提供該接口的實現。

你可以通過在<aop:aspect/>中使用<aop:declare-parents/>元素進行引入。你可以使用<aop:declare-parents/>元素聲明匹配類型具有新的父類(因此而得名)。例如,給定一個名為UsageTracked的接口和該接口名為DefaultUsageTracked的實現,以下切面聲明服務接口的所有實現者也都實現UsageTracked接口。(例如,為了通過JMX公開統計信息。)

<aop:aspect id="usageTrackerAspect" ref="usageTracking">

    <aop:declare-parents
        types-matching="com.xzy.myapp.service.*+"
        implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
        default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>

    <aop:before
        pointcut="com.xyz.myapp.SystemArchitecture.businessService()
            and this(usageTracked)"
            method="recordUsage"/>

</aop:aspect>

支持usageTracking bean的類將包含以下方法:

public void recordUsage(UsageTracked usageTracked) {
    usageTracked.incrementUseCount();
}

要實現的接口由Implement-interface屬性確定。類型匹配屬性的值是AspectJ類型模式。匹配類型的任何bean都實現UsageTracked接口。請注意,在前面示例的之前通知中,服務Bean可以直接用作UsageTracked接口的實現。要以編程方式訪問bean,可以編寫以下代碼:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
5.5.5 切面實例化模式

模式定義切面唯一受支持的實例化模型是單例模型。在將來的版本中可能會支持其他實例化模型。

5.5.6 Advisors

`

advisors的概念來自於Spring中定義的AOP支持,在AspectJ中沒有直接的對等物。advisors就像一個小的自包含的切面,只有一條通知。通知本身由bean表示,並且必須實現Spring的“通知類型”中描述的通知接口之一。advisors可以利用AspectJ切入點表達式。

Spring通過<aop:advisor>元素支持advisors概念。你通常會看到它與事務通知結合使用,事務通知在Spring中也有其自己的名稱空間支持。以下示例顯示advisors

<aop:config>

    <aop:pointcut id="businessService"
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>

    <aop:advisor
        pointcut-ref="businessService"
        advice-ref="tx-advice"/>

</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

除了在前面的示例中使用的pointcut-ref屬性之外,你還可以使用pointcut屬性內聯定義一個pointcut表達式。

要定義advisor的優先級以便通知可以參與排序,可以使用order屬性來定義advisor的排序值。

5.5.7 AOP Schema例子

本節將展示AOP示例中的併發鎖定失敗重試示例在使用模式支持重寫時的例子。

有時由於併發問題(例如,死鎖失敗者),業務服務的執行可能會失敗。如果重試該操作,則很可能在下一次嘗試中成功。對於適合在這種情況下重試的業務(不需要為解決衝突而需要返回給用戶冪等操作),我們希望透明地重試該操作,以避免客戶端看到PessimisticLockingFailureException。這項要求明確地跨越了服務層中的多個服務,因此非常適合通過一個切面實現。

因為我們想重試該操作,所以我們需要使用周圍建議,以便可以多次調用proceed。下面的清單顯示了基本的切面實現(它是一個使用schema支持的常規Java類)

public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }

}

請注意,切面實現了Ordered接口,因此我們可以將切面的優先級設置為高於事務通知(每次重試時都希望有新的事務)。maxRetriesorder屬性均由Spring配置。主要操作發生在通知方法周圍的doConcurrentOperation中。我們試著繼續。如果我們因為一個PessimisticLockingFailureException異常失敗了,我們會再次嘗試,除非我們已經耗盡了所有的重試嘗試。

該類與@AspectJ示例中使用的類相同,但是除去了註解。

相應的Spring配置如下:

<aop:config>

    <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">

        <aop:pointcut id="idempotentOperation"
            expression="execution(* com.xyz.myapp.service.*.*(..))"/>

        <aop:around
            pointcut-ref="idempotentOperation"
            method="doConcurrentOperation"/>

    </aop:aspect>

</aop:config>

<bean id="concurrentOperationExecutor"
    class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
        <property name="maxRetries" value="3"/>
        <property name="order" value="100"/>
</bean>

請注意,目前我們假設所有業務服務都是冪等的。如果不是這種情況,我們可以改進切面,以便通過引入等冪註解並使用註解來註釋服務操作的實現,使其僅重試真正的冪等操作,如以下示例所示:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

切面更改為僅重試冪等操作涉及更改切入點表達式,以便僅@Idempotent操作匹配,如下所示:

<aop:pointcut id="idempotentOperation"
        expression="execution(* com.xyz.myapp.service.*.*(..)) and
        @annotation(com.xyz.myapp.service.Idempotent)"/>
5.6 選擇哪一種AOP聲明格式使用

一旦確定切面是實現給定需求的最佳方法,你如何在使用Spring AOP或AspectJ以及在Aspect語言(代碼)樣式,@AspectJ註解樣式或Spring XML樣式之間做出選擇?這些決定受許多因素影響、包括應用程序需求、開發工具和團隊對AOP的熟悉程度。

5.6.1 Spring AOP 或 完整的AspectJ?

使用最簡單的方法即可。 Spring AOP比使用完整的AspectJ更簡單,因為不需要在開發和構建過程中引入AspectJ編譯器/編織器。如果你只需要通知在Spring bean上執行操作,則Spring AOP是正確的選擇。如果你需要通知不受Spring容器管理的對象(通常是領域對象),則需要使用AspectJ。如果你希望通知除簡單方法執行之外的連接點(例如,字段get或set連接點等),則還需要使用AspectJ (備註:對字段屬性進行通知使用AspectJ )。

使用AspectJ時,可以選擇AspectJ語言語法(也稱為“代碼樣式”)或@AspectJ註解樣式。顯然,如果你不使用Java 5+,則已經為你做出了選擇:使用代碼樣式。如果切面在你的設計中起著重要作用,並且你能夠使用Eclipse的AspectJ開發工具(AJDT)插件,則AspectJ語言語法是首選。它更乾淨、更簡單,因為該語言是專為編寫切面而設計的。如果你不使用Eclipse或只有少數幾個切面在你的應用程序中,那麼你可能要考慮使用@AspectJ樣式,在IDE中堅持常規Java編譯,並向其中添加切面編織階段你的構建腳本。

5.6.2 Spring AOP中使用@AspectJ XML選擇?

如果你選擇使用Spring AOP,則可以選擇@AspectJXML樣式。有各種折衷考慮。

XML樣式可能是現有Spring用戶最熟悉的,並且得到了真正的POJO的支持。當使用AOP作為配置企業服務的工具時,XML是一個不錯的選擇(一個很好的嘗試是你是否將切入點表達式視為你可能希望獨立更改的配置的一部分)。使用XML樣式,可以說從你的配置中可以更清楚地瞭解系統中存在哪些切面。

XML樣式有兩個缺點。首先,它沒有完全將要解決的需求的實現封裝在一個地方。DRY原則說,系統中的任何知識都應該有一個單一、明確、權威的表示形式。當使用XML樣式時,需求如何實現的知識在支持bean類的聲明和配置文件中的XML中被分割開來。當你使用@AspectJ樣式時,此信息將封裝在一個模塊中:切面。其次,與@AspectJ樣式相比,XML樣式在表達能力上有更多限制:僅支持“單例”切面實例化模型,並且無法組合以XML聲明的命名切入點。例如,使用@AspectJ樣式,你可以編寫如下內容:

@Pointcut("execution(* get*())")
public void propertyAccess() {}

@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}

@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}

在XML樣式中,你可以聲明前兩個切入點:

<aop:pointcut id="propertyAccess"
        expression="execution(* get*())"/>

<aop:pointcut id="operationReturningAnAccount"
        expression="execution(org.xyz.Account+ *(..))"/>

XML方法的缺點是你無法通過組合這些定義來定義accountPropertyAccess切入點(備註:不能組合多個切面)。

@AspectJ樣式支持其他實例化模型和更豐富的切入點組合。它具有將切面保持為模塊化單元的優勢。它還具有的優點是,Spring AOP和AspectJ都可以理解@AspectJ方面。因此,如果你以後決定需要AspectJ的功能來實現其他要求,則可以輕鬆地遷移到AspectJ設置。總而言之,Spring在自定義方面更喜歡@AspectJ樣式,而不是簡單地配置企業服務。

5.7 混合切面類型

通過使用自動代理支持,模式定義的<aop:aspect>切面、<aop:advisor>聲明的advisors,甚至是同一配置中其他樣式的代理和攔截器,完全可以混合@AspectJ樣式的切面。所有這些都是通過使用相同的底層支持機制實現的,並且可以毫無困難地共存。

5.8 代理機制

Spring AOP使用JDK動態代理或CGLIB創建給定目標對象的代理。JDK動態代理內置在JDK中,而CGLIB是常見的開源類定義庫(重新包裝到spring-core中)。

如果要代理的目標對象實現至少一個接口,則使用JDK動態代理。代理了由目標類型實現的所有接口。如果目標對象未實現任何接口,則將創建CGLIB代理。

如果要強制使用CGLIB代理(例如,代理為目標對象定義的每個方法,而不僅是由其接口實現的方法),可以這樣做。但是,你應該考慮以下問題:

  • 使用CGLIB,不能通知final方法,因為不能在運行時生成的子類中覆蓋它們。
  • 從Spring 4.0開始,由於CGLIB代理實例是通過Objenesis創建的,因此不再調用代理對象的構造函數兩次。只有在你的JVM不允許繞過構造函數的情況下,你才可能從Spring的AOP支持中得到兩次調用和相應的調試日誌條目。

要強制使用CGLIB代理,請將<aop:config>元素的proxy-target-class屬性的值設置為true,如下所示:

<aop:config proxy-target-class="true">
    <!-- other beans defined here... -->
</aop:config>

要在使用@AspectJ自動代理支持時強制CGLIB代理,請將<aop:aspectj-autoproxy>元素的proxy-target-class屬性設置為true,如下所示:

<aop:aspectj-autoproxy proxy-target-class="true"/>

多個<aop:config/>部分在運行時摺疊到一個統一的自動代理創建器中,該創建器將應用任何<aop:config />部分(通常來自不同的XML bean定義文件)指定的最強的代理設置。這也適用於<tx:annotation-driven/><aop:aspectj-autoproxy/>元素。

為了清楚起見,在<tx:annotation-driven/><aop:aspectj-autoproxy/><aop:config/>元素上使用proxy-target-class =“true”會強制對所有三個元素使用CGLIB代理其中。

5.8.1 理解AOP代理

Spring AOP是基於代理的。在編寫自己的切面或使用Spring Framework隨附的任何基於Spring AOP的切面之前,掌握最後一條語句實際含義的語義至關重要。

首先考慮以下情況:你有一個普通的、未經代理的、無特殊要求的直接對像引用,如以下代碼片段所示:

public class SimplePojo implements Pojo {

    public void foo() {
        // 調用當前對象的bar方法
        this.bar();
    }

    public void bar() {
        // some logic...
    }
}

如果在對象引用上調用方法,則直接在該對象引用上調用該方法,如下圖清單所示:

aop proxy plain pojo call

public class Main {

    public static void main(String[] args) {
        Pojo pojo = new SimplePojo();
        // this is a direct method call on the 'pojo' reference
        pojo.foo();
    }
}

當客戶端代碼具有的引用是代理時,情況會稍有變化。考慮以下圖表和代碼片段:

aop proxy call

public class Main {

    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());

        Pojo pojo = (Pojo) factory.getProxy();
        // this is a method call on the proxy!
        pojo.foo();
    }
}

此處要理解的關鍵是,Main類的main(..)方法內部的客戶端代碼具有對代理的引用。這意味著該對象引用上的方法調用是代理上的調用。因此,代理可以委託給與特定方法調用相關的所有攔截器(通知)。然而,一旦調用最終到達目標對象(本例中是SimplePojo,即引用),它可能對自身進行的任何方法調用,比如this.bar()或this.foo(),都將針對this引用而不是代理進行調用。這具有重要意義。這意味著自調用不會導致與方法調用相關的通知得到執行的機會(備註:通過代理調用才能觸發相關的通知)。

Okay,那麼該怎麼辦?最佳方法(在這裡寬鬆地使用術語“最佳”)是重構代碼,以免發生自調用。這確實需要你做一些工作,但這是最好的,侵入性最小的方法。下一種方法絕對可怕,我們正要指出這一點,恰恰是因為它是如此可怕。你可以(這對我們來說很痛苦)將類中的邏輯完全綁定到Spring AOP,如下面的示例所示:

public class SimplePojo implements Pojo {

    public void foo() {
        // this works, but... gah!
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // some logic...
    }
}

這將你的代碼完全耦合到Spring AOP,並且使類本身意識到在AOP上下文中使用的事實,而AOP上下文卻是這樣。創建代理時,還需要一些其他配置,如以下示例所示:

public class Main {

    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        //需要指定暴露代理
        factory.setExposeProxy(true);

        Pojo pojo = (Pojo) factory.getProxy();
        // this is a method call on the proxy!
        pojo.foo();
    }
}

最後,必須注意,AspectJ沒有此自調用問題,因為它不是基於代理的AOP框架。

5.9 編程創建 @AspectJ代理

除了通過使用<aop:config><aop:aspectj-autoproxy>聲明配置中的各個切面外,還可以通過編程方式創建通知目標對象的代理。有關Spring的AOP API的完整詳細信息,請參閱下一章。在這裡,我們要重點介紹使用@AspectJ切面自動創建代理的功能。

你可以使用org.springframework.aop.aspectj.annotation.AspectJProxyFactory類為一個或多個@AspectJ切面通知的目標對象創建代理。此類的基本用法非常簡單,如以下示例所示:

// 創建一個工廠,這個工廠可以為給定對象生成代理
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);

// 增加切面,這個切面必須被標註@AspectJ註解
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);

// 添加存在的切面,這個切面對象背心支持@AspectJ
factory.addAspect(usageTracker);

// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();

有關更多信息,請參見javadoc

5.10 在Spring應用中使用AspectJ

到目前為止,本章介紹的所有內容都是純Spring AOP。在本節中,我們將研究如果你的需求超出了Spring AOP所提供的功能,那麼如何使用AspectJ編譯器或weaver代替Spring AOP或除Spring AOP之外使用。

Spring附帶了一個小的AspectJ切面庫,該庫在你的發行版中可以作為spring-aspects.jar獨立使用。你需要將其添加到類路徑中才能使用其中的切面。使用AspectJ依賴於Spring和AspectJ的其他Spring切面來注入域對象以及AspectJ討論該庫的內容以及如何使用它。使用Spring IoC配置AspectJ切面討論瞭如何依賴注入使用AspectJ編譯器編織的AspectJ切面。最後,Spring Framework中使用AspectJ進行的加載時編織為使用AspectJ的Spring應用順序提供了加載時編織的介紹。

5.10.1 在Spring中使用AspectJ去依賴注入領域對象

Spring容器實例化並配置在你的應用程序上下文中定義的bean。給定包含要應用的配置的Bean定義的名稱,也可以要求Bean工廠配置預先存在的對象。spring-aspects.jar包含註解驅動的切面,該切面利用此功能允許依賴項注入任何對象。該支撐旨在用於在任何容器的控制範圍之外創建的對象。領域對象通常屬於此類,因為它們通常是通過數據庫查詢的結果由new操作或ORM工具以編程方式創建的。

@Configurable註解將一個類標記為符合Spring驅動的配置。在最簡單的情況下,你可以將其純粹用作標記註解,如以下示例所示:

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable
public class Account {
    // ...
}

當以這種方式作為標記接口使用時,Spring通過使用與完全限定類型名(com.xyz.myapp.domain.Account)同名的bean定義(通常是原型作用域)來配置註解類型(在本例中為Account)的新實例。由於bean的默認名稱是其類型的全限定名,因此聲明原型定義的一種方便的方法是省略id屬性,如下面的示例所示:

<bean class="com.xyz.myapp.domain.Account" scope="prototype">
    <property name="fundsTransferService" ref="fundsTransferService"/>
</bean>

如果要顯式指定要使用的原型bean定義的名稱,則可以直接在註解中這樣做如以下示例所示:

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable("account")
public class Account {
    // ...
}

Spring現在查找名為account的bean定義,並將其用作配置新Account實例的定義。

你也可以使用自動裝配來避免完全指定專用的bean定義。要讓Spring應用自動裝配,請使用@Configurable註解的autowire屬性。你可以指定@Configurable(autowire = Autowire.BY_TYPE)@Configurable(autowire = Autowire.BY_NAME)分別按類型或名稱進行自動裝配。或者,最好在字段或方法級別通過@Autowired@Inject@Configurable bean指定顯式的,註解驅動的依賴項注入(有關更多詳細信息,請參見基於註釋的容器配置)。最後,你可以使用dependencyCheck屬性(例如,@Configurable(autowire = Autowire.BY_NAME,dependencyCheck = true))為新創建和配置的對象中的對象引用啟用Spring依賴檢查。如果該屬性設置為true,則Spring在配置後驗證所有屬性(不是原生類型或集合)是否已經設置。

請注意,單獨使用註解不會執行任何操作。spring-aspects.jar中的AnnotationBeanConfigurerAspect會對註解的存在起作用。本質上,切面的意思是,在初始化一個帶有@Configurable註解的類型的新對象之後,使用Spring根據註解的屬性配置新創建的對象。在這種情況下,“初始化”是指新實例化的對象(例如,用new運算符實例新的對象)以及正在進行反序列化(例如,通過readResolve()的可序列化的對象)。

上述段落中的一個關鍵短語是in essence(本質)。在大多數情況下,“從新對象的初始化返回後”的確切語義是可以的。在這種情況下,“初始化之後”是指在構造對象之後注入依賴項。這意味著該依賴項不可在類的構造函數體中使用。如果你希望在構造函數主體執行之前注入依賴項,從而可以在構造函數主體中使用這些依賴項,則需要在@Configurable聲明中對此進行定義,如下所示:

@Configurable(preConstruction = true)

你可以在《 AspectJ編程指南》的此附錄中找到有關各種切入點類型的語言語義的更多信息。

為此,必須將帶註解的類型與AspectJ編織器編織在一起。你可以使用構建時的Ant或Maven任務來執行此操作(例如,參見《 AspectJ開發環境指南》),也可以使用加載時編織(請參見Spring Framework中的使用AspectJ進行加載時編織)。Spring需要配置AnnotationBeanConfigurerAspect自身(以便獲得對將用於配置新對象Bean工廠的引用)。如果使用基於Java的配置,則可以將@EnableSpringConfigured添加到任何@Configuration類中,如下所示:

@Configuration
@EnableSpringConfigured
public class AppConfig {
}

如果你更喜歡基於XML的配置,則Spring context namespace定義了一個方便的context:spring-configured元素,你可以按以下方式使用它:

<context:spring-configured/>

在配置切面之前創建的@Configurable對象實例導致向調試日誌發出消息,並且不進行對象配置。一個例子可能是Spring配置中的一個bean,當它由Spring初始化時會創建域對象。在這種情況下,你可以使用depends-on bean屬性來手動指定bean依賴於配置切面。下面的示例顯示如何使用depends-on屬性:

<bean id="myService"
        class="com.xzy.myapp.service.MyService"
        depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">

    <!-- ... -->

</bean>

除非你真的想在運行時依賴它的語義,否則不要通過bean configurer激活@Configurable處理。特別是,請確保不要在通過容器註冊為常規Spring bean的bean類上使用@Configurable。這樣做會導致兩次初始化,一次是通過容器,一次是通過切面。

單元測試@Configurable對象

@Configurable支持的目標之一是實現領域對象的獨立單元測試,而不會需要複雜的硬編碼查找。如果AspectJ尚未編織@Configurable類型,則註解在單元測試期間不起作用。你可以在被測對象中設置mockstub屬性引用,然後照常進行。如果AspectJ編織了@Configurable類型,你仍然可以像往常一樣在容器外部進行單元測試,但是每次構造@Configurable對象時,你都會看到一條警告消息,指示該對象尚未由Spring配置。

使用多個應用程序上下文

用於實現@Configurable支持的AnnotationBeanConfigurerAspect是AspectJ單例切面。單例切面的範圍與靜態成員的範圍相同:每個類加載器都有一個切面實例定義類型。這意味著,如果你在同一個類加載器層次結構中定義多個應用程序上下文則需要考慮在何處定義@EnableSpringConfigured bean,以及在哪裡將spring-aspects.jar放置在類路徑上。

考慮一個典型的Spring Web應用程序配置,該配置具有一個共享的父應用程序上下文,該上下文定義了通用的業務服務、支持那些服務所需的一切、以及每個Servlet的一個子應用程序上下文(其中包含該Servlet的特定定義)。所有這些上下文共存於同一類加載器層次結構中,因此AnnotationBeanConfigurerAspect只能保存對其中一個的引用。在這種情況下,我們建議在共享(父)應用程序上下文中定義@EnableSpringConfigured bean。這定義了你可能想注入領域對象的服務。結果是,你無法使用@Configurable機制來配置領域對象,該領域對象引用的是在子(特定於servlet的)上下文中定義的Bean的引用(無論如何,這可能不是你想要做的)。

在同一容器中部署多個Web應用程序時,請確保每個Web應用程序通過使用其自己的類加載器(例如,將spring-aspects.jar放置在“ WEB-INF/lib”中)將其類型加載到spring-aspects.jar中。如果將spring-aspects.jar僅添加到容器級的類路徑中(並因此由共享的父類加載器加載),則所有Web應用程序都共享相同的切面實例(可能不是你想要的)。

5.10.2 AspectJ的其他Spring切面

除了@Configurable切面之外,spring-aspects.jar還包含一個AspectJ切面,你可以使用該切面來驅動Spring的事務管理,以使用@Transactional註解來註釋類型和方法。這主要適用於希望在Spring容器之外使用Spring Framework的事務支持的用戶。

解析@Transactional註解的切面是AnnotationTransactionAspect。使用此切面時,必須註解實現類(或該類中的方法或兩者),而不是註釋實現類所實現的接口(如果有)。AspectJ遵循Java的規則,即不繼承接口上的註解。

類上的@Transactional註解指定用於執行該類中任何公共操作的默認事務語義。可以註解任何可見性的方法,包括私有方法。直接註解非公共方法是執行此類方法而獲得事務劃分的唯一方法。

從Spring Framework 4.2開始,spring-aspects提供了一個相似的切面,為標準javax.transaction.Transactional註解提供了完全相同的功能。檢查JtaAnnotationTransactionAspect瞭解更多詳細信息。

對於希望使用Spring配置和事務管理支持但又不想(或不能)使用註解的AspectJ編程者,spring-aspects.jar也包含抽象切面,你可以擴展它們以提供自己的切入點定義。有關更多信息,請參見AbstractBeanConfigurerAspectAbstractTransactionAspect切面的資源。作為示例,以下摘錄顯示瞭如何編寫切面來使用與完全限定的類名匹配的類型bean定義來配置域模型中定義的對象的所有實例:

public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {

    public DomainObjectConfiguration() {
        setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
    }

    // the creation of a new bean (any object in the domain model)
    protected pointcut beanCreation(Object beanInstance) :
        initialization(new(..)) &&
        SystemArchitecture.inDomainModel() &&
        this(beanInstance);
}
5.10.3 通過使用Spring IoC配置AspectJ切面

當你將AspectJ切面與Spring應用程序一起使用時,既自然又希望能夠使用Spring配置這些切面。AspectJ運行時本身負責切面的創建,並且通過Spring配置AspectJ創建的切面的方法取決於切面所使用的AspectJ實例化模型(per-xxx子句)。

AspectJ的大多數切面都是單例切面。這些切面的配置很容易。你可以創建一個bean定義,該bean定義按常規引用切面類型,幷包括factory-method =“aspectOf” bean屬性。這樣可以確保Spring通過向AspectJ獲取實例,而不是嘗試自己創建實例。以下示例顯示如何使用factory-method =“aspectOf”屬性:

<bean id="profiler" class="com.xyz.profiler.Profiler"
        factory-method="aspectOf"> //1

    <property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>
  1. 注意factory-method =“aspectOf”屬性

非單例切面更難配置。但是,可以通過創建原型Bean定義並使用spring-aspects.jar中的@Configurable支持來實現,一旦它們由AspectJ運行時創建了Bean,就可以配置切面實例。

如果你有一些要與AspectJ編織的@AspectJ切面(例如,對域模型類型使用加載時編織)以及要與Spring AOP一起使用的其他@AspectJ切面,那麼這些切面都已在Spring中配置,你需要告訴Spring AOP @AspectJ自動代理支持,應使用配置中定義的@AspectJ切面的確切子集進行自動代理。你可以通過在<aop:aspectj-autoproxy />聲明中使用一個或多個<include />元素來做到這一點。每個<include />元素都指定一個名稱模式,只有名稱與至少一個模式匹配的bean才可用於Spring AOP自動代理配置。以下示例顯示瞭如何使用<include />元素:

<aop:aspectj-autoproxy>
    <aop:include name="thisBean"/>
    <aop:include name="thatBean"/>
</aop:aspectj-autoproxy>

不要被<aop:aspectj-autoproxy />元素的名稱所迷惑。使用它可以創建Spring AOP代理。這裡使用的是@AspectJ樣式的切面聲明,但不涉及AspectJ運行時。

5.10.4 在Spring Framework中使用AspectJ進行加載時編織

加載時編織(LTW)是指在將AspectJ切面加載到應用程序的類文件中時將其編織到Java虛擬機(JVM)中的過程。本節的重點是在Spring框架的特定上下文中配置和使用LTW。本節不是LTW的一般介紹。有關LTW的詳細信息以及僅使用AspectJ配置LTW(完全不涉及Spring)的詳細信息請參閱《 AspectJ開發環境指南》的LTW部分。Spring框架為AspectJ LTW帶來的價值在於能夠對編織過程進行更精細的控制。“ Vanilla” AspectJ LTW通過使用Java(5+)代理來實現,該代理在啟動JVM時通過指定VM參數來切換。因此,它是一個JVM範圍的設置,在某些情況下可能很好,但通常有點過於粗糙。啟用spring的LTW允許你在每個類加載器的基礎上打開LTW,這是更細粒度的,在“單jvm -多應用程序”環境中更有意義(比如在典型的應用程序服務器環境中)。

此外,在某些環境中,此支持激活裝載時編織不需要應用程序服務器的啟動腳本進行任何修改,也不需要添加-javaagent:path/to/aspectjweaver.jar(如本節稍後所述)或-javaagent:path/to/spring-instrument.jar。開發人員將應用程序上下文配置為啟用加載時編織,而不是依賴通常負責部署配置(例如啟動腳本)的管理員。

現在介紹結束了,讓我們首先瀏覽一個使用Spring的AspectJ LTW的快速示例,然後詳細介紹示例中引入的元素。有關完整的示例,請參見Petclinic示例應用程序

第一個例子

假設你是一位負責診斷系統中某些性能問題的原因的應用程序開發人員。我們將打開一個簡單的配置切面,而不是使用配置文件工具,使我們能夠快速獲取一些性能指標。然後,我們可以立即在該特定區域應用更細粒度的分析工具。

此處提供的示例使用XML配置。你還可以配置@AspectJ並將其與Java配置一起使用。具體來說,你可以使用@EnableLoadTimeWeaving註解替代<context:load-time-weaver />(有關詳細信息,請參見下文)。

下面的示例顯示了配置切面的信息,這並不理想。這是一個基於時間的探查器,它使用@AspectJ樣式的切面聲明:

package foo;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.util.StopWatch;
import org.springframework.core.annotation.Order;

@Aspect
public class ProfilingAspect {

    @Around("methodsToBeProfiled()")
    public Object profile(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch sw = new StopWatch(getClass().getSimpleName());
        try {
            sw.start(pjp.getSignature().getName());
            return pjp.proceed();
        } finally {
            sw.stop();
            System.out.println(sw.prettyPrint());
        }
    }

    @Pointcut("execution(public * foo..*.*(..))")
    public void methodsToBeProfiled(){}
}

我們還需要創建一個META-INF/aop.xml文件,以通知AspectJ編織者我們希望將ProfilingAspect編織到類中。此文件約定,即在Java類路徑上稱為META-INF/aop.xml的文件,是標準的AspectJ。以下示例顯示aop.xml文件:

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>

    <weaver>
        <!-- only weave classes in our application-specific packages -->
        <include within="foo.*"/>
    </weaver>

    <aspects>
        <!-- weave in just this aspect -->
        <aspect name="foo.ProfilingAspect"/>
    </aspects>

</aspectj>

現在,我們可以繼續進行配置中特定於Spring的部分。我們需要配置一個LoadTimeWeaver(稍後說明)。該加載時織布器是必不可少的組件,負責將一個或多個META-INF/aop.xml文件中的切面配置編織到應用程序的類中。好處是,它不需要很多配置(你可以指定一些其他選項,但是稍後會詳細介紹),如以下示例所示:

<?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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- a service object; we will be profiling its methods -->
    <bean id="entitlementCalculationService"
            class="foo.StubEntitlementCalculationService"/>

    <!-- this switches on the load-time weaving -->
    <context:load-time-weaver/>
</beans>

現在,所有必需的組件(切面,META-INF/aop.xml文件和Spring配置)均已就緒,我們可以使用main(..)方法創建以下驅動程序類,以演示LTW的實際作用:

package foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class);

        EntitlementCalculationService entitlementCalculationService =
                (EntitlementCalculationService) ctx.getBean("entitlementCalculationService");

        // the profiling aspect is 'woven' around this method execution
        entitlementCalculationService.calculateEntitlement();
    }
}

我們還有最後一件事要做。本節的引言確實說過,可以使用Spring在每個ClassLoader的基礎上選擇性地打開LTW,這是事實。但是,對於此示例,我們使用Java代理(Spring提供)打開LTW。我們使用以下命令來運行前面顯示的Main類:

java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main

-javaagent是一個標誌,用於指定和啟用代理以對在JVM上運行的程序進行檢測。Spring框架附帶了這樣的代理工具InstrumentationSavingAgent,該代理文件打包在spring-instrument.jar中,在上一示例中作為-javaagent參數的值提供。

執行Main程序的輸出類似於下一個示例。 (我在calculateEntitlement()實現中引入了Thread.sleep(..)語句,以便探查器實際上捕獲的不是0毫秒的內容(01234毫秒不是AOP引入的開銷)。以下清單顯示了運行分析器時得到的輸出:

Calculating entitlement

StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms     %     Task name
------ ----- ----------------------------
01234  100%  calculateEntitlement

由於此LTW是通過使用成熟的AspectJ來實現的,因此我們不僅限於為Spring Bean提供通知。在Main程序上進行以下細微改動會產生相同的結果:

package foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Main {

    public static void main(String[] args) {
        new ClassPathXmlApplicationContext("beans.xml", Main.class);

        EntitlementCalculationService entitlementCalculationService =
                new StubEntitlementCalculationService();

        // the profiling aspect will be 'woven' around this method execution
        entitlementCalculationService.calculateEntitlement();
    }
}

請注意,在前面的程序中,我們如何引導Spring容器,然後完全在Spring上下之外創建StubEntitlementCalculationService的新實例。剖析通知仍會被應用。

誠然,這個例子很簡單。但是,在前面的示例中已經介紹了Spring對LTW支持的基礎,本節的其餘部分詳細解釋了每個配置和用法背後的“原因”。

在此示例中使用的ProfilingAspect可能是基本的,但它非常有用。它是開發時間方面的一個很好的例子,開發人員可以在開發期間使用它,然後很容易地從部署到UAT或生產中的應用程序的構建中排除它。

切面

你在LTW中使用的切面必須是AspectJ切面。你可以使用AspectJ語言本身來編寫它們,也可以使用@AspectJ風格來編寫切面。這樣,你的切面是有效的AspectJ和Spring AOP方面。此外,編譯的切面類需要在類路徑上可用。

'META-INF/aop.xml'

通過使用Java類路徑上的一個或多個META-INF/aop.xml文件(直接或通常在jar文件中)來配置AspectJ LTW基礎結構。

該文件的結構和內容在AspectJ參考文檔的LTW部分中進行了詳細說明。由於aop.xml文件是100%的AspectJ,因此在此不再贅述。

所需的庫(JARS)

至少,你需要使用以下庫來使用Spring Framework對AspectJ LTW的支持:

  • spring-aop.jar
  • aspectjweaver.jar

如果使用Spring提供的代理來啟用檢測,則還需要:

  • spring-instrument.jar

Spring配置

Spring的LTW支持的關鍵組件是LoadTimeWeaver接口(在org.springframework.instrument.classloading包中),以及Spring發行版附帶的眾多實現。LoadTimeWeaver負責在運行時將一個或多個java.lang.instrument.ClassFileTransformers添加到ClassLoader,這為各種有趣的應用程序打開了大門,其中之一就是方面的LTW。

如果你不熟悉運行時類文件轉換的概念,請在繼續操作之前參閱java.lang.instrument的javadoc API文檔。儘管該文檔並不全面,但至少你可以看到關鍵的接口和類(在你通讀本節文檔作為參考)。

為特定的ApplicationContext配置LoadTimeWeaver就像添加一行一樣容易。(請注意,你幾乎肯定需要將ApplicationContext用作Spring容器-通常,僅BeanFactory是不夠的,因為LTW支持使用BeanFactoryPostProcessors。)

要啟用Spring Framework的LTW支持,你需要配置一個LoadTimeWeaver,通常通過使用@EnableLoadTimeWeaving註解來完成,如下所示:

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}

另外,如果你喜歡基於XML的配置,請使用<context:load-time-weaver />元素。注意,該元素是在上下文名稱空間中定義的。以下示例顯示瞭如何使用<context:load-time-weaver />

<?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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:load-time-weaver/>

</beans>

前面的配置會自動為你定義並註冊許多LTW特定的基礎結構Bean,例如LoadTimeWeaverAspectJWeavingEnabler。默認的LoadTimeWeaverDefaultContextLoadTimeWeaver類,它嘗試裝飾自動檢測到的LoadTimeWeaver。“自動檢測到”的LoadTimeWeaver的確切類型取決於你的運行時環境。下表總結了各種LoadTimeWeaver實現:

運行時環境 LoadTimeWeaver實現
運行在 Apache Tomcat TomcatLoadTimeWeaver
運行在 GlassFish (限於EAR部署) GlassFishLoadTimeWeaver
運行在 Red Hat的 JBoss ASWildFly JBossLoadTimeWeaver
運行在 IBM的 WebSphere WebSphereLoadTimeWeaver
運行在 Oracle的 WebLogic WebLogicLoadTimeWeaver
JVM從Spring開始InstrumentationSavingAgent(java -javaagent:path/to/spring-instrument.jar) InstrumentationLoadTimeWeaver
期望基礎ClassLoader遵循通用約定 (即addTransformer和可選的getThrowawayClassLoader方法) ReflectiveLoadTimeWeaver

請注意,該表僅列出使用DefaultContextLoadTimeWeaver時自動檢測到的LoadTimeWeavers。你可以確切指定要使用的LoadTimeWeaver實現。

要使用Java配置指定特定的LoadTimeWeaver,請實現LoadTimeWeavingConfigurer接口並覆該getLoadTimeWeaver()方法。

以下示例指定了ReflectiveLoadTimeWeaver

@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {

    @Override
    public LoadTimeWeaver getLoadTimeWeaver() {
        return new ReflectiveLoadTimeWeaver();
    }
}

如果使用基於XML的配置,則可以在<context:load-time-weaver />元素上將全限定的類名指定為weaver-class屬性的值。同樣,以下示例指定了ReflectiveLoadTimeWeaver

<?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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:load-time-weaver
            weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>

</beans>

稍後可以使用眾所周知的名稱loadTimeWeaver從Spring容器中檢索由配置定義和註冊的LoadTimeWeaver。請記住,LoadTimeWeaver僅作為Spring LTW基礎結構添加一個或多個ClassFileTransformers的一種機制而存在。執行LTW的實際ClassFileTransformerClassPreProcessorAgentAdapter(來自org.aspectj.weaver.loadtime包)類。有關更多詳細信息,請參見ClassPreProcessorAgentAdapter類的類級javadoc,因為實際上如何實現編織的細節不在本文檔的討論範圍之內。

剩下要討論的配置的最後一個屬性:aspectjWeaving屬性(如果使用XML,則為Aspectj-weaving)。此屬性控制是否啟用LTW。它接受三個可能的值之一,如果屬性不存在,則默認值為autodetect。如果屬性不存在。下表總結了三個可能的值:

註解值 XML 值 Explanation
ENABLED on AspectJ正在編織,並且在加載時適當地編織了切面。
DISABLED off LTW已關閉。加載時不會編織任何切面。
AUTODETECT autodetect 如果Spring LTW基礎結構可以找到至少一個META-INF / aop.xml文件,則AspectJ編織已啟動。否則,它關閉。這是默認值。

特定環境配置

最後一部分包含在應用程序服務器和Web容器等環境中使用Spring的LTW支持時所需的任何其他設置和配置。

Tomcat、JBoss、WebSphere、WebLogic

TomcatJBoss/WildFlyIBM WebSphere Application ServerOracle WebLogic Server都提供了通用應用程序ClassLoader,該應用程序能夠進行本地檢測。Spring的本地LTW可以利用這些ClassLoader實現來提供AspectJ編織。如前所述,你可以簡單地啟用加載時編織。具體來說,你無需修改JVM啟動腳本即可添加-javaagent:path/to/spring-instrument.jar。請注意,在JBoss上,你可能需要禁用應用服務器掃描,以防止它在應用程序實際啟動之前加載類。一個快速的解決方法是將一個名為WEB-INF/jboss-scanning.xml的文件添加到你的構件中,其中包含以下內容:

<scanning xmlns="urn:jboss:scanning:1.0"/>

通用Java應用程序

如果特定LoadTimeWeaver實現不支持的環境中需要類檢測,則JVM代理是通用解決方案。對於這種情況,Spring提供了InstrumentationLoadTimeWeaver,它需要特定於Spring(但非常通用)的JVM代理spring-instrument.jar,並由常見@EnableLoadTimeWeaving<context:load-time-weaver />設置自動檢測到。

要使用它,必須通過提供以下JVM選項來使用Spring代理啟動虛擬機:

-javaagent:/path/to/spring-instrument.jar

請注意,這需要修改JVM啟動腳本,這可能會阻止你在應用程序服務器環境中使用它(取決於你的服務器和你的操作策略)。也就是說,對於每個JVM一個應用程序的部署(例如獨立的Spring Boot應用程序),無論如何,你通常都可以控制整個JVM的設置。

5.11 更多資源

可以在AspectJ網站上找到有關AspectJ的更多信息。

作者

個人從事金融行業,就職過易極付、思建科技、某網約車平臺等重慶一流技術團隊,目前就職於某銀行負責統一支付系統建設。自身對金融行業有強烈的愛好。同時也實踐大數據、數據存儲、自動化集成和部署、分佈式微服務、響應式編程、人工智能等領域。同時也熱衷於技術分享創立公眾號和博客站點對知識體系進行分享。關注公眾號:青年IT男 獲取最新技術文章推送!

博客地址: http://youngitman.tech

CSDN: https://blog.csdn.net/liyong1028826685

微信公眾號:

技術交流群:

Leave a Reply

Your email address will not be published. Required fields are marked *