1. 程式人生 > >Spring原理與原始碼分析系列(七)- Spring AOP實現過程與實戰

Spring原理與原始碼分析系列(七)- Spring AOP實現過程與實戰

二、Spring AOP

1、什麼是Spring AOP

Spring AOP是Spring核心框架的重要組成部分,採用Java作為AOP的實現語言。與AspectJ實現AOP方式不同之處在於,Spring AOP僅支援方法級別的攔截。

2、Spring AOP的組成

Spring AOP中主要包括:Joinpoint、Pointcut、Advice、Aspect,下面一一介紹。

(1)Joinpoint

當我們將橫切邏輯織入到OOP模組當中,需要知道在哪些執行點進行織入操作。這些執行點就被稱為Joinpoint。

(通俗點理解:Joinpoint是一個點,規定了在哪個位置插入橫切邏輯)

Spring AOP只支援方法級別的Joinpoint織入,所以如果超出這個範圍,就需要通過AspectJ等進行操作。

(2)Pointcut

Pointcut通常使用正則表示式來描述多組符合條件的某個方法。

我們來看下Spring AOP中提供的Pointcut介面的定義:

public interface Pointcut {
    Pointcut TRUE = TruePointcut.INSTANCE;
    ClassFilter getClassFilter();
    MethodMatcher getMethodMatcher();
}

可以看到,Pointcut 通過ClassFilter 和MethodMatcher 兩個過濾來匹配要織入的方法。首先ClassFilter來匹配需要操作的類,然後使用MethodMatcher 匹配類中具體的方法。只有兩種型別均匹配後才會執行織入操作。
當然,Pointcut TRUE = TruePointcut.INSTANCE;

表示與class型別無關。

1)ClassFilter
ClassFilter介面作用是對Joinpoint所處物件進行Class級別的型別匹配。
下面進入看下ClassFilter的定義:

public interface ClassFilter {
    ClassFilter TRUE = TrueClassFilter.INSTANCE;
    boolean matches(Class<?> var1);
}

可以看到ClassFilter通過matches(Class

public interface MethodMatcher {
    MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;
    boolean
matches(Method var1, Class<?> var2); boolean isRuntime(); boolean matches(Method var1, Class<?> var2, Object[] var3); }

MethodMatcher中定義了兩個過載的matches()方法,一個不帶引數,一個帶引數Object[] var3,中間用isRuntime()隔離。
如果不需要檢查引數,就呼叫第一個matches()方法,此時isRuntime()返回false,第二個matches()方法就不會去執行,此時的MethodMatcher被稱之為StaticMethodMatcher;
如果需要對方法中的引數進行匹配,就需要呼叫第二個matches()方法,此時isRuntime()返回true。此時的MethodMatcher被稱之為DynamicMethodMatcher。

因此,在MethodMatcher基礎上,Pointcut又分為兩類:

  • StaticMethodMatcher
  • DynamicMethodMatcher

兩者關係圖如下:
這裡寫圖片描述

Pointcut常見的實現類有如下幾種:

  • NameMatchMethodPointcut
  • JdkRegexpMethodPointcut
  • Perl5RegexpMethodPointcut
  • AnnotationMatchingPointcut
  • ComposablePointcut
  • ControlFlowPointcut

1)NameMatchMethodPointcut
最簡單的Pointcut實現,是StaticMethodMatcher的子類,可以指定Joinpoint處的方法名稱進行匹配。
如:

new NameMatchMethodPointcut().setMappedName("login");

2)JdkRegexpMethodPointcut、Perl5RegexpMethodPointcut
StaticMethodMatcher下可以使用正則表示式對攔截方法進行匹配,
如:

new JdkRegexpMethodPointcut().setPattern(".*doSth().*");

3)AnnotationMatchingPointcut
AnnotationMatchingPointcut根據目標物件中是的存在指定型別的註解來匹配Joinpoint。
如:

new AnnotationMatchingPointcut(ClassLevelAnnotation.class, MethodLevelAnnotation.class);

以上程式碼會對使用類註解ClassLevelAnnotation、方法註解MethodLevelAnnotation所匹配到的方法進行攔截。

4)ComposablePointcut
ComposablePointcut可以進行Pointcut邏輯運算的Pointcut實現。不常用,不贅述。

5)ControlFlowPointcut
假設要織入的Joinpoint處所在的方法為login(),
ControlFlowPointcut可以指定具體的目標物件呼叫login()才進行攔截,別的目標物件呼叫login()方法時不會進行攔截。
不常用,不贅述。

(3)Advice

Advice是單一橫切關注點邏輯的載體,實現類將被織入到Pointcut規定的Joinpoint位置的橫切邏輯。

(通俗點理解就是:Advice就是我們實現的如日誌、安全、事務等邏輯操作,這些Advice需要被織入到業務程式碼中。)

Spring AOP中提供瞭如下Advice:
這裡寫圖片描述
主要分為兩大類:

  • per-class型別的Advice:該型別的Advice可以在目標物件類的所有例項之間共享,只是提供方法攔截的功能,不會為目標物件類新增新的特性;
    主要有:BeforeAdvice,ThrowsAdvice,AfterReturningAdvice,AroundAdvice;
  • per-instance型別的Advice:不可以在目標物件類的所有例項之間共享,可以為不同的例項物件儲存各自的狀態以及相關邏輯,在不改變目標類定義的情況下,為目標類新增新的屬性以及行為;
    Introduction是唯一一種per-instance型別的Advice。

1)Before Advice

BeforeAdvice所實現的橫切邏輯將在相應的Joinpoint之前執行,BeforeAdvice執行完之後將會從Joinpoint繼續執行。

2)ThrowsAdvice

ThrowsAdvice對應AOP中的AfterThrowingAdvice,通常用於對系統中特定的異常情況進行監控,以統一的方式對所發生的異常進行處理。

3)AfterReturningAdvice

在Joinpoint處所在的方法正常執行完成之後執行AfterReturningAdvice。

4)Around Advice

Spring中沒有直接定義Around Advice的實現介面,而是用MethodInterceptor來控制對相應Joinpoint的攔截行為。

5)Introduction

Introduction可以在不改變目標類定義的情況下,為目標類新增新的屬性以及行為。

(4)Aspect

Aspect是對系統的橫切關注點邏輯進行模組化封裝的AOP實體。Aspect於AOP對應Class與OOP。
一個Aspect包含多個Pointcut以及相關Advice。

Spring中提供了Advisor代表Aspect,Advisor一般只有一個Pointcut和Advice,可以看作是特殊的Aspect。
Spring中提供的Advisor關係圖如下:
這裡寫圖片描述
主要分為兩大類:

  • PointcutAdvisor:主要有DefaultPointcutAdvisor,NameMatchMethodPointcutAdvisor,RegexpMethodPointcutAdvisor;
  • IntroductionAdvisor:DefaultIntroductionAdvisor。

IntroductionAdvisor與PointcutAdvisor區別:IntroductionAdvisor只能用於類級別的攔截和Introduction型別的Advice;而PointcutAdvisor可以使用任意型別的Pointcut和除Introduction型別以外的Advice。

1)PointcutAdvisor的具體實現如下:
這裡寫圖片描述

DefaultPointcutAdvisor:最通用的PointcutAdvisor,可以使用任何型別的Pointcut和Advice(除Introduction型別的Advice)。
如:

DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
advisor.setPointcut(pointcut);//任何Pointcut型別
advisor.setAdvice();//除Introduction型別的Advice

NameMatchMethodPointcutAdvisor:限定了Pointcut的使用型別,即只能使用NameMatchMethodPointcut型別的Pointcut,其他除Introduction型別外的任何Advice都可以使用。
如:

NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor(advice);
advisor.setMappedName("login");

RegexpMethodPointcutAdvisor:限定了Pointcut的使用型別,即只能使用RegexpMethodPointcut型別的Pointcut,其他除Introduction型別外的任何Advice都可以使用。
如:

2)IntroductionAdvisor的具體實現如下:
這裡寫圖片描述

IntroductionAdvisor只有一個預設實現類:DefaultIntroductionAdvisor(另外一個通過AspectJ拓展),只能指定Introduction型別的Advice。

3)Ordered介面
可以看到,PointcutAdvisor、IntroductionAdvisor的實現類都實現了Ordered介面。
當多個Advisor介面中的Pointcut匹配了同一個Joinpoint時,就會在這同一個Joinpoint處執行Advice橫切邏輯的織入。
但是哪個Advisor中的Advice先執行呢?
Ordered介面就負責規定同一Joinpoint處Advice執行的先後順序。

Spring在處理同一Joinpoint處的多個Advisor的時候,會按照Ordered介面規定的順序號來執行,順序號越小,優先順序越高。
如:
假設有兩個Advisor,一個進行許可權檢查的PermissionAuthAdvisor;一個進行異常檢測的ExceptionAdvisor。

<bean id="permissionAuthAdvisor" class="....PermissionAuthAdvisor">
    <property name="order" value="1"
</bean>
<bean id="exceptionAdvisor" class="....ExceptionAdvisor">
    <property name="order" value="0"
</bean>

可以看到通過為order屬性賦值,可以規定ExceptionAdvisor首先執行。

3、Spring AOP的實現原理

同上節AOP中介紹的AOP實現原理一樣,Spring AOP在實現AOP的過程中使用了代理模式,並提供了以下兩種機制分別對實現了介面的目標類和沒有實現任何介面的目標類進行代理:

  • JDK動態代理;
  • CGLIB

Spring AOP框架內使用AopProxy對不同的代理機制進行了抽象並提供了相應的子類實現,相關結構圖如下:
這裡寫圖片描述
AopProxy有CglibAopProxy和JdkDynamicAopProxy兩種實現。
Spring AOP會通過策略模式—即根據目標物件是類還是介面來使用CglibAopProxy或JdkDynamicAopProxy完成對目標類進行代理。
不同的AopProxy實現的例項化過程採用工廠模式—即通過AopProxyFactory進行封裝(在下一小節織入過程詳細介紹AopProxyFactory是如何進行封裝的過程)。

Spring AOP採用ProxyFactory來完成織入操作(下小節會詳細介紹),下面來看下在Spring AOP中兩種機制的程式碼實現。
(1)基於介面的代理—JDK動態代理
程式碼實現:

//公共介面
public interface ILogin {
    void login();
}
//真正實現類
public class RealLogin implements ILogin {
    @Override
    public void login() {
        System.out.println("登入。。。");
    }
}
//Around Advice:橫切邏輯
public class LoginInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        try{
            System.out.println("攔截下,再執行");
            return methodInvocation.proceed();
        }finally {
            System.out.println("攔截結束");
        }
    }
}
//新增攔截器,完成織入操作,測試
public static void main(String[] args) {
        RealLogin login = new RealLogin();
        //ProxyFactory會自動檢測MockTask實現的介面
        ProxyFactory weaver = new ProxyFactory(login);
        //設定Advisor(Aspect,包括設定Pointcut和Advice)
        NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor();
        advisor.setMappedNames("login");//設定攔截的方法
        advisor.setAdvice(new LoginInterceptor());//設定Advice

        //新增攔截器,完成織入操作
        weaver.addAdvisor(advisor);
        ILogin loginProxy = (ILogin) weaver.getProxy();//獲取代理物件
        loginProxy.login();

    }

最後輸出:

攔截下,再執行
登入。。。
攔截結束
class com.sun.proxy.$Proxy0

可以看出獲取的代理物件是Proxy型別。

(2)基於類的代理—CGLIB
程式碼實現:

//沒有實現任何介面的目標類
public class LoginWithoutInterface {
    public void login(){
        System.out.println("沒有介面的登入--基於類的代理");
    }
}
//Around Advice(同上)
public class LoginInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        try{
            System.out.println("攔截下,再執行");
            return methodInvocation.proceed();
        }finally {
            System.out.println("攔截結束");
        }
    }
}
//織入操作
public static void main(String[] args) {
   LoginWithoutInterface login = new LoginWithoutInterface();
   ProxyFactory weaver = new ProxyFactory(login);
   NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor();
   advisor.setMappedNames("login");
   advisor.setAdvice(new LoginInterceptor());
   weaver.addAdvisor(advisor);

    //返回代理物件
   LoginWithoutInterface loginProxy = (LoginWithoutInterface) weaver.getProxy();
   loginProxy.login();
   System.out.println(loginProxy.getClass());
}

輸出:

攔截下,再執行
沒有介面的登入–基於類的代理
攔截結束
class com.wgs.spring.aop2.LoginWithoutInterface$$EnhancerBySpringCGLIB$$d1551df8

可以看出最後返回的代理物件是屬於SpringCGLIB的,表明基於CGLIB的代理模式完成了AOP流程。

三、Spring AOP的織入操作過程

第二節我們介紹了Advisor(Aspect),一個Advisor中有攔截的橫切邏輯Advice以及定義了織入位置的Pointcut。下面如何將這個Advisor織入到具體位置處Joinpoint處呢?

Spring AOP採用ProxyFactory等織入器來完成整個織入過程。
這裡寫圖片描述

其中,

  • ProxyFactory:Spring AOP提供的最基本的織入器;
  • ProxyFactoryBean:可以結合Spring IoC容器,在容器中對Pointcut和Advice等進行管理;
  • AspectJProxyFactory:基於 AspectJ的ProxyFactory。

下面來詳細介紹ProxyFactory和ProxyFactoryBean這兩個織入器。

1、ProxyFactory

==(1)ProxyFactory程式碼實現==

ProxyFactory是Spring AOP提供的最基本的織入器。在完成織入過程後,ProxyFactory會返回織入橫切邏輯的目標物件的代理物件。

ProxyFactory完成整個織入過程需要兩個要素:

  • 要進行織入操作的目標物件:可以通過ProxyFactory的setTarget(Obj)方法或者構造器設定目標物件;
  • 應用到目標物件的Advisor(Aspect):通過ProxyFactory的addAdvisor(advisor)新增Advisor,完成橫切邏輯的織入。

具體實現的程式碼我們在第3節Spring AOP的實現原理完成動態代理 和CGLIB代理中已經完成,大致流程如下:

//設定目標物件
ProxyFactory weaver = new ProxyFactory(obj);
//(weaver.setTarget(obj);)
//設定Advisor(Aspect,包括設定Pointcut和Advice),新增攔截器,完成織入操作
weaver.addAdvisor(advisor);
//返回代理物件
Object objProxy = (Object ) weaver.getProxy();

可以看到,ProxyFactory返回的是具有織入功能的代理物件,而不是已經新增橫切邏輯的目標物件,這點需要注意。

我們在第3節已經看到,Spring AOP的代理過程分為JDK動態代理和CGLIB兩種機制,那麼時候會使用基於類的代理呢?

  • 如果目標類沒有實現任何介面,就必須使用基於類的代理;
  • 如果目標類實現了介面,可以通過ProxyFactory設定proxyTargetClass或者optimize的屬性為true,這樣就會使用基於類的代理機制,即:
    waver.setProxyTargetClass(true);weaver.setOptimize(true);

==(2)ProxyFactory實現原理==
下面來看一看ProxyFactory是如何實現織入過程並返回代理物件的。首先ProxyFactory類層次圖如下:
這裡寫圖片描述
可以看到ProxyFactory繼承了ProxyCreatorSupport類,而ProxyCreatorSupport又繼承了AdvisedSupport類,AdvisedSupport繼承了ProxyConfig類同時實現了Advised介面,下面我們從AdvisedSupport開始看起,看看AdvisedSupport代表了什麼。

Ⅰ)AdvisedSupport類
由圖可以看出,AdvisedSupport繼承了ProxyConfig類同時實現了Advised介面,因此AdvisedSupport承載的資訊分為兩類:

  • ProxyConfig:記載生成代理物件的控制資訊;
  • Advised:記載生成代理物件的必要資訊,如目標類、Advisor、Advice等。

ProxyConfig:普通的JavaBean,記載生成的代理物件的控制資訊,包含5個屬性:
org.springframework.aop.framework.ProxyConfig

proxyTargetClass:屬性設定為true時,使用基於類的代理方式,即使用CGLIB對目標物件進行代理,預設為false;
optimize:為true時,使用CGLIB對目標物件進行代理,預設為false;
opaque:控制生成的代理物件是否可以強制換為Advised型別,預設false;
exposeProxy:可以將生成的代理物件繫結到ThreadLocal當中,預設false;
frozen:設定為true時,對生成的代理物件資訊就不允許修改。

Advised:提供生成的代理物件的具體資訊,如針對哪些目標類生成代理物件,加入什麼樣的橫切邏輯等。
返回的代理物件可以轉換為Advised型別,這樣就可以查詢到代理物件中的相關的目標類、Advice等資訊,也可以進行新增、移除Advisor等操作。
org.springframework.aop.framework.Advised

Ⅱ)ProxyCreatorSupport
ProxyCreatorSupport只是將一些公用的邏輯抽取到了ProxyCreatorSupport類當中,其繼承了AdvisedSupport類,因此也具有ProxyConfig和Advised等功能。
ProxyCreatorSupport內部持有一個AopProxyFactory的例項。

Ⅲ)ProxyFactory
ProxyFactory繼承自ProxyCreatorSupport,由於ProxyCreatorSupport內部持有一個AopProxyFactory的例項,ProxyFactory當然可以獲取到AopProxyFactory生成的代理物件。
這裡寫圖片描述

public interface AopProxyFactory {
    AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException;
}

AopProxyFactory 持有AdvisedSupport ,根據AdvisedSupport 例項提供的相關資訊,來決定生成CglibAopProxy或者JdkDynamicAopProxy。

走到這裡,我們已經理清了大致思路,即:
ProxyFactory根據AdvisedSupport承載的ProxyConfig和Advised中提供的代理物件的控制資訊和具體資訊(ProxyConfig提供的proxyTargetClass或optimize的屬性),選擇AopProxy中的CglibAopProxy或者JdkDynamicAopProxy去實現代理物件的生成,而不同的AopProxy是由AopProxyFactory的實現類DefaultAopProxyFactory通過工廠模式生成的。

2、ProxyFactoryBean

ProxyFactory是Spring AOP提供的最基本的織入器,可以獨立與IoC容器之外來使用Spring的AOP支援。當然如果想要結合IoC容器,在容器中對Pointcut和Advic進行管理,就需要使用到ProxyFactoryBean織入器。

ProxyFactoryBean的本質就是生成Proxy的FactoryBean,類似FactoryBean的作用一樣。
ProxyFactoryBean的getObject()方法返回的是目標物件的代理物件。
由於ProxyFactoryBean同ProxyFactory一樣,都繼承自ProxyCreatorSupport,而ProxyCreatorSupport中已經設定好了目標物件、Advice等資訊,ProxyFactoryBean可以呼叫父類ProxyFactoryBean中的createAopProxy()直接返回建立的代理物件即可,只是在返回的時候需要根據返回的代理物件是Singleton還是prototype型別,稍微進行修改。

ProxyFactoryBean的getObject()方法:

    public Object getObject() throws BeansException {
        initializeAdvisorChain();
        if (isSingleton()) {
            return getSingletonInstance();
        }
        else {
            if (this.targetName == null) {
                logger.warn("Using non-singleton proxies with singleton targets is often undesirable. " +
                        "Enable prototype proxies by setting the 'targetName' property.");
            }
            return newPrototypeInstance();
        }
    }

可以看到,如果返回的代理物件型別是Singleton型別,就將生成的代理物件放入快取;否則每次都生成新的代理物件。

下面看看在IoC容器如何使用ProxyFactoryBean生成代理物件。
(1)基於介面代理的實現程式碼

//公共介面
public interface ILogin {
    void login();
}
//具體實現類
public class RealLogin implements ILogin {
    @Override
    public void login() {
        System.out.println("登入。。。");
    }
}

//動態代理類
public class DynamicProxy implements InvocationHandler {
    //要代理的物件
    private Object obj;

    //實際注入的物件
    public DynamicProxy(Object realObj){
        this.obj = realObj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("攔截下,再執行");
        method.invoke(obj, args);
        System.out.println("攔截結束");

        return null;
    }
}

proxyfactorybean.xml:在classpath下配置ProxyFactoryBean
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--設定pointcut:攔截方法-->
    <bean id="login-pointcut" class="org.springframework.aop.support.NameMatchMethodPointcut">
        <property name="mappedName" value="login"></property>
    </bean>
    <!--設定advice:橫切邏輯-->
    <bean id="login-advice" class="com.wgs.spring.aop2.DynamicProxy"></bean>
    <!--設定advisor:新增pointcut和pointcut-->
    <bean id="login-advisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="pointcut" ref="login-pointcut"></property>
        <property name="advice" ref="login-advice"></property>
    </bean>

    <!--設定ProxyFactoryBean,新增攔截器,最後返回代理物件loginProxy-->
    <bean id="loginProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
    <!--設定目標物件-->
        <property name="target">
            <bean id="realTask" class="com.wgs.spring.aop2.RealLogin"></bean>
        </property>
        <!--設定公共介面-->
        <property name="proxyInterfaces">
            <value>com.wgs.spring.aop2.ILogin</value>
        </property>
        <!--新增攔截器-->
        <property name="interceptorNames">
            <list>
                <value>login-advisor</value>
            </list>
        </property>
    </bean>
</beans>


//測試
public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("proxyfactorybean.xml");
    //返回IoC容器中獲取的代理物件
    ILogin loginProxy = (ILogin) ctx.getBean("loginProxy");
    loginProxy.login();
}

輸出:

攔截下,再執行
登入。。。
攔截結束
class com.sun.proxy.$Proxy2

可以看到,是基於動態代理的方式。

(2)基於類代理的實現程式碼
基於類的代理實現與上大致類似,只不過需要將proxyTargetClass屬性設定為true,即採用類代理的方式。
具體實現程式碼如下:

//目標類,沒有實現任何介面
public class LoginWithoutInterface {
    public void login(){
        System.out.println("沒有介面的登入--基於類的代理");
    }
}

//Around Advice
public class LoginInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        try{
            System.out.println("攔截下,再執行");
            return methodInvocation.proceed();
        }finally {
            System.out.println("攔截結束");
        }
    }
}

proxyfactorybean2.xml:配置ProxyFactoryBean,設定proxyTargetClass採用類代理方式,返回代理物件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--設定pointcut:攔截方法-->
    <bean id="login-pointcut" class="org.springframework.aop.support.NameMatchMethodPointcut">
        <property name="mappedName" value="login"></property>
    </bean>
    <!--設定advice:橫切邏輯-->
    <bean id="login-advice" class="com.wgs.spring.aop2.LoginInterceptor"></bean>
    <!--設定advisor:新增pointcut和pointcut-->
    <bean id="login-advisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="pointcut" ref="login-pointcut"></property>
        <property name="advice" ref="login-advice"></property>
    </bean>

    <!--設定ProxyFactoryBean,新增攔截器,最後返回代理物件loginProxy-->
    <bean id="loginProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <!--設定目標物件-->
        <property name="target">
            <bean id="realTask" class="com.wgs.spring.aop2.LoginWithoutInterface"></bean>
        </property>
        <!--設定proxyTargetClass屬性,true:表示採用類代理的方式-->
        <property name="proxyTargetClass">
            <value>true</value>
        </property>
        <!--新增攔截器-->
        <property name="interceptorNames">
            <list>
                <value>login-advisor</value>
            </list>
        </property>
    </bean>
</beans>

//測試
public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("proxyfactorybean2.xml");
    LoginWithoutInterface loginProxy = (LoginWithoutInterface) ctx.getBean("loginProxy");
    loginProxy.login();
}

最後輸出:

class com.wgs.spring.aop2.LoginWithoutInterface$$EnhancerBySpringCGLIB$$2ca0f625
攔截下,再執行
沒有介面的登入–基於類的代理
攔截結束

可以看到是基於CGLIB的代理方式獲取代理物件,完成織入過程的:EnhancerBySpringCGLIB

四、@AspectJ形式的Spring AOP

Spring2.0之後,就支援AspectJ形式的AOP實現。

@AspectJ代表一種定義Aspect的風格,讓我們能夠以POJO的形式定義Aspect,沒有其他介面定義的限制。
@AspectJ形式的AOP需要使用相應的註解標註Aspect定義的POJO類,之後,Spring會根據標註的註解搜尋這些Aspect定義類,將其織入系統。

Spring AOP使用AspectJ的類庫進行Pointcut的解析和匹配,最終的實現機制還是Spring AOP使用代理模式處理橫切邏輯的織入。

下面,通過一個簡單的例子,來看下@AspectJ形式的Spring AOP是如何完成橫切邏輯的織入過程的。

//1 定義目標類,通過@EnableAspectJAutoProxy註解表示基於類代理
Component
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Target {
    public void method1(){
        System.out.println("============method2正常方法開始執行了============");
    }

}


//定義切面類,設定pointcut和Advice
@Component
@Aspect
public class LogAspect {

    @Pointcut("execution(void method1())")
    private void pointcutName() {

    }

    @Before("pointcutName()")
    public void beforeMethod(){
        System.out.println("before。。。");
    }

    @After("pointcutName()")
    public void afterMethod(){
        System.out.println("after:類似finally方法...");
    }

    @AfterReturning("pointcutName()")
    public void doAfterReturning(){
        System.out.println("AfterReturning:在pointcutName所在方法正常執行結束後執行after advice");
    }

    @AfterThrowing(value = "pointcutName()",throwing = "e")
    public void doAfterThrowing(RuntimeException e){
        System.out.println("異常:"+e);
        System.out.println("AfterThrowing:在pointcutName所在方法執行異常時執行");
    }

    @Around("pointcutName()")
    public Object  doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("進入Around advice...");
        Object o = joinPoint.proceed();
        System.out.println("離開Around advice...");
        return o;
    }

}

//3 測試
public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
     Target target = (Target) ctx.getBean("target");
     target.method1();
}

最後的輸出結果:

進入Around advice…
before。。。
====method2正常方法開始執行了====
離開Around advice…
after:類似finally方法…
AfterReturning:在pointcutName所在方法正常執行結束後執行after advice

用一張圖表示上圖的織入位置和橫切邏輯的織入過程:
這裡寫圖片描述

由於程式沒有發生異常,因此@AfterThrowingAdvice沒有執行織入操作。

從上面例子我們可以總結基於@AspectJ形式的Spring AOP完成過程:
首先編寫目標類 ,在 目標類中加入註解
@EnableAspectJAutoProxy(proxyTargetClass = true)設定使用基於類的代理方式;然後寫一個Aspect切面,並通過註解@Aspect註明,在切面類設定Pointcut和各種Advice;
最後啟動目標類即可看到Aspect中具有橫切邏輯的各種Advice已經生效。

1、@AspectJ形式的Pointcut—@Pointcut

@Pointcut需要在@Aspect定義內,常用execution匹配指定方法簽名的Joinpoint。

@Aspect形式宣告的所有Pointcut表示式,在Spring AOP內部都會解析轉化為具體的Pointcut物件(Spring AOP有自己的Pointcut介面),即@Aspect形式宣告的Pointcut都會轉化為一個專門面向AspectJ的Pointcut實現。

2、@AspectJ形式的Advice

@AspectJ形式的Advice註解包括:

  • @Before:前置Advice,在Pointcut執行之前執行;
  • @After:後置Advice;類似finally方法,無論Pointcut定義的方法是否丟擲異常最後都會執行after advice
  • @AfterReturning:宣告返回Advice,在pointcut定義的方法正常執行結束執行AfterReturning Advice;
  • @AfterThrowing:異常Advice,在pointcut定義的方法丟擲異常時執行;
  • @Around:環繞Advice,可以pointcut定義的方法前後執行;

具體的使用方法見上面的例子,在此不一一贅述。

3、@AspectJ形式的Aspect—@Aspect

AspectJ形式的Aspect需要注意的地方就是切面Aspect中Advice的執行順序:

  • 當Advice宣告在同一Aspect內的時候,先宣告的擁有高優先順序。
    對於Before Advice,先宣告優先順序越高,越快執行;
    對於AfterReturning Advice來說,先宣告優先順序越高,但是執行越靠後;

  • 當Advice宣告在不同的Aspect內的時候,需要每個Aspect去實現Ordered介面,然後設定order屬性,order越小,優先順序越高。

舉個栗子:

//1 目標類不變:
@Component
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Target {
    public void method1(){
        System.out.println("============method1正常方法開始執行了============");
    }
}

//2 切面類1
@Aspect
@Component
public class SecurityAspect implements Ordered{

    @Pointcut("execution(void method1())")
    public void pointcutName(){
    }

    @Before("pointcutName()")
    public void beforeMethod(){
        System.out.println("SecurityAspect1 beforeMethod");
    }

    @AfterReturning("pointcutName()")
    public void aftetReturningMethod(){
        System.out.println("SecurityAspect1 aftetReturningMethod");
    }

    @Override
    public int getOrder() {
        return 100;
    }

}

//3 切面類2
@Aspect
@Component
public class SecurityAspect2 implements Ordered{

    @Pointcut("execution(void method1())")
    public void pointcutName(){
    }

    @Before("pointcutName()")
    public void beforeMethod(){
        System.out.println("SecurityAspect2 beforeMethod");
    }

    @AfterReturning("pointcutName()")
    public void aftetReturningMethod(){
        System.out.println("SecurityAspect2 aftetReturningMethod");
    }
    @Override
    public int getOrder() {
        return 200;
    }

}

//4 測試
public static void main(String[] args) {
   ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
   Target target = (Target) ctx.getBean("target");
    target.method1();
}

輸出結果:

SecurityAspect1 beforeMethod
SecurityAspect2 beforeMethod
============method1正常方法開始執行了============
SecurityAspect2 aftetReturningMethod
SecurityAspect1 aftetReturningMethod

上面,切面類1-SecurityAspect的order是100,切面類2-SecurityAspect2的order是200,因此SecurityAspect優先順序更高。因此SecurityAspect的beforeMethod應該先執行,而其afterReturningMethod後在執行。

4、基於@AspectJ形式的Spring AOP實現方式

基於@AspectJ形式的Spring AOP實現方式有兩種:

  • (1)通過程式設計方式織入;
  • (2)通過自動代理的方式織入

(1)通過程式設計方式織入
在講解織入器的時候,繼承自ProxyCreatorSupprot有3個基本類:
ProxyFactory、ProxyFactoryBean、AspectJProxyFactory。
我們已經介紹過前兩個織入器,在此將介紹AspectJProxyFactory織入器。
AspectJProxyFactory可以將Aspect織入到目標類中,
如下:

//目標類
public class Target2 {
    public void target(){      System.out.println("============target============");
    }
 }  

//切面
@Aspect
@Component
public class SecurityAspect {

    @Pointcut("execution(void target())")
    public void pointcutName(){
    }

    @Before("pointcutName()")
    public void beforeMethod(){
        System.out.println("SecurityAspect1 beforeMethod");
    }

    @AfterReturning("pointcutName()")
    public void aftetReturningMethod(){
        System.out.println("SecurityAspect1 aftetReturningMethod");
    }

}
//織入 
public static void main(String[] args) {
  AspectJProxyFactory weaver = new AspectJProxyFactory();
  //設定基於類代理的方式
   weaver.setProxyTargetClass(true);
   //設定目標類
   weaver.setTarget(new Target2());
   //新增切面
   weaver.addAspect(SecurityAspect.class);
   //獲取代理物件
   Object proxy  = weaver.getProxy();
   ((Target2)proxy).target();
    }

AspectJProxyFactory通過反射獲取到Aspect中的@Pointcut定義的AspectJ形式的Pointcut定義之後,在Spring AOP內部會構造一個對應的AspectJExpressionPointcut物件例項,AspectJExpressionPointcut內部持有通過反射獲取到的Pointcut表示式。
然後Spring AOP框架內部處理Pointcut匹配的邏輯,即通過ClassFilter和MethodMatcher進行具體Pointcut匹配。
不過這個過程會委託AspectJ類庫中的相關類來做具體的解析工作。

(2)通過自動代理的方式織入
實際上,我們在開始舉得例子中使用的就是自動代理的方式進行織入操作的,也推薦此種方法。

Spring AOP提供了AutoProxyCreator類類完成自動代理的過程。
可以在目標類上加上
@EnableAspectJAutoProxy(proxyTargetClass = true)
註解
或者在配置檔案中加上
<aop:aspectj-autoproxy proxy-target-class="true"/>
即可完成整個織入過程。

五、基於Schema形式的Spring AOP

基於Schema的AOP是新增加的一種AOP的使用方式,基於Schema的XML配置,提供了獨有的名稱空間。

要使用基於Schema形式的Spring AOP,IoC容器的配置檔案需要使用基於Schema的XML,同時在檔案頭中針對AOP的名稱空間宣告,如下所示:

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

</beans>

基於Schema形式的Spring AOP使用方法也較為簡單,大致如下,在此不再詳細介紹,詳情請看官方文件。

//1 目標類
public class Target {
    public void target(){
        System.out.println("============target正常方法開始執行了============");
    }
}

//2 切面類
public class SecurityAspect {

    public void beforeMethod(){
        System.out.println("SecurityAspect1 beforeMethod....");
    }

    public void aftetReturningMethod(){
        System.out.println("SecurityAspect1 aftetReturningMethod....");
    }

}
// 3 基於Schema的Spring AOP的實現
<?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"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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">


    <bean id="target" class="com.wgs.aop.Target"></bean>
    <bean id="securityAspect" class="com.wgs.aop.SecurityAspect"></bean>
    <aop:config proxy-target-class="true">
        <aop:pointcut id="pointcutName" expression="execution(* com.wgs.aop.Target.target())"></aop:pointcut>
        <aop:aspect ref="securityAspect" order=