天天看點

Spring AOP詳解

程式員還是需要把基礎打紮實,修煉自己的内功。” 是以趕緊把學習的東西總結一下,加深印象。 。基于代理模式,了解了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);

     * 這方法不被切面切

    public String add(String obj);

     * 這方法切不切呢?可以設定

    public String modify(String obj);

}

b、業務類:

 * 業務類,也是目标對象

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、通知類:

前置通知:

 * 前置通知。

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");

後置通知:

 * 後置通知

public class BaseAfterReturnAdvice implements AfterReturningAdvice {

     * returnValue :切入點執行完方法的傳回值,但不能修改 <br>

     * method :切入點方法 <br>

     * args :切入點方法的參數數組 <br>

    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");

環繞通知:

 * 環繞通知

public class BaseAroundAdvice implements MethodInterceptor {

     * invocation :連接配接點

    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;

異常通知:

 * 異常通知,接口沒有包含任何方法。通知方法自定義

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、定義指定切點:

 * 定義一個切點,指定對應方法比對。來供切面來針對方法進行處理<br>

 * 繼承NameMatchMethodPointcut類,來用方法名比對

public class Pointcut extends NameMatchMethodPointcut {

    private static final long serialVersionUID = 3990456017285944475L;

    @SuppressWarnings("rawtypes")

    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、配置:

<?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" />

    </bean>

    <!-- 使用ProxyFactoryBean 産生代理對象 -->

    <bean id="businessProxy" class="org.springframework.aop.framework.ProxyFactoryBean">

        <!-- 代理對象所實作的接口 ,如果有接口可以這樣設定 -->

        <property name="proxyInterfaces">

            <value>aop.base.IBaseBusiness</value>

        <!-- 設定目标對象 -->

        <property name="target">

            <ref local="baseBusiness" />

        <!-- 代理對象所使用的攔截器 -->

        <property name="interceptorNames">

            <list>

                <value>matchBeforeAdvisor</value>

                <value>baseAfterReturn</value>

                <value>baseAround</value>

            </list>

</beans>

f、測試類:

public class Debug {

    public static void main(String[] args) {

        ApplicationContext context = new ClassPathXmlApplicationContext("aop/schema_aop.xml");

        AspectBusiness business = (AspectBusiness) context.getBean("aspectBusiness");

        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來包裝通知,指定切點。

本來是想一口氣梳理完的。但是大晚上時間不夠(無奈一場奧運籃球總決賽耗費掉了2小時,不過的确相當精彩),又考慮到篇幅太長,閱讀性比較差,是以将後半部分更偏于應用的重起一篇随筆。

  利用方式一的配置起來,可見代碼還是非常的厚重的,定義一個切面就要定義一個切面類,然而切面類中,就一個通知方法,着實沒有必要。是以Spring提供了,依賴aspectj的schema配置和基于aspectj 注解方式。這兩種方式非常簡介友善使用,也是項目中普遍的使用方式。梳理之:

4、方式二:schema配置

a、業務類:

 * 業務類

public class AspectBusiness {

b、切面類:切面類中,包含了所有的通知

 * 定義一個切面

public class AspectAdvice {

     * 前置通知

     * @param jp

    public void doBefore(JoinPoint jp) {

        System.out.println("===========進入before advice============ \n");

        System.out.print("準備在" + jp.getTarget().getClass() + "對象上用");

        System.out.print(jp.getSignature().getName() + "方法進行對 '");

        System.out.print(jp.getArgs()[0] + "'進行删除!\n\n");

     * 後置通知

     *            連接配接點

     * @param result

     *            傳回值

    public void doAfter(JoinPoint jp, String result) {

        System.out.println("==========進入after advice=========== \n");

        System.out.print(jp.getArgs()[0] + "在");

        System.out.print(jp.getTarget().getClass() + "對象上被");

        System.out.print(jp.getSignature().getName() + "方法删除了");

        System.out.print("隻留下:" + result + "\n\n");

     * 環繞通知

     * @param pjp

    public void doAround(ProceedingJoinPoint pjp) throws Throwable {

        Object[] args = pjp.getArgs();

        // 調用的方法名

        String method = pjp.getSignature().getName();

        Object target = pjp.getTarget();

        Object result = pjp.proceed();

        System.out.println("輸出:" + args[0] + ";" + method + ";" + target + ";" + result + "\n");

     * 異常通知

     * @param e

    public void doThrow(JoinPoint jp, Throwable e) {

c、配置檔案:

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

    <!-- ==============================利用spring 利用

asp

ectj來配置AOP================================ -->

    <bean id="aspectBusiness" class="aop.schema.AspectBusiness" />

    <bean id="aspectAdvice" class="aop.schema.advice.AspectAdvice" />

    <aop:config>

        <aop:aspect id="businessAspect" ref="aspectAdvice">

            <!-- 配置指定切入的對象 -->

            <aop:pointcut id="point_cut" expression="execution(* aop.schema.*.*(..))" />

            <!-- 隻比對add方法作為切入點

            <aop:pointcut id="except_add" expression="execution(* aop.schema.*.add(..))" />

             -->

            <!-- 前置通知 -->

            <aop:before method="doBefore" pointcut-ref="point_cut" />

            <!-- 後置通知 returning指定傳回參數 -->

            <aop:after-returning method="doAfter"

                pointcut-ref="point_cut" returning="result" />

            <aop:around method="doAround" pointcut-ref="point_cut"/>

            <aop:after-throwing method="doThrow" pointcut-ref="point_cut" throwing="e"/>

        </aop:aspect>

    </aop:config>

d、測試類:

5、方式三:aspectj注解

注解在項目中已經到處都是了,撇開一些優劣不提,開發的便利性和可讀性是非常的友善的。用來配置Spring AOP也非常簡單便利

@Component

public class Business {

b、切面類:

 * 定義切面

 * @Aspect : 标記為切面類

 * @Pointcut : 指定比對切點

 * @Before : 指定前置通知,value中指定切入點比對

 * @AfterReturning :後置通知,具有可以指定傳回值

 * @AfterThrowing :異常通知

@Aspect

     * 指定切入點比對表達式,注意它是以方法的形式進行聲明的。

    @Pointcut("execution(* aop.annotation.*.*(..))")

    public void anyMethod() {

    @Before(value = "execution(* aop.annotation.*.*(..))")

    @AfterReturning(value = "anyMethod()", returning = "result")

    @Around(value = "execution(* aop.annotation.*.*(..))")

    @AfterThrowing(value = "execution(* aop.annotation.*.*(..))", throwing = "e")

c、配置:

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

    <!-- 打開aop 注解 -->

    <aop:aspectj-autoproxy />

 * 測試類

        ApplicationContext context = new ClassPathXmlApplicationContext("aop/annotation_aop.xml");

        Business business = (Business) context.getBean("business");