Spring 建立切面

俺就不起網名發表於2018-08-24

目錄

 

1、概述

2、切點型別

3、切面型別

4、靜態普通方法名匹配切面

5、靜態正規表示式方法匹配切面

6、動態切面

7、流程切面

8、複合切點切面

9、總結


1、概述

在前面介紹各類增強時,大家可能沒有注意到一個問題:增強被織入到目標類的所有方法中。假設我們希望有選擇地織入到目標類某些特定的方法中,就需要使用切點進行目標連線點的定位了。描述連線點是進行AOP程式設計最主要的一項工作。增強提供了連線點方位資訊:如織入到方法前面、後面等,而切點進一步描述織入到哪些類的哪些方法上。

Spring通過org.springframework.aop.Pointcut介面描述切點,Pointcut由ClassFilter和MethodMatcher構成。它通過ClassFilter定位到某些特定類上,通過MethodMatcher定位到某些特定方法上,這樣Pointcut就擁有了描述某些類的某些特定方法的能力。Pointcut介面如下:

public interface Pointcut {
    Pointcut TRUE = TruePointcut.INSTANCE;
    //切點定位到特定類上
    ClassFilter getClassFilter();
    //切點定位到特定方法上
    MethodMatcher getMethodMatcher();
}

含義:切面由切點和增強(引介)組成,它既包括了橫切邏輯的定義,也包括了連線點的定義,Spring AOP就是負責實施切面的框架,它將切面所定義的橫切邏輯織入到切面所指定的連線點中。

2、切點型別

Spring提供了六種型別切點:

靜態方法切點:org.springframework.aop.support.StaticMethodMatcherPointcut是靜態方法切點的抽象基類,預設情況下它匹配所有的類。 StaticMethodMatcherPointcut包括 兩個主要的子類,分別是NameMatchMethodPointcut和AbstractRegexpMethodPointcut,前者提供簡單字串匹配方法簽名,而後者使用正規表示式匹配方法簽名;

動態方法切點:org.springframework.aop.support.DynamicMethodMatcherPointcut是動態方法切點的抽象基類,預設情況下它匹配所有的類。 DynamicMethodMatcherPointcut類已經過時,可以使用DefaultPointcutAdvisor和DynamicMethodMatcherPointcut動態方法匹配器替代之;

註解切點:org.springframework.aop.support.annotation.AnnotationMatchingPointcut實現類表示註解切點;

表示式切點:org.springframework.aop.support.ExpressionPointcut介面主要是為了支援AspectJ切點表示式語法而定義的介面;

流程切點:org.springframework.aop.support.ControlFlowPointcut實現類表示控制流程切點。ControlFlowPointcut是一種特殊的切點,它根據程式執行堆疊的資訊檢視目標方法是否由某一個方法直接或間接發起呼叫,以此判斷是否為匹配的連線點;

複合切點:org.springframework.aop.support.ComposablePointcut實現類是為建立多個切點而提供的方便操作類。它所有的方法都返回ComposablePointcut類,這樣,我們就可以使用連結表示式對其進行操作,形如:Pointcut pc=new ComposablePointcut().union(classFilter).intersection(methodMatcher).intersection(pointcut)

3、切面型別

切面就是由切點和增強組成。由於增強既包含橫切程式碼,又包含部分的連線點資訊(方法前、方法後等的方位資訊),所以我們可以僅通過增強類生成一個切面。但切點僅代表目標類連線點的部分資訊(類和方法的定位),所以僅有切點,我們無法制作出一個切面,必須結合增強才能製作出切面。

Spring使用org.springframework.aop.Advisor介面表示切面的概念,一個切面同時包含橫切程式碼和連線點資訊。

切面可以分為三類:一般切面、切點切面、引介切面,可以通過Spring定義的切面介面清楚的瞭解切面的分類。類圖如下:

Advisor:代表一般切面,它僅包含一個Advice,我們說過,因為Advice包含了橫切程式碼和連線點的資訊,所以Advice本身就是一個簡單的切面,只不過它代表的橫切的連線點是所有目標類的所有方法,因為這個橫切面太寬泛,所以一般不會直接使用;

PointcutAdvisor:代表具有切點的切面,它包含Advice和Pointcut兩個類,這樣我們就可以通過類、方法名以及方法方位等資訊靈活地定義切面的連線點,提供更具適用性的切面;

 

IntroductionAdvisor:代表引介切面,是對應引介增強的特殊切面,它應用於類層面上;

下面我們分析切點切面介面PointcutAdvisor,主要有6個具體的實現類,分別介紹如下:

1、DefaultPointcutAdvisor:最常用的切面型別,它可以通過任意Pointcut和Advice定義一個切面,唯一不支援的是引介的切面型別,一般可以通過擴充套件該類實現自定義的切面;

2、NameMatchMethodPointcutAdvisor:通過該類可以定義按方法名定義切點的切面;

3、RegexpMethodPointcutAdvisor:對於按正規表示式匹配方法名進行切點定義的切面,該類已經是功能齊備的實現類,一般情況下無須擴充套件該類,直接配置即可使用;

4、StaticMethodMatcherPointcutAdvisor:靜態方法匹配器切點定義的切面,預設情況下,匹配所有的目標類;

5、AspecJExpressionPointcutAdvisor:用於Aspecj切點表示式定義切點的切面;

6、AspecJPointcutAdvisor:用於AspecJ語法定義切點的切面。

下面具體講解PointcutAdvisor下常用的幾個類 。

4、靜態普通方法名匹配切面

 

StaticMethodMatcherPointcutAdvisor代表一個靜態方法匹配切面,它通過StaticMethodMatcherPointcut來定義切點,並通過類過濾和方法名來匹配所定義的切點.

業務類Waiter

/**
 * 服務員業務類
 */
public class Waiter {
    public void greetTo(String name) {
        System.out.println("Waiter Greet to " + name);
    }

    public void serverTo(String name) {
        System.out.println("Waiter Server to " + name);
    }
}

業務類Seller

public class Seller {
    /**
     * 和Waiter類中的同名的方法,目的是為了驗證僅僅織入了Waiter類中的greetTo方法
     */
    public void greetTo(String name) {
        System.out.println("Seller Greet to " + name);
    }
}

現在我們希望通過StaticMethodMatcherPointcutAdvisor定義一個切面,在Waiter#greetTo()方法呼叫前織入一個增強,即連線點為Waiter#greetTo()方法呼叫前的位置。

/**
 * 靜態普通方法名匹配切面
 */
public class GreetingAdvisor extends StaticMethodMatcherPointcutAdvisor {
    /**
     * 重寫matches方法,切點方法匹配規則:方法名為greetTo
     */
    public boolean matches(Method method, Class<?> aClass) {
        return "greetTo".equals(method.getName());
    }

    /**
     * 預設情況下,匹配所有的類,重寫getClassFilter,定義匹配規則切點型別匹配規則,為Waiter的類或者之類
     */
    public ClassFilter getClassFilter() {
        return new ClassFilter() {
            public boolean matches(Class<?> clazz) {
                return Waiter.class.isAssignableFrom(clazz);
            }
        };
    }
}

StaticMethodMatcherPointcutAdvisor 抽象類唯一需要定義的是matches()方法,在預設情況下,該切面匹配所有的類,這裡通過覆蓋getClassFilter()方法,讓它僅匹配Waiter類及其子類。

當然,Advisor還需要一個增強類的配合,我們來定義一個前置增強

/**
 * 前置增強
 */
public class GreetBeforeAdivce implements MethodBeforeAdvice {
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        // 輸出切點
        String clientName = (String) objects[0];
                System.out.println("How are you " + clientName + " ?(切點方法為:" + method + ")");
    }
}

我們使用Spring配置來定義切面等資訊

<!-- 配置切面:靜態方法匹配切面 start -->
    <!-- Waiter目標類 -->
    <bean id="waiterTarget" class="demo04.advisor.Waiter"/>
    <!-- Seller目標類 -->
    <bean id="sellerTarget" class="demo04.advisor.Seller"/>

    <!-- 前置增強 -->
    <bean id="greetBeforeAdvice" class="demo04.advisor.GreetBeforeAdivce"/>

    <!-- 切面 -->
    <bean id="greetAdvicesor" class="demo04.advisor.GreetingAdvisor"
          p:advice-ref="greetBeforeAdvice"/> <!-- 向切面注入一個前置增強 -->

    <!-- 通過父bean,配置公共的資訊 -->
    <bean id="parent" abstract="true"
          class="org.springframework.aop.framework.ProxyFactoryBean"
          p:interceptorNames="greetAdvicesor"
          p:proxyTargetClass="true"/>

    <!-- waiter代理 -->
    <bean id="waiter2" parent="parent" p:target-ref="waiterTarget"/>
    <!-- seller代理 -->
    <bean id="seller" parent="parent" p:target-ref="sellerTarget"/>
    <!-- 配置切面:靜態方法匹配切面 end -->

測試類

public class StaticMethodMatcherPointcutAdvisorTest {
    @Test
    public void test() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans/beans.xml");
        Waiter waiter = ctx.getBean("waiter2", Waiter.class);
        Seller seller = ctx.getBean("seller", Seller.class);
        waiter.greetTo("JayChou");
        waiter.serverTo("JayChou");
        seller.greetTo("JayChou");
    }
}

執行結果

How are you JayChou ?(切點方法為:public void demo04.advisor.Waiter.greetTo(java.lang.String))
Waiter Greet to JayChou
Waiter Server to JayChou
Seller Greet to JayChou

我們可以看到切面僅僅織入了Waiter.greetTo()方法呼叫前的連線點上, Waiter.serverTo()和Seller.greetTo()方法並沒有織入切面。

5、靜態正規表示式方法匹配切面

在 StaticMethodMatcherPointcutAdvisor中,僅能通過方法名定義切點,這種描述方式不夠靈活。假設目標類中有多個方法。且它們都滿足一定的命名規範,使用正規表示式進行匹配描述就要靈活多了。RegexpMethodPointcutAdvisor 是正規表示式方法匹配的切面實現類,該類已經是功能齊備的實現類,一般情況下無須擴充套件該類。如下配置即可

    <bean id="regexpAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"
          p:advice-ref="greetingAdvice">
        <property name="patterns">
                <list>
                        <value>.*greet.*</value>
                </list>
        </property>
    </bean>
    <bean id="waiter1" class="org.springframework.aop.framework.ProxyFactoryBean"
          p:interceptorNames="regexpAdvisor"
          p:target-ref="waiterTarget"
          p:proxyTargetClass="true"/>

6、動態切面

DynamicMethodMatcherPointcut 是一個抽象類,它將 isRuntime( )標識為 final 且返回true,這樣其子類就一定是一個動態切點。該抽象類預設匹配所有的類和方法,因此需要通過擴充套件該類編寫符合要求的動態切點。

/**
 * 會先執行靜態切點檢查,再執行動態切點檢查
 */
public class GreetingDynamicPointcut extends DynamicMethodMatcherPointcut {
    private static List<String> specialClientList = asList("JayChou", "LiHong");

    /**
     * 對方法進行靜態切點檢查
     */
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        System.out.println("呼叫了" + targetClass.getName() + "." + method.getName() + "做靜態檢查.");
        return "greetTo".equals(method.getName());
    }

    /**
     * 對方法進行動態切點檢查
     * 如果返回false,那麼該切點即不匹配
     * 思考:既然先匹配靜態方法,那麼為啥還需要動態匹配?
     * 原因:可以在執行期根據方法入參的值來判斷增強是否需要織入目標類的連線點上
     */
    public boolean matches(Method method, Class<?> aClass, Object[] objects) {
        String clientName = (String) objects[0];
        System.out.println("呼叫了" + aClass.getName() + "." + method.getName() + "." + clientName + "做動態檢查.");
        return specialClientList.contains(clientName);
    }

}

如上,GreetingDynamicPointcut 類既有用於靜態切點檢查的方法,又有用於動態檢查的方法。由於動態切點檢查會對效能造成很大的影響,所以應當儘量避免在執行時每次都對目標類的各個方法進行動態檢查。Spring 採用這樣的機制:在建立代理時對目標類的每個連線點使用靜態切點檢查,如果僅通過靜態切點檢查就可以知道連線點是不匹配的,則在執行時就不再進行動態檢查;如果靜態切點檢查是匹配的,則在執行時才進行動態切點檢查。在動態切點類中定義靜態切點檢查的方法可以避免不必要的動態檢查操作,從而極大地提高執行效率。所以如果自定義動態切面,可以先進行靜態切點和類檢查,再進行動態切點檢查會極大的提高效率。

進行xml檔案配置,如下

    <!-- 配置切面:動態態方法匹配切面 start -->
    <bean id="dynamicAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="pointcut"> <!--① 注入動態切點-->
            <bean class="demo04.advisor.GreetingDynamicPointcut"/>
        </property>
        <property name="advice"><!--② 注入增強-->
            <bean class="demo04.advisor.GreetBeforeAdivce"/>
        </property>
    </bean>

    <bean id="waiter3" class="org.springframework.aop.framework.ProxyFactoryBean"
          p:target-ref="waiterTarget"
          p:interceptorNames="dynamicAdvisor"
          p:proxyTargetClass="true" />
    <!-- 配置切面:動態態方法匹配切面 end -->

動態切面的配置和靜態切面的配置沒什麼區別。使用 DefaultPointcutAdvisor 定義切面,在 ① 處使用內部 Bean 方式注入動態切點 GreetingDynamicPointcut,在 ② 處注入增強。此外,DefaultPointcutAdvisor 還有一個 order 屬性,用於定義切面的織入順序。

測試類,如下

public class DynamicMethodMatcherPointcutTest {
    @Test
    public void test() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans/beans.xml");
        Waiter waiter = ctx.getBean("waiter3", Waiter.class);
        Seller seller = ctx.getBean("seller", Seller.class);
        waiter.greetTo("JayChou");
        waiter.greetTo("JayChou2");
        waiter.greetTo("JayChou3");
        waiter.serverTo("JayChou");
        waiter.serverTo("JayChou2");
        waiter.serverTo("JayChou3");
        seller.greetTo("LiHong");
    }
}

執行結果

呼叫了demo04.advisor.Waiter.greetTo做靜態檢查.
呼叫了demo04.advisor.Waiter.greetTo.JayChou做動態檢查.
How are you JayChou ?(切點方法為:public void demo04.advisor.Waiter.greetTo(java.lang.String))
Waiter Greet to JayChou
呼叫了demo04.advisor.Waiter.greetTo.JayChou2做動態檢查.
Waiter Greet to JayChou2
呼叫了demo04.advisor.Waiter.greetTo.JayChou3做動態檢查.
Waiter Greet to JayChou3
呼叫了demo04.advisor.Waiter.serverTo做靜態檢查.
Waiter Server to JayChou
Waiter Server to JayChou2
Waiter Server to JayChou3
Seller Greet to LiHong

通過以上輸出資訊,發現Spring 會在建立代理織入切面時,對目標類的所有方法進行靜態切點檢查;在生成織入切面的代理物件後,第一次呼叫代理類的每一個方法時都會進行一次靜態切點檢查,以後該方法的呼叫就不再執行靜態切點檢查;對於那些在靜態切點檢查時匹配的方法,在後續呼叫該方法時,將繼續會執行動態切點檢查。

動態代理的“動態”是相對於那些編譯期生成代理 Class 檔案和類載入期生成代理 Class 檔案而言的。動態代理是執行時動態產生的代理。在 Spring 中,不管是靜態切面還是動態切面,都是通過動態代理技術實現的。所謂靜態切面,是指在生成代理物件時就確定了增強是否需要織入目標類的連線點上;而動態切面是指必須在執行期根據方法入參的值來判斷增強是否需要織入目標類的連線點上。

7、流程切面

Spring 的流程切面由 DefaultPointcutAdvisor 和 ControlFlowPointcut 實現。流程切點代表由某個方法直接或間接發起呼叫的其他方法。來看下面的例項,假設通過一個 WaiterDelegate 類代理 Waiter 所有的方法,程式碼如下:

public class WaiterDelegate {
    private Waiter waiter;

    public void waiterService(String clientName) {
        waiter.greetTo(clientName);
        waiter.serverTo(clientName);
    }

    public void setWaiter(Waiter waiter) {
        this.waiter = waiter;
    }
}

如果希望所有由 WaiterDelegate#waiterService() 方法發起呼叫的其他方法都織入 GreetingBeforeAdvice 增強,就必須使用流程切面來完成目標。下面使用 DefaultPointcutAdvisor 配置一個流程切面來完成這一需求:

<!-- 配置切面:流程切面配置 start -->
    <!--切點配置-->
    <bean id="controlFlowPointcut" class="org.springframework.aop.support.ControlFlowPointcut">
        <!--指定一個類作為流程切點-->
        <constructor-arg type="java.lang.Class" value="demo04.advisor.WaiterDelegate"/>
        <!--指定一個方法作為流程切點-->
        <constructor-arg type="java.lang.String" value="waiterService"/>
    </bean>
    <!--配置增強-->
    <bean id="controllFlowAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"
          p:pointcut-ref="controlFlowPointcut"
          p:advice-ref="greetBeforeAdvice"/>
    <!--配置代理類-->
    <bean id="waiter4" class="org.springframework.aop.framework.ProxyFactoryBean"
          p:target-ref="waiterTarget"
          p:interceptorNames="controllFlowAdvisor"
          p:proxyTargetClass="true">
    </bean>
    <!-- 配置切面:流程切面配置 end -->

測試類

public class DefaultPointcutAdvisorTest {
    @Test
    public void test() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans/beans.xml");
        Waiter waiter = ctx.getBean("waiter4", Waiter.class);
        waiter.greetTo("JayChou");
        waiter.serverTo("JayChou");
        WaiterDelegate wd = new WaiterDelegate();
        wd.setWaiter(waiter);
        wd.waiterService("LiHong");
    }
}

執行結果

Waiter Greet to JayChou
Waiter Server to JayChou
How are you LiHong ?(切點方法為:public void demo04.advisor.Waiter.greetTo(java.lang.String))
Waiter Greet to LiHong
How are you LiHong ?(切點方法為:public void demo04.advisor.Waiter.serverTo(java.lang.String))
Waiter Server to LiHong

流程切面和動態切面從某種程度上說可以算是一類切面,因為二者都需要在執行期判斷動態環境。對於流程切面來說,代理物件在每次呼叫目標類方法時,都需要判斷方法呼叫堆疊中是否有滿足流程切點要求的方法。因此,和動態切面一樣,流程切面對效能的影響也很大。

8、複合切點切面

比如上面流程切面的例子中,只需要對WaiterDelegate#waiterService()中的greetTo方法進行切點,使用者可以可以通過一個切點同時滿足以上兩個匹配條件的連線點,而更好的方法是使用Spring提供的ComposablePointcut,通過多個切點的並集或者交集的方式組合起來,提供了切點之間的複合運算。

/**
 * 複合切點切面
 */
public class GreetingComposablePointcut {
    public Pointcut getIntersectionPointcut(){
        // 建立一個複合切點
        ComposablePointcut cp = new ComposablePointcut();
        // 建立一個流程切點
        Pointcut pt1 = new ControlFlowPointcut(WaiterDelegate.class,"waiterService");
        // 建立一個方法名切點
        NameMatchMethodPointcut pt2 = new NameMatchMethodPointcut();
        pt2.addMethodName("greetTo");
        // 將兩個切點進行交集操作
        return cp.intersection(pt1).intersection((Pointcut)pt2);
    }
}

xml配置

    <!-- 配置切面:複合切點切面配置 start -->
    <bean id="gcp" class="demo04.advisor.GreetingComposablePointcut" />
    <!-- 引用gcp.getIntersectionPointcut()方法所返回的複合切點 -->
    <bean id="composableAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"
          p:pointcut="#{gcp.intersectionPointcut}"
          p:advice-ref="greetBeforeAdvice" />

    <!-- p:interceptorNames指定使用的複合切面 -->
    <bean id="waiter5" class="org.springframework.aop.framework.ProxyFactoryBean"
          p:interceptorNames="composableAdvisor"
          p:target-ref="waiterTarget"
          p:proxyTargetClass="true" />
    <!-- 配置切面:複合切點切面配置 end -->

測試類

public class GreetingComposablePointcutTest {
    @Test
    public void test() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans/beans.xml");
        Waiter waiter = ctx.getBean("waiter5", Waiter.class);
        waiter.greetTo("JayChou");
        waiter.serverTo("JayChou");
        WaiterDelegate wd = new WaiterDelegate();
        wd.setWaiter(waiter);
        wd.waiterService("LiHong");
    }
}

執行結果

Waiter Greet to JayChou
Waiter Server to JayChou
How are you LiHong ?(切點方法為:public void demo04.advisor.Waiter.greetTo(java.lang.String))
Waiter Greet to LiHong
Waiter Server to LiHong


9、總結

1、Spring通過org.springframework.aop.Pointcut介面描述切點,Pointcut由ClassFilter和MethodMatcher構成。它通過ClassFilter定位到某些特定類上,通過MethodMatcher定位到某些特定方法上,這樣Pointcut就擁有了描述某些類的某些特定方法的能力。

2、切點型別共有6種

靜態方法切點:org.springframework.aop.support.StaticMethodMatcherPointcut
動態方法切點:org.springframework.aop.support.DynamicMethodMatcherPointcut
註解切點:org.springframework.aop.support.annotation.AnnotationMatchingPointcut
表示式切點:org.springframework.aop.support.ExpressionPointcut
流程切點:org.springframework.aop.support.ControlFlowPointcut
複合切點:org.springframework.aop.support.ComposablePointcut

3、Spring使用org.springframework.aop.Advisor介面表示切面的概念,一個切面同時包含橫切程式碼和連線點資訊。
切面可以分為三類:一般切面(Advisor)、切點切面(PointcutAdvisor)、引介切面(IntroductionAdvisor)

4、切點切面介面PointcutAdvisor,主要有6個具體的實現類:

DefaultPointcutAdvisor:最常用的切面型別,它可以通過任意Pointcut和Advice定義一個切面;
NameMatchMethodPointcutAdvisor:通過該類可以定義按方法名定義切點的切面;
RegexpMethodPointcutAdvisor:對於按正規表示式匹配方法名進行切點定義的切面; 
StaticMethodMatcherPointcutAdvisor:靜態方法匹配器切點定義的切面,預設情況下,匹配所有的目標類;
AspecJExpressionPointcutAdvisor:用於Aspecj切點表示式定義切點的切面。
AspecJPointcutAdvisor:用於AspecJ語法定義切點的切面。

相關文章