1. 程式人生 > >Spring學習筆記 —— AOP(面向切面程式設計) 之使用ProxyFactoryBean實現AOP

Spring學習筆記 —— AOP(面向切面程式設計) 之使用ProxyFactoryBean實現AOP

引言

到上一篇文章Spring學習筆記 —— Spring Context為止,我們已經基本瞭解Spring中的基本物件——Bean——的建立、相關屬性的注入以及獲取。其實在這不難發現,Spring的容器設計與Java的物件設計之間是有相似的地方的,比如BeanDefinition和Class物件,Bean和Object物件。

而Java的反射,則對應到了Spring中的AOP(面向切面程式設計),當然,AOP有著比反射更強大的功能以及更方便的配置。關於反射,我在前面已經有系列文章分析過了,有興趣的可以從這裡開始再閱讀一下。

Spring中的AOP分為兩種,一種是由Spring框架實現,基於ProxyFactoryBean

的AOP,而另外一種則是基於AspectJ的AOP,我們首先介紹前者,在後續的文章也會介紹後者。

本文主要分成三部分,AOP概念的介紹,Spring AOP例項以及Spring AOP具體實現的分析。

AOP(Aspect Oriented Programming)簡介

關於什麼是AOP,我想通過一個《Spring in Action》中的一個例子來解釋。

我們每家每戶都會有電腦,電腦會耗電,而每家每戶都會有一個電錶來記錄用電量,每個月會有人來查電錶,這樣電力公司就知道該收取多少電費了。

但是,如果沒有電錶,也沒有人來檢視用電量,而是由戶主來聯絡電力公司並報告自己的用電量。雖然可能會有一些戶主會很詳細地記錄所有的用電量,但是肯定大多數人並不會這麼做。因為每個人每天都有很多事要處理,而監控電力使用情況會浪費他們大量時間,不交電費對他們只有好處而沒有壞處。

軟體系統的某些功能就像我們家裡的電錶,這些功能需要應用到系統的多個地方,但卻不適合在應用到的地方被顯式呼叫。

監控電量是一個很重要的功能,但卻並不是大多數家庭重點關注的問題。監控電量更像是一個被動事件。

在軟體中,有些行為對於大多數應用都是通用的。日誌、安全和事務管理的確很重要。但它們是否應用物件主動參與的行為呢?如果讓應用物件只關注於自己所針對的業務領域問題,而其他方面的問題由其他應用物件來處理,會不會更好呢?

在軟體開發中,分佈於應用中多處的功能被稱為橫切關注點(cross-cutting concerns)。通常,這些橫切關注點從概念上是與應用的業務邏輯相分離的(但是往往直接嵌入到應用的業務邏輯之中)。將這些橫切關注點與業務邏輯相分離正式面向切面程式設計(AOP)所要解決的。

而在AOP中,會涉及到一些新的概念,現在先對這些概念進行描述。

名稱 含義
切面(Aspect) 作為提供業務程式碼之外的功能物件,如例子中提到的電錶,記錄了每家每戶的用電量。程式中有可能會是日誌,輸出特定的日誌結果。
目標物件(target object) 執行業務邏輯的物件,如例子中每個獨立的戶主。
織入(Weaving) 就是將切面中的物件與目標物件糅合在一起的過程。
連結點(JoinPoint) 需要插入切面的地方,通常指業務程式碼中具體某個物件的某個方法。而在例子中,就是需要裝電錶的家庭(有些地方可能不需要安裝電錶)。
通知(Advice) 通知是指在連線點上發生的事情,比如說電錶會記錄用電量。
切點(pointCut) 是指一系列連結點的組合

在Spring AOP中暫時不會用到切面的概念,但是在AspectJ AOP中則會用到。下面我們就實現一個Spring AOP吧。

Spring AOP例項

首先是TargetObject,TargetObjectSample.java

public class TargetObjectSample {
    public void getMessage() {
        System.out.println("getMsg!");
    }

    public void getPointCutMessage() {
        System.out.println("get point cut Msg!");
    }
}

再宣告一個連線點。PointCutSample.java

public class PointCutSample implements Pointcut{

    @Override
    public ClassFilter getClassFilter() {
        return ClassFilter.TRUE;
    }

    @Override
    public MethodMatcher getMethodMatcher() {
        return new MethodMatcher(){

            @Override
            public boolean matches(Method method, Class<?> targetClass) {
                if(method.getName().contains("PointCut")){
                    return true;
                }
                return false;
            }

            @Override
            public boolean isRuntime() {
                return true;
            }

            @Override
            public boolean matches(Method method, Class<?> targetClass, Object... args) {
                if(method.getName().contains("PointCut")){
                    return true;
                }
                return false;
            }};
    }

}

還有AdvisorSample.java

public class AdvisorSample implements PointcutAdvisor{
    @Override
    public Advice getAdvice() {
        return new BeforeAdviceSample();
    }

    @Override
    public boolean isPerInstance() {
        return false;
    }

    @Override
    public Pointcut getPointcut() {
        return new PointCutSample();
    }
}

最後是AdviceBeforeAdviceSample.java

public class BeforeAdviceSample implements MethodBeforeAdvice{

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("before method execution!");
    }

}

配置檔案beans.xml

<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
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <bean id="advisorSample" class="com.study.Spring.AdvisorSample"></bean>

    <bean id="targetSample" class="com.study.Spring.TargetObjectSample"></bean>

    <bean id="aopSample" class="org.springframework.aop.framework.ProxyFactoryBean">
       <property name="targetName">
            <value>targetSample</value>
        </property>
        <property name="interceptorNames">
            <list>
                <value>advisorSample</value>
            </list>
        </property>
    </bean>
</beans>

Maven 依賴

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.2.6.RELEASE</version>
    </dependency>

主函式App.java

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

        TargetObjectSample obj = (TargetObjectSample)app.getBean("aopSample");

        obj.getMessage();
        //getMsg!

        obj.getPointCutMessage();
        //before method execution!
        //get point cut Msg!
    }
}

得到以上結果,就證明已經成功的完成了。我們在不修改業務邏輯程式碼的情況下,成功地在方法執行前加上了自己想要執行的程式碼。

Spring AOP 實現分析

在引言中提到過,AOP其實是Java代理的一種增強,所以我們也能夠想到,最後會生成一個代理的物件。

但是,具體的生成過程又是怎樣的呢? 我們下面先通過一個時序圖來進行分析。

AOP Proxy物件生成過程

因為最後生成的物件仍舊是一個Bean物件,所以前面仍然是呼叫getBean方法獲取的。真正產生差異的地方在於,我們定義的bean是ProxyFactoryBean,它實現了FactoryBean這個介面。

public interface FactoryBean<T> {
    //返回Bean例項
    T getObject() throws Exception;
    //返回Bean的具體型別
    Class<?> getObjectType();
    //這個bean是否為單例
    boolean isSingleton();
}

因為必須要實現getBoject方法,所以我們就能夠自定義生成代理物件的時候需要經歷哪些步驟了。

首先,會根據我們在property中宣告的interceptorNames來初始化advisors(如果代理物件不是單例,初始化的advisors也不是真正的bean物件,advisors會在newPrototypeInstance時再進行初始化)。

其次,就是根據advisors生成cglib的代理物件。在Spring裡面會根據類的定義/bean的定義,決定要生成jdk原生的代理還是cglib的代理。因為我們示例中的類沒有實現任何介面,使用的是cglib的代理實現。
CglibAopProxy.getProxy

public Object getProxy(ClassLoader classLoader) {
        //省略debug程式碼

        try {
            Class<?> rootClass = this.advised.getTargetClass();
            Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");

            Class<?> proxySuperClass = rootClass;
            if (ClassUtils.isCglibProxyClass(rootClass)) {
                proxySuperClass = rootClass.getSuperclass();
                Class<?>[] additionalInterfaces = rootClass.getInterfaces();
                for (Class<?> additionalInterface : additionalInterfaces) {
                    this.advised.addInterface(additionalInterface);
                }
            }

            validateClassIfNecessary(proxySuperClass, classLoader);

            // 對cglib enhancer進行配置
            Enhancer enhancer = createEnhancer();
            if (classLoader != null) {
                enhancer.setClassLoader(classLoader);
                if (classLoader instanceof SmartClassLoader &&
                        ((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
                    enhancer.setUseCache(false);
                }
            }
            enhancer.setSuperclass(proxySuperClass);

                enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
            enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
            enhancer.setStrategy(new ClassLoaderAwareUndeclaredThrowableStrategy(classLoader));

            //根據advisors來生成callback物件
            Callback[] callbacks = getCallbacks(rootClass);
            Class<?>[] types = new Class<?>[callbacks.length];
            for (int x = 0; x < types.length; x++) {
                types[x] = callbacks[x].getClass();
            }
            // 根據advisors生成callbackFilter,確定哪個方法將會被代理。
            enhancer.setCallbackFilter(new ProxyCallbackFilter(
                    this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
            enhancer.setCallbackTypes(types);

            // 建立代理物件
            return createProxyClassAndInstance(enhancer, callbacks);
        }
        //異常處理
    }

而對應的類圖,則如下圖所示。
AOP - Cglib Class Diagram
這裡看大圖

小結

使用ProxyFactoryBean實現的AOP結構較為簡單,原理就是通過我們配置的屬性(interceptors),生成對應的callback以及callbackfilter,最後通過cglib生成動態代理類,實現代理的功能。

而對於使用JDK原生代理的,也不難想象代理的原理是通過配置的屬性,生成需要代理的介面以及設定對應的代理方法。

下一篇文章,我們會對AspectJ中的代理進行分析。

參考文章

  • 《Spring in Action 第三版》
  • 《深入分析Java Web技術內幕》