1. 程式人生 > >Spring自定義面向切面程式設計(AOP)

Spring自定義面向切面程式設計(AOP)

摘自標度 -> AOP為Aspect Oriented Programming的縮寫,意為:面向切面程式設計,通過預編譯方式和執行期動態代理實現程式功能的統一維護的一種技術。AOP是OOP的延續,是軟體開發中的一個熱點,也是Spring框架中的一個重要內容

個人理解的AOP產生由來:

面向切面程式設計是一種思想(AOP),以另一種角度來優化程式程式碼.
OOP的特點是封裝,繼承,多型,將功能封裝到多個物件中,而在封裝的過程中,難免會有相同的程式碼。
例如:日誌記錄,效能統計,安全控制,事務處理,異常處理等等。

比方說,我們現在有N個類,每個類都有N個方法,每個方法中都要寫一遍事務處理,提交回滾,這不是蛋疼嗎?
寫的不煩嗎?有沒有什麼辦法進行統一的管理?有的人可能會想把公共程式碼提取出來,寫成頗為複雜的抽象基類,在N個類之間呼叫,但是這樣又形成了高耦合,因為Java是單繼承,還佔用了一個繼承名額。

你想想啊。在公司的業務層寫業務程式碼,每個方法前需要寫日誌記錄,每個方法後又需要寫日誌記錄,業務的核心程式碼被淹沒在重複的日誌程式碼中,可讀性又差,程式碼又重複。

有些時候,我們需要在Controller層的某些方法前乾點別的事情?需要在Dao層的某些方法後乾點別的事情?難道就只有硬編碼到程式碼裡,而不能基於一些配置,一些約定,將程式碼靈活的織入進去嗎?

AOP是如何處理的呢,程式設計師將共有的功能抽取出來,由AOP動態的切入到程式碼中,實現零程式設計新增功能,程式碼更簡潔,很好的解決了程式碼冗餘問題。比方說業務層,就只需要專注於業務邏輯程式碼,這些瑣碎的事務處理,日誌記錄就完全不需要管了,是不是很開心?

接下來,關於Spring的AOP的幾個必須要清楚的概念:

通知(Advice):就是需要的功能的具體定義實現,在Java中,就是切面類的具體方法,這些方法就是通知。通知分為五種型別:環繞通知(Around),前置通知(Before),後置通知(AfterReturning),異常通知(AfterThrowing), 最終通知(After)。

連線點(Joinpoint):用我自己的理解來看,就是地點。目標呼叫前,呼叫後,丟擲異常時。就是目標執行的時候某個特定的點。程式在執行的時候能夠織入的一個點。這個點可以是方法,欄位,屬性,類,但是在目前的Spring中,只支援方法連線點。連線點只是一個概念,為了更好的理解,實際開發中我個人是沒有發現有啥用的…

切入點(Pointcut):切入點可以篩選通知應該織入到哪個包的哪個類的哪個方法連線點上,不同的通知肯定是切入到不同的連線點嘛,如何定位呢?Spring用正則表示式來定位。

目標物件(Target):這個真的沒啥好說的,打個比方:業務層的功能太多,有日誌記錄,核心業務程式碼,我們把日誌記錄抽取出來,業務層只剩下乾乾淨淨的核心邏輯程式碼,日誌記錄就可以用切面表示,而這個等待Spring動態織入功能的業務層就是目標物件,業務層會在毫不知情的情況下,加入新的功能,實現零程式碼編寫。

切面(Aspect):切面包含了通知和切入點,通知實現了具體的功能和應該什麼時候幹活,切入點決定了通知在什麼地方幹活,這就是一個完整的切面

引入(Introduction):SpringAOP是針對方法的增強,雖然沒有在類中新增新的方法,但是實實在在的在已有的方法上增加了新的功能,既然能夠在現有的基礎上增強,能不能直接性的新增新的功能,引入做到了…

織入(Weaving):將切面織入到目標中,這種行為就叫織入。有三種實現方式:
1.編譯期織入
2.類裝載期織入
3.動態代理織入
Spring預設使用J2SE動態代理(dynamic proxies)作為AOP的代理方式,也支援使用CGLIB代理,如果一個類沒有實現介面,就會使用CGLIB

代理(Proxy):通知被應用到了目標中,會產生一個新的類,這就是代理類,具有目標的功能加上共有的功能。代理類是執行時的產物,實際上是看不見的…

例子1:
核心業務介面:

public interface Sport{
    //運動
    void sport();
}

子類實現:

public class Run implements Sport{
    //核心業務
    @Override
    public void sport() {
        System.out.println("盡情的跑步");
    }
}
public class Swim implements Sport {
    //核心業務
    @Override
    public void sport() {
        System.out.println("歡樂的游泳");
    }
}

切面類:

//切面類
public class SportHelper implements MethodBeforeAdvice,AfterReturningAdvice{
    //前置通知
    @Override
    public void before(Method arg0, Object[] arg1, Object arg2)
            throws Throwable {
        System.out.println("運動前先熱身啊");
    }
    //後置通知
    @Override
    public void afterReturning(Object arg0, Method arg1, Object[] arg2,
            Object arg3) throws Throwable {
        System.out.println("運動後會流汗啊");
    }
}

配置資訊:

    <bean id="sportHelper" class="aop.SportHelper"></bean>
    <bean id="run" class="intercept.impl.Run"></bean>
    <bean id="swim" class="intercept.impl.Swim"></bean>

    <!-- 定義切入點   這裡是只要以sport結尾的方法都需要織入 -->
    <bean id="sportPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
      <property name="pattern" value=".*sport"/>
    </bean>

    <!-- 定義切面  包含通知和切入點 -->
    <bean id="sportHelperAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
         <property name="advice" ref="sportHelper"/>
         <property name="pointcut" ref="sportPointcut"/>
    </bean>

    <!-- 代理類  
        肯定需要指定目標物件,
        指定介面(這裡指定了介面,就肯定是用JDK的動態代理),
        指定切面
    -->
    <bean id="RunProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
         <property name="target" ref="run"/>
         <property name="interceptorNames" value="sportHelperAdvisor" />
         <property name="proxyInterfaces" value="intercept.Sport" />
    </bean>

    <!-- 代理類  
        肯定需要指定目標物件,
        指定介面(這裡指定了介面,就肯定是用JDK的動態代理),
        指定切面
    -->
    <bean id="SwimProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
         <property name="target" ref="swim"/>
         <property name="interceptorNames" value="sportHelperAdvisor" />
         <property name="proxyInterfaces" value="intercept.Sport" />
    </bean>

如果前面的幾個AOP術語看懂了,這些配置看懂是非常輕鬆的…

測試程式碼與結果:

    @Test
    public void test() {
       ApplicationContext appCtx = new ClassPathXmlApplicationContext("config/spring/applicationContext-transaction.xml");
        Sport run = (Sport)appCtx.getBean("RunProxy");
        run.sport();
        System.out.println();
        Sport swim = (Sport)appCtx.getBean("SwimProxy");
        swim.sport();
    }
運動前先熱身啊
盡情的跑步
運動後會流汗啊

運動前先熱身啊
歡樂的游泳
運動後會流汗啊

我只保留了核心的業務功能,其他共有的輔助功能交給SpringAOP去管理,每次我只需要關心核心業務的編寫,其他的配置一遍都不需要管了..

例子2(註解的形式):

核心業務:

@Component
public class Run implements Sport{
    //核心業務
    @Override
    public void sport() {
        System.out.println("盡情的跑步");
    }
}
@Component
public class Swim implements Sport {
    //核心業務
    @Override
    public void sport() {
        System.out.println("歡樂的游泳");
    }
}

切面類:

@Component
@Aspect
public class SportHelper1 {
    //切入點,以方法的形式存在
    @Pointcut("execution(* *.sport())")
    public void sportPoint(){}

    //前置通知
    @Before("sportPoint()")
    public void beforeSport(){
        System.out.println("運動前先熱身啊");
    }
    //後置通知
    @After("sportPoint()")
    public void afterSport(){
        System.out.println("運動後會流汗啊");
    }
}

配置資訊:

    <!-- 啟動對@AspectJ註解的支援 --> 
    <!-- proxy-target-class=true使用cglib代理,
        proxy-target-class=false使用JDK動態代理
        預設false
    -->
    <aop:aspectj-autoproxy proxy-target-class="false"/>

    <!-- 註解掃描 -->
    <context:component-scan base-package="aop.impl"/>

測試:

    @Test
    public void test1() {
        ApplicationContext appCtx = new ClassPathXmlApplicationContext("config/spring/applicationContext-transaction.xml");
        Sport run = (Sport)appCtx.getBean("run");
        run.sport();
        System.out.println();
        Sport swim = (Sport)appCtx.getBean("swim");
        swim.sport();
    }

結果:

運動前先熱身啊
盡情的跑步
運動後會流汗啊

運動前先熱身啊
歡樂的游泳
運動後會流汗啊

例子3:
核心業務與例子2是一樣的
切面類:

@Component
public class SportHelper1 {

    public void beforeSport(){
        System.out.println("運動前先熱身啊");
    }

    public void afterSport(){
        System.out.println("運動後會流汗啊");
    }

}

配置資訊:

    <aop:aspectj-autoproxy/> 
    <!-- 註解掃描 -->
    <context:component-scan base-package="aop.impl"/>

    <aop:config>
        <!-- 引入切面 -->
        <aop:aspect ref="sportHelper1">
            <!-- 配置通知與切入點 -->
            <aop:before method="beforeSport" pointcut="execution(* *.sport(..))"/>
            <aop:after method="afterSport" pointcut="execution(* *.sport(..))"/>
        </aop:aspect>
    </aop:config>

測試與結果與例子2是一模一樣的。反正程式碼寫來寫去,都是圍繞幾個關鍵點來的

講講寫程式碼時遇到的問題:

配置報錯?
檢查XML的名稱空間,是否還有哪些沒有配置

配置註解掃描獲取不到物件?

<context:component-scan base-package="aop.impl"/>

配置了註解的類一定要放到指定的包及子包下
主動獲取物件或者由IOC注入物件時,物件名預設是類名的首字母小寫

配置不理解?
先搞懂AOP思想,再來學Spring的AOP時,理解關鍵術語,一定要理解

不懂JDK動態代理和CGLIB?
也許會再寫一篇文章分析分析,也許不會。JDK動態代理這麼重要的東西,設計模式之代理模式,必須掌握的好嘛

切入點表示式不理解?
這個還是有些東西可以講的。。先放這

SpringAOP是Spring的一個很重要的模組,以上觀點僅代表個人理解,作者沒有能力閱讀英文文件,╮(╯▽╰)╭,如有錯誤的地方,請及時指出…