Spring AOP (上)

huidaoli發表於2013-08-01

工作忙,時間緊,不過事情再多,學習是必須的。記得以前的部門老大說過:“開發人員不可能一天到晚只有工作,肯定是需要自我學習。第一:為了更充實自己,保持進步狀態。第二:為了提升技術,提高開發能力。第三:保持程式設計師對技術和學習的熱情,工作的激情。程式設計師還是需要把基礎打紮實,修煉自己的內功。” 所以趕緊把學習的東西總結一下,加深印象。之前有說了下AOP的原理 (http://www.cnblogs.com/yanbincn/archive/2012/06/01/2530377.html) 。基於代理模式,瞭解了jdk動態代理和cglib的用法。但是在真正的使用AOP的時候,不可能寫這麼厚重的方法。

  Spring有兩大核心,IOC和AOP。IOC在java web專案中無時無刻不在使用。然而AOP用的比較少,的確也是一般的專案用的場所不多。事務控制基本都用,但卻是Spring封裝的不需要我們再去實現,但Spring的AOP遠不止這些,不能因為專案中沒有使用,而不去學習及理解。我覺得這是作為一個java web軟體開發人員必須具備的技能。業內很多將AOP應用在日誌記錄上,可惜我們專案沒這麼做,後面需要學習下。在這先把Spring AOP的基本用法,在腦子裡理一邊,做一次積累。

1、概念術語  

  在開始之前,需要理解Spring aop 的一些基本的概念術語(總結的個人理解,並非Spring官方定義):

  切面(aspect):用來切插業務方法的類。

  連線點(joinpoint):是切面類和業務類的連線點,其實就是封裝了業務方法的一些基本屬性,作為通知的引數來解析。

  通知(advice):在切面類中,宣告對業務方法做額外處理的方法。

  切入點(pointcut):業務類中指定的方法,作為切面切入的點。其實就是指定某個方法作為切面切的地方。

  目標物件(target object):被代理物件。

  AOP代理(aop proxy):代理物件。

  通知:

  前置通知(before advice):在切入點之前執行。

  後置通知(after returning advice):在切入點執行完成後,執行通知。

  環繞通知(around advice):包圍切入點,呼叫方法前後完成自定義行為。

  異常通知(after throwing advice):在切入點丟擲異常後,執行通知。

2、Spring AOP環境

  要在專案中使用Spring AOP 則需要在專案中匯入除了spring jar包之外,還有aspectjweaver.jar,aopalliance.jar ,asm.jar 和cglib.jar 。

好了,前提工作準備完成,Spring 提供了很多的實現AOP的方式,在學習過程中,循序漸進。進行Spring 介面方式,schema配置方式和註解的三種方式進行學習。好了廢話不多說了,開始spring aop學習之旅:

3、方式一:AOP介面

  利用Spring AOP介面實現AOP,主要是為了指定自定義通知來供spring AOP機制識別。主要介面:前置通知 MethodBeforeAdvice ,後置通知:AfterReturningAdvice,環繞通知:MethodInterceptor,異常通知:ThrowsAdvice 。見例子程式碼:

a、業務介面:

View Code
/**
 * 代理類介面,也是業務類介面<br>
 * 
 * 利用介面的方式,spring aop 將預設通過jdk 動態代理來實現代理類<br>
 * 不利用介面,則spring aop 將通過cglib 來實現代理類
 * 
 * @author yanbin
 * 
 */
public interface IBaseBusiness {

    /**
     * 用作代理的切入點方法
     * 
     * @param obj
     * @return
     */
    public String delete(String obj);

    /**
     * 這方法不被切面切
     * 
     * @param obj
     * @return
     */
    public String add(String obj);

    /**
     * 這方法切不切呢?可以設定
     * 
     * @param obj
     * @return
     */
    public String modify(String obj);

}

b、業務類:

View Code
/**
 * 業務類,也是目標物件
 * 
 * @author yanbin
 * 
 */
public class BaseBusiness implements IBaseBusiness {

    /**
     * 切入點
     */
    public String delete(String obj) {
        System.out.println("==========呼叫切入點:" + obj + "說:你敢刪除我!===========\n");
        return obj + ":瞄~";
    }

    public String add(String obj) {
        System.out.println("================這個方法不能被切。。。============== \n");
        return obj + ":瞄~ 嘿嘿!";
    }

    public String modify(String obj) {
        System.out.println("=================這個也設定加入切吧====================\n");
        return obj + ":瞄改瞄啊!";
    }

}

c、通知類:

前置通知:

View Code
/**
 * 前置通知。
 * 
 * @author yanbin
 * 
 */
public class BaseBeforeAdvice implements MethodBeforeAdvice {

    /**
     * method : 切入的方法 <br>
     * args :切入方法的引數 <br>
     * target :目標物件
     */
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("===========進入beforeAdvice()============ \n");

        System.out.print("準備在" + target + "物件上用");
        System.out.print(method + "方法進行對 '");
        System.out.print(args[0] + "'進行刪除!\n\n");

        System.out.println("要進入切入點方法了 \n");
    }

}

後置通知:

View Code
/**
 * 後置通知
 * 
 * @author yanbin
 * 
 */
public class BaseAfterReturnAdvice implements AfterReturningAdvice {

    /**
     * returnValue :切入點執行完方法的返回值,但不能修改 <br>
     * method :切入點方法 <br>
     * args :切入點方法的引數陣列 <br>
     * target :目標物件
     */
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("==========進入afterReturning()=========== \n");
        System.out.println("切入點方法執行完了 \n");

        System.out.print(args[0] + "在");
        System.out.print(target + "物件上被");
        System.out.print(method + "方法刪除了");
        System.out.print("只留下:" + returnValue + "\n\n");
    }

}

環繞通知:

View Code
/**
 * 環繞通知
 * 
 * @author yanbin
 * 
 */
public class BaseAroundAdvice implements MethodInterceptor {

    /**
     * invocation :連線點
     */
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("===========進入around環繞方法!=========== \n");

        // 呼叫目標方法之前執行的動作
        System.out.println("呼叫方法之前: 執行!\n");

        // 呼叫方法的引數
        Object[] args = invocation.getArguments();
        // 呼叫的方法
        Method method = invocation.getMethod();
        // 獲取目標物件
        Object target = invocation.getThis();
        // 執行完方法的返回值:呼叫proceed()方法,就會觸發切入點方法執行
        Object returnValue = invocation.proceed();

        System.out.println("===========結束進入around環繞方法!=========== \n");
        
        System.out.println("輸出:" + args[0] + ";" + method + ";" + target + ";" + returnValue + "\n");

        System.out.println("呼叫方法結束:之後執行!\n");

        return returnValue;
    }

}

異常通知:

View Code
/**
 * 異常通知,介面沒有包含任何方法。通知方法自定義
 * 
 * @author yanbin
 * 
 */
public class BaseAfterThrowsAdvice implements ThrowsAdvice {

    /**
     * 通知方法,需要按照這種格式書寫
     * 
     * @param method
     *            可選:切入的方法
     * @param args
     *            可選:切入的方法的引數
     * @param target
     *            可選:目標物件
     * @param throwable
     *            必填 : 異常子類,出現這個異常類的子類,則會進入這個通知。
     */
    public void afterThrowing(Method method, Object[] args, Object target, Throwable throwable) {
        System.out.println("刪除出錯啦");
    }

}

d、定義指定切點:

View Code
/**
 * 定義一個切點,指定對應方法匹配。來供切面來針對方法進行處理<br>
 * 
 * 繼承NameMatchMethodPointcut類,來用方法名匹配
 * 
 * @author yanbin
 * 
 */
public class Pointcut extends NameMatchMethodPointcut {

    private static final long serialVersionUID = 3990456017285944475L;

    @SuppressWarnings("rawtypes")
    @Override
    public boolean matches(Method method, Class targetClass) {
        // 設定單個方法匹配
        this.setMappedName("delete");
        // 設定多個方法匹配
        String[] methods = { "delete", "modify" };
        
        //也可以用“ * ” 來做匹配符號
        // this.setMappedName("get*");
        
        this.setMappedNames(methods);

        return super.matches(method, targetClass);
    }

}

e、配置: 

View Code
<?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:p="http://www.springframework.org/schema/p"
    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-3.0.xsd     
          http://www.springframework.org/schema/context     
          http://www.springframework.org/schema/context/spring-context-3.0.xsd 
          http://www.springframework.org/schema/aop     
          http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"
    default-autowire="byName">

    <!-- ==============================利用spring自己的aop配置================================ -->
    <!-- 宣告一個業務類 -->
    <bean id="baseBusiness" class="aop.base.BaseBusiness" />
    
    <!-- 宣告通知類 -->
    <bean id="baseBefore" class="aop.base.advice.BaseBeforeAdvice" />
    <bean id="baseAfterReturn" class="aop.base.advice.BaseAfterReturnAdvice" />
    <bean id="baseAfterThrows" class="aop.base.advice.BaseAfterThrowsAdvice" />
    <bean id="baseAround" class="aop.base.advice.BaseAroundAdvice" />

    <!-- 指定切點匹配類 -->
    <bean id="pointcut" class="aop.base.pointcut.Pointcut" />

    <!-- 包裝通知,指定切點 -->
    <bean id="matchBeforeAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="pointcut">
            <ref bean="pointcut" />
        </property>
        <property name="advice">
            <ref bean="baseBefore" />
        </property>
    </bean>

    <!-- 使用ProxyFactoryBean 產生代理物件 -->
    <bean id="businessProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <!-- 代理物件所實現的介面 ,如果有介面可以這樣設定 -->
        <property name="proxyInterfaces">
            <value>aop.base.IBaseBusiness</value>
        </property>

        <!-- 設定目標物件 -->
        <property name="target">
            <ref local="baseBusiness" />
        </property>
        <!-- 代理物件所使用的攔截器 -->
        <property name="interceptorNames">
            <list>
                <value>matchBeforeAdvisor</value>
                <value>baseAfterReturn</value>
                <value>baseAround</value>
            </list>
        </property>
    </bean>
</beans>

f、測試類:

View Code
public class Debug {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("aop/schema_aop.xml");
        IBaseBusiness business = (BaseBusiness ) context.getBean("businessProxy");
        business.delete("貓");
    }

}

g、測試結果:執行下測試類,清晰明瞭。由於結果呈現太長就不貼了。

  具體的程式碼實現可以從程式碼註釋中很容易理解 介面方式的實現。結果也可想而知,前置方法會在切入點方法之前執行,後置會在切入點方法執行之後執行,環繞則會在切入點方法執行前執行同事方法結束也會執行對應的部分。主要是呼叫proceed()方法來執行切入點方法。來作為環繞通知前後方法的分水嶺。然後在實現的過程中,有幾點卻是可以細揣摩一下的。

  可以看出在xml 配置 businessProxy這個bean的時候,ProxyFactoryBean類中指定了,proxyInterfaces引數。這裡我把他配置了IBaseBusiness介面。因為在專案開發過程中,往往業務類都會有對應的介面,以方便利用IOC解耦。但Spring AOP卻也能支援沒有介面的代理。這就是為什麼需要匯入cglib.jar的包了。看過spring的原始碼,知道在目標切入物件如果有實現介面,spring會預設走jdk動態代理來實現代理類。如果沒有介面,則會通過cglib來實現代理類。

  這個業務類現在有 前置通知,後置通知,環繞三個通知同時作用,可能以及更多的通知進行作用。那麼這些通知的執行順序是怎麼樣的?就這個例子而言,同時實現了三個通知。在例子xml中,則顯示執行before通知,然後執行around的前處理,執行切點方法,再執行return處理。最後執行around的後處理。經過測試,知道spring 處理順序是按照xml配置順序依次處理通知,以佇列的方式存放前通知,以壓棧的方式存放後通知。所以是前通知依次執行,後通知到切入點執行完之後,從棧裡在後進先出的形式把後通知執行。

  在實現過程中發現通知執行對應目標物件的整個類中的方法,如何精確到某個方法,則需要定義一個切點匹配的方式:spring提供了方法名匹配或正則方式來匹配。然後通過DefaultPointcutAdvisor來包裝通知,指定切點。

由於時間和篇幅原因:其他方式總結在另外一篇隨筆,http://www.cnblogs.com/yanbincn/archive/2012/08/13/2636961.html