Spring AOP

2020-02-10

img

AOP is an abbreviation of Aspect-Oriented Programming.

  • Cross-cutting concern

在DynamicProxyDemo專案的例子中,記錄的 動作原先被橫切(Cross-cutting)入至HelloSpeaker本身所負責的商務流程之中,另外類似於日誌這類的動作,如安全 (Security)檢查、交易(Transaction)等系統層面的服務(Service),在一些應用程式之中常被見到安插至各個物件的處理流程之 中,這些動作在AOP的術語中被稱之為Cross-cutting concerns。

以圖片說明可強調出Cross-cutting concerns的意涵,例如原來的商務流程是很單純的:

avatar

Cross-cutting concerns若直接撰寫在負責某商務的物件之流程中,會使得維護程式的成本增高,例如若您今天要將物件中的記錄功能修改或是移除該服務,則必須修改所 有撰寫曾記錄服務的程式碼,然後重新編譯,另一方面,Cross-cutting concerns混雜於商務邏輯之中,使得商務物件本身的邏輯或程式的撰寫更為複雜。

現在為了要加入日誌(Logging)與安全(Security)檢查等服務,物件的程式碼中若被硬生生的寫入相關的Logging、Security程式片段,則可使用以下圖解表示出Cross-cutting與Cross-cutting concerns的概念:

avatar

Cross-cutting concerns若直接撰寫在負責某商務的物件之流程中,會使得維護程式的成本增高,例如若您今天要將物件中的日誌功能修改或是移除該服務,則必須修改所 有撰寫曾日誌服務的程式碼,然後重新編譯,另一方面,Cross-cutting concerns混雜於商務邏輯之中,使得商務物件本身的邏輯或程式的撰寫更為複雜。

  • Aspect

    將散落於各個商務物件之中的Cross-cutting concerns收集起來,設計各個獨立可重用的物件,這些物件稱之為Aspect,例如在 動態代理 中 將日誌的動作設計為一個LogHandler類別,LogHandler類別在AOP的術語就是Aspect的一個具體實例,在AOP中著重於 Aspect的辨認,將之從商務流程中獨立出來,在需要該服務的時候,縫合(Weave)至應用程式之上,不需要服務的時候,也可以馬上從應用程式中脫 離,應用程式中的可重用元件不用作任何的修改,例如在 動態代理 中的HelloSpeaker所代表的角色就是應用程式中可重用的元件,在它需要日誌服務時並不用修改本身的程式碼。

    另一方面,對於應用程式中可重用的元件來說,以AOP的設 計方式,它不用知道處理提供服務的物件之存在,具體的說,與服務相關的API不會出現在可重用的應用程式元件之中,因而可提高這些元件的重用性,您可以將 這些元件應用至其它的應用程式之中,而不會因為目前加入了某些服務而與目前的應用程式框架發生耦合。

  • Advice

    Aspect的具體實作稱之為Advice,以日誌的動作而言,Advice中會包括真正的日誌程式碼是如何實作的,像是動態代理 中的LogHandler類別就是Advice的一個具體實例,Advice中包括了Cross-cutting concerns的行為或所要提供的服務。

  • Joinpoint

    Aspect在應用程式執行時加入商務流程的點或時機稱之為Joinpoint,具體來說,就是Advice在應用程式中被呼叫執行的時機,這個時機可能是某個方法被呼叫之前或之後(或兩者都有),或是某個例外發生的時候。

  • Pointcut

    Pointcut是一個定義,藉由這個定義您可以指定某個Aspect在哪些Joinpoint時被應用至應用程式之上。具體的說,您可以在某個定義檔中撰寫Pointcut,當中說明了哪些Aspect要應用至應用程式中的哪些Joinpoint。

  • Target

    一個Advice被應用的對象或目標物件,例如 動態代理 中的HelloSpeaker就是LogHandler這個Advice的Target。

  • Introduction

    對於一個現存的類別,Introduction可以為其增加行為,而不用修改該類別的程式,具體的說,您可以為某個已撰寫、編譯完成的類別,在執行時期動態加入一些方法或行為,而不用修改或新增任何一行程式碼。

  • Proxy

    在《Expert One-on-One J2EE Development WIthout EJB》一書中,Rod Johnson、Juergen Hoeller在第八章中有提到,AOP的實作有五個主要的策略: Dynamic Proxies、Dynamic Byte Code Generation、Java Code Generation、Use of a Custon Class Loader、Language Extensions。

    在之前 從代理機制初探 AOP 與 動態代理 中,已經使用實際的程式範例介紹過代理機制的實現,Spring的AOP主要是透過動態代理來完成。

  • Weave

    Advice被應用至物件之上的過程稱之為縫合(Weave),在AOP中縫合的方式有幾個時間點:編譯時期(Compile time)、類別載入時期(Classload time)、執行時期(Runtime)。

結合 動態代理 的實例,將以上介紹過的AOP相關名詞具體的使用圖片來加以表示,有助於您對每一個名詞的理解與認識:

avatar


從代理機制初探 AOP

暫且先將AOP這個英文縮寫名詞放到一邊,先從一個簡單常見的例子來看一個議題,這個例子當中含有日誌(Logging)動作,程式中很常需要為某些動作或事件作下記錄,以便在事後檢視程式運作過程或是作為除錯時的資訊。

來看一個最簡單的例子,當您需要在執行某些方法時留下日誌訊息,直覺的,您可能會如下撰寫:

package onlyfun.caterpillar;

import java.util.logging.*;

public class HelloSpeaker {
    private Logger logger =
            Logger.getLogger(this.getClass().getName());

    public void hello(String name) {
        // 方法執行開始時留下日誌
        logger.log(Level.INFO, "hello method starts....");
        // 程式主要功能
        System.out.println("Hello, " + name);
        // 方法執行完畢前留下日誌
        logger.log(Level.INFO, "hello method ends....");
    }
}

在HelloSpeaker類別中,當執行hello()方法時,您希望該方法執行開始與執行完畢時都能留下日誌,最簡單的作法就是如以上的程式設計,在 方法執行的前後加上日誌動作,然而記錄的這幾行程式碼橫切入(Cross-cutting)HelloSpeaker類別中,對於 HelloSpeaker來說,日誌的這幾個動作並不屬於HelloSpeaker商務邏輯(顯示”Hello”等文字),這使得 HelloSpeaker增加了額外的職責。

想想如果程式中這種日誌的動作到處都有需求,以上的寫法勢必造成您必須到處撰寫這些日誌動作的程式碼,這將使得維護日誌程式碼的困難度加大。如果需要的服 務(Service)不只有日誌動作,有一些非物件本身職責的相關動作也混入了物件之中(例如權限檢查、交易管理等等),會使得物件的負擔更形加重,甚至 混淆了物件本身該負有的職責,物件本身的職責所佔的程式碼,或許還小於這些與物件職責不相關的動作或服務的程式碼。

另一方面,使用以上的寫法,若您有一日不再需要日誌(或權限檢查、交易管理等)的服務,那麼您將需要修改所有留下日誌動作的程式碼,您無法簡單的就將這些相關服務從即有的程式中移去。

可以使用代理(Proxy)機制來解決這個問題,在這邊討論兩種代理方式:靜態代理(Static proxy)與動態代理(Dynamic proxy)。

在靜態代理的實現中,代理物件與被代理的物件都必須實現同一個介面,在代理物件中可以實現記錄等相關服務,並在需要的時候再呼叫被代理的物件,如此被代理物件當中就可以僅保留業務相關職責。

舉個實際的例子來說,首先定義一個IHello介面:

  • IHello.java
package onlyfun.caterpillar;

public interface IHello {
    public void hello(String name);
}

然後讓實現商務邏輯的HelloSpeaker類別要實現IHello介面,例如:

  • HelloSpeaker.java
package onlyfun.caterpillar;

public class HelloSpeaker implements IHello {
    public void hello(String name) {
        System.out.println("Hello, " + name); 
    }
}

可以看到,在HelloSpeaker類別中現在沒有任何日誌的程式碼插入其中,日誌服務的實現將被放至代理物件之中,代理物件同樣也要實現IHello介面,例如:

  • HelloProxy.java
package onlyfun.caterpillar;

import java.util.logging.*; 

public class HelloProxy implements IHello { 
    private Logger logger = 
            Logger.getLogger(this.getClass().getName());
    
    private IHello helloObject; 

    public HelloProxy(IHello helloObject) { 
        this.helloObject = helloObject; 
    } 

    public void hello(String name) { 
        // 日誌服務
        log("hello method starts....");      

        // 執行商務邏輯
        helloObject.hello(name);
        
        // 日誌服務
        log("hello method ends...."); 
    } 
    
    private void log(String msg) {
        logger.log(Level.INFO, msg);
    }
}

在HelloProxy類別的hello()方法中,真正實現商務邏輯前後可以安排記錄服務,可以實際撰寫一個測試程式來看看如何使用代理物件。

  • ProxyDemo.java
package onlyfun.caterpillar;

public class ProxyDemo {
    public static void main(String[] args) {
        IHello proxy = 
            new HelloProxy(new HelloSpeaker());
        proxy.hello("Justin");
    }
} 

程式中呼叫執行的是代理物件,建構代理物件時必須給它一個被代理物件,記得在操作取回的代理物件時,必須轉換操作介面為IHello介面。

代理物件HelloProxy將代理真正的HelloSpeaker來執行hello(),並在其前後加上日誌的動作,這使得我們的 HelloSpeaker在撰寫時不必介入日誌動作,HelloSpeaker可以專心於它的職責,可以從圖解的方式來更進一步看出代理機制的運作流程。

avatar

這是靜態代理的基本範例,然而如您所看到的,代理物件的一個介面只服務於一種類型的物件,而且如果要代理的方法很多,您勢必要為每個方法進行代理,靜態代理在程式規模稍大時就必定無法勝任,在這邊介紹靜態代理的目的,是在讓您了解代理的基本原理。


動態代理

在JDK 1.3之後加入了可協助開發動態代理功能的API等相關類別,您不必為特定物件與方法撰寫特定的代理物件,使用動態代理,可以使得一個處理者 (Handler)服務於各個物件,首先,一個處理者的類別設計必須實作java.lang.reflect.InvocationHandler介面, 以實例來進行說明,例如設計一個LogHandler類別:

  • LogHandler.java
package onlyfun.caterpillar;

import java.util.logging.*; 
import java.lang.reflect.*; 

public class LogHandler implements InvocationHandler { 
    private Logger logger = 
            Logger.getLogger(this.getClass().getName()); 
    
    private Object delegate;

    public Object bind(Object delegate) { 
        this.delegate = delegate; 
        return Proxy.newProxyInstance( 
                           delegate.getClass().getClassLoader(), 
                           delegate.getClass().getInterfaces(), 
                           this); 
    } 

    public Object invoke(Object proxy, Method method, 
                         Object[] args) throws Throwable { 
        Object result = null; 
        
        try { 
            log("method starts..." + method);
            
            result = method.invoke(delegate, args);
            
            logger.log(Level.INFO, "method ends..." + method); 
        } catch (Exception e){ 
            log(e.toString()); 
        }
        
        return result; 
    } 
    
    private void log(String message) {
        logger.log(Level.INFO, message);
    }
}

主要的概念是使用Proxy.newProxyInstance()靜態方法建立一個代理物件,建立代理物件時必須告知所要代理的介面,之後您可以操作所 建立的代理物件,在每次操作時會呼叫InvocationHandler的invoke()方法,invoke()方法會傳入被代理物件的方法名稱與執行 參數,實際上要執行的方法交由method.invoke(),您在method.invoke()前後加上記錄動作,method.invoke()傳 回的物件是實際方法執行過後的回傳結果。

要實現動態代理,同樣必須定義所要代理的介面,例如:

  • IHello.java
package onlyfun.caterpillar;

public interface IHello {
    public void hello(String name);
}

然後讓實現商務邏輯的HelloSpeaker類別要實現IHello介面,例如:

  • HelloSpeaker.java
package onlyfun.caterpillar;

public class HelloSpeaker implements IHello {
    public void hello(String name) {
        System.out.println("Hello, " + name); 
    }
}

眼尖的您或許發現到了,這跟之前 從代理機制初探 AOP 中的IHello介面、HelloSpeaker是相同的內容,在這邊撰寫出來是為了範例的完整呈現。接下來撰寫一個測試的程式,您要使用LogHandler的bind()方法來綁定被代理物件,如下所示:

  • ProxyDemo.java
package onlyfun.caterpillar;

public class ProxyDemo {
    public static void main(String[] args) {
        LogHandler logHandler  = new LogHandler(); 
        
        IHello helloProxy = 
                (IHello) logHandler.bind(new HelloSpeaker()); 
        helloProxy.hello("Justin");
    }
}    

回到AOP的議題上,這個例子與AOP有何關係?

如以上的例子中示範的,HelloSpeaker本身的職責是顯示招呼文字,卻必須插入日誌(Log)動作,這使得HelloSpeaker的職責加重,在AOP的術語來說,日誌的程式碼橫切(Cross-cutting)入HelloSpeaker的程式執行流程中,日誌這樣的動作在AOP中稱之為橫切關切點(Cross-cutting concern)。

使用代理物件將記錄等與商務邏輯無關的動作或務提取出來,設計為為一個服務物件,像是之前範例中示範的HelloProxy或是LogHandler,這樣的物件稱之為切面(Aspect)。

AOP中的Aspect所指的可以是像日誌等這類的動作或服務,您將這些動作(Cross-cutting concerns)設計為通用、不介入特定業務物件的一個職責清楚的Aspect物件,這就是所謂的Aspect-oriented programming,縮寫名詞即為AOP。

在好的設計之下,Aspect可以獨立於應用程式之外,在必要的時候,可以介入應用程式之中提供服務,而不需要相關服務的時候,又可以將這些Aspect直接從應用程式中脫離,而您的應用程式本身不需修改任何一行程式碼。


AOP(Aspect Oriented Programming) 面向切面编程,是目前软件开发中的一个热点,是Spring框架内容,利用AOP可以对业务逻辑的各个部分隔离,从而使的业务逻辑各部分的耦合性降低,提高程序的可重用性,踢开开发效率,主要功能:日志记录,性能统计,安全控制,事务处理,异常处理等。

AOP实现原理是java动态代理,但是jdk的动态代理必须实现接口,所以spring的aop是用cglib这个库实现的,cglis使用里asm这个直接操纵字节码的框架,所以可以做到不使用接口的情况下实现动态代理。

AOP与OOP的却别:

OOP面向对象编程,针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。这两种设计思想在目标上有着本质的差异。

举例:

对于“雇员”这样一个业务实体进行封装,自然是OOP的任务,我们可以建立一个“Employee”类,并将“雇员”相关的属性和行为封装其中。而用AOP 设计思想对“雇员”进行封装则无从谈起。

同样,对于“权限检查”这一动作片段进行划分,则是AOP的目标领域。

OOP面向名次领域,AOP面向动词领域。


  1. 理解

    面向切面编程的基本思想是在极少影响原程序的代码的前提下,在程序中的某些地方,使用某些方式,不可见的(即不在原程序中添加其他代码)为原程序切入一些额外的功能。 优点 减少代码间的耦合性,使功能具有拔插性,保证自己代码的清洁型。 能够让你只关注自己的代码,不需要关注切面是如何实现的。

  2. 名词解释

    这里使用电力公司下的去各家各户查电表的工人为例。

    通知(advice):

     查电表的工人,在工作前心里明白,或者说必须接到公司老板下达的任务,即包括什么时候去查电表,以及去住户人家怎样查电表。和此相似,通知定义了切面做什么和什么时候去做(确切的说是程序员在通知中定义的)。除了定义切面要完成的工作(what job),还会定位什么时候(when)去履行这项工作。是在方法调用前,还是调用之后,还是前后都是,还是抛出异常时。
    

    切面一共有五种通知

    • Before 某方法调用之前发出通知。
    • After 某方法完成之后发出通知,不考虑方法运行的结果。
    • After-returning 将通知放置在被通知的方法成功执行之后。
    • After-throwing 将通知放置在被通知的方法抛出异常之后。
    • Around 通知包裹在被通知的方法的周围,在方法调用之前和之后发出通知。

    连接/参加点(Join Point):

    就像每一个电力公司下有很多使用他提供的电的房子,每一个房子都可以,都有机会被查电表。同样,在你的应用程序中会有好多的地方可以,或者有机会被通知申请完成通知自己的工作。这些机会(某个点)就被称为连接点。连接点是切面在应用程序执行过程中插入的地方,可能是方法调用的时候,异常抛出的时候。

    切点(pointcuts):

    很明显一位查电表的人不能把所有的用电客户都拜访个遍,而是每一个人都会有自己管辖的客户。同样,一个切面没必要通知(advise)应用程序中所有的连接点(join point)。切点(pointcuts)能够精确定位被切面通知的连接点。

    如果通知(advice)定义了切面what和when,那切点就定义的切面的 where。切点决定去匹配通知们应该去编织的一个或者多个连接点。通常需要使用类或者方法的名字来指定切点,或者通过正则表达式去匹配类和方法的名字

    切面(aspects):

    当查电表的开始了他的一天(when),他知道他需要去做些什么(what),(通知),以及那些房子需要他访问(where)(切点)。事实上他知道他工作上要做的每一件事。 很明显,切面即是通知和切点的结合体。放在一块,通知和切点定义了切面需要知道的每一件事。要做什么,以及在哪里什么时候去做。

    引入(introductions):

    引入允许你添加一个新的方法给已经存在的类。

  3. Spring AOP的支持

    Spring建议在Java中书写AOP Spring是在运行阶段才将切面编织进bean中,是使用代理类。 Spring只支持方法级别的连接点。

  4. Spring使用AspectJ的切点表达式去定义Spring切面

    AspectJ标志符 解释
    args() 定制join-point去匹配那些参数为指定类型的方法的执行动作。
    @args() 定制join-point去匹配那些参数被指定类型注解的方法的执行动作。
    execution() 开始匹配在其内部编写的定制。
    this() 定制join-pont去匹配由AOP代理的Bean引用的指定类型的类。
    target() 定制join-point去匹配特定的对象,这些对象一定是指定类型的类。
    @target() 定制join-point去匹配特定的对象,这些对象要具有的指定类型的注解。
    within() 定制join-point在必须哪一个包中。
    @within() 定制join-point在必须由指定注解标注的类中。
    @annotation 定制连接点具有指定的注解。
    • 只有execution用来执行匹配,其他标志符都只是为了限制/定制他们所要匹配的连接点的位置。

    4.1 编写切点,一个简答的示例

    开始写代码之前需要做一些准备。 如果你使用的是Maven工具构建的项目,除了基本的Spring依赖,还需要导入下面两个支持Spring切面的依赖。

     <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
     <dependency>    
         <groupId>org.aspectj</groupId>            
         <artifactId>aspectjweaver</artifactId>    
         <version>1.8.9</version>
     </dependency>
     <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjrt -->
     <dependency>   
         <groupId>org.aspectj</groupId> 
         <artifactId>aspectjrt</artifactId>    
         <version>1.8.9</version>
     </dependency>
    

    Gradle 相似。注意这里的groupId要为org.aspectj,而不是aspectj,否则运行时会抛出异常。

    首先定义一个接口

     public interface Performance {
         public void perform();
     }
    

    Performance代表一个表演者,比如舞台上的表演者,电影里的演员等等。 现在你想写一个当Performance的perform()方法执行时触发的切面。下面这个表达式就展示了一个切点表达式,只要perform()方法执行了,他就能得到切面的通知。

     execution(* concert.Performance.perform(..))
    

    这里解释一下:

    execution意思是当某方法执行时触发。

    第一个 * 意思配备任何一个perform方法,是不在乎方法返回类型是什么。

    concert.Performance是为了定位perform()方法再哪个类中。

    .. 代表配备任何一个perform方法,不在意他的参数列表是啥。

    如果你想限定切点的范围,只匹配在concert包被调用的perform()。你可以使用within()标识符:

     execution(* concert.Performance.perform(..))
         && within(concert.*))
    
    

    注意这里的 && 就是与的意思,当然还有 或 || ,非 ! ,如果你是在XML配置文件中编写切面,那就建议使用 and or not 来替代他们。

    4.2 在切点中选取bean

    在表达式中,可以使用bean()标识符,通过bean的ID来识别一个bean。例如:

     execution(* concert.Performance.perform())
     and bean('woodstock')
    

    4.3 创建注解切面

    AspectJ 5 之后你就能使用注解的方式来创建切面,他能够让你使用很少的注解就能实现切面。 现在你已经定义了一个Performance作为你切面要切入的主体。现在我们就使用AspectJ 注解来创建切面。

    定义一个切面。

    没有观众的表演家就不能叫表演家,观众对于表演家非常重要,但又不属于表演家,他是一个分离出来的点,因此,我们就能将观众定义成一个切面,好让他能够轻松的观看一个表演家的表演。

     package com.mengxiang.concert;
    
     import org.aspectj.lang.annotation.*;
    
     /**
     * 观众
     * Created by Henvealf on 2016/8/26.
     */
     @Aspect
     public class Audience {
         @Before("execution(* com.mengxiang.concert.Performance.perform(..))")
         public void slienceCellPhones(){
             System.out.println("关掉手机");
         }
    
         @Before("execution(* com.mengxiang.concert.Performance.perform(..))")
         public void takeSeats() {
             System.out.println("找到座位");
         }
    
         @AfterReturning("execution(* com.mengxiang.concert.Performance.perform(..))")
         public void applause() {
             System.out.println("好好好!!鼓掌");
         }
    
         @AfterThrowing("execution(* com.mengxiang.concert.Performance.perform())")
         public  void demandRefund() {
             System.out.println("退票! 退票! ");
         }
     }
    

    然后编写一个配置文件,注入观众Bean和演员Bean。

     @Configuration
     @EnableAspectJAutoProxy
     @ComponentScan
     public class ConcertConfig {
         @Bean
         public Audience audience() {
             return new Audience();
         }
         @Bean(name = "dancer")
         public Performance dancer(){
             return new Dancer();
         }
     }
    

    最后测试

     import org.springframework.context.ApplicationContext;
     import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    
     /**
     * Created by Henvealf on 2016/8/27.
     */
     public class Main {
         public static void main(String[] args) {
             ApplicationContext context = new AnnotationConfigApplicationContext("com.mengxiang.concert");
             Performance per = (Performance) context.getBean("dancer");
             per.perform();
    
         }
     }
    

    测试结果完全可以预料到:

    关掉手机
    找到座位
    表演开始,跳舞,跳舞。
    好好好!!鼓掌

参考链接 参考链接


注解方式

  1. 定义目标对象

     package com.bytebeats.spring4.aop.annotation.service;
    
     /**
     * ${DESCRIPTION}
     *
     * @author Ricky Fung
     * @create 2017-04-03 11:11
     */
     public interface BankService {
    
         boolean transfer(String from, String to, int amount);
     }
    

    BankServiceImpl

     package com.bytebeats.spring4.aop.annotation.service;
    
     import org.springframework.stereotype.Service;
    
     /**
     * ${DESCRIPTION}
     *
     * @author Ricky Fung
     * @create 2017-04-03 11:12
     */
     @Service
     public class BankServiceImpl implements BankService {
    
         @Override
         public boolean transfer(String from, String to, int amount) {
             if(amount<1){
                 throw new IllegalArgumentException("transfer amount must be a positive number");
             }
             System.out.println("["+from+"]向["+to+ "]转账金额"+amount);
             return false;
         }
     }
    
  2. 定义切面

     package com.bytebeats.spring4.aop.annotation;
    
     import org.aspectj.lang.JoinPoint;
     import org.aspectj.lang.ProceedingJoinPoint;
     import org.aspectj.lang.annotation.*;
     import org.springframework.core.annotation.Order;
     import org.springframework.stereotype.Component;
    
     import java.util.Arrays;
     import java.util.List;
    
     @Order(1)
     @Aspect
     @Component
     public class TransferLogAdvice {
    
         @Pointcut("execution(* com.bytebeats.spring4.aop.annotation.service.BankServiceImpl.*(..))")
         public void pointcut1() {
         }
    
         @Pointcut("execution(* com.bytebeats.spring4.aop.annotation.service.*ServiceImpl.*(..))")
         public void myPointcut() {
         }
    
         /**
         * 前置通知:在方法执行前执行的代码
         * @param joinPoint
         */
         @Before(value = "pointcut1() || myPointcut()")
         //@Before("execution(* com.bytebeats.spring4.aop.annotation.service.BankServiceImpl.*(..))")
         public void beforeExecute(JoinPoint joinPoint){
                
             String methodName = joinPoint.getSignature().getName();
             List<Object> args = Arrays.asList(joinPoint.getArgs());
                
             System.out.println(this.getClass().getSimpleName()+ " before execute:"+methodName+ " begin with "+args);
         }
            
         /**
         * 后置通知:在方法执行后执行的代码(无论该方法是否发生异常),注意后置通知拿不到执行的结果
         * @param joinPoint
         */
         @After(value = "pointcut1()")
         public void afterExecute(JoinPoint joinPoint){
                
             String methodName = joinPoint.getSignature().getName();
             System.out.println(this.getClass().getSimpleName()+ " after execute:"+methodName+" end!");
         }
            
         /**
         * 后置返回通知:在方法正常执行后执行的代码,可以获取到方法的返回值
         * @param joinPoint
         */
         @AfterReturning(value = "pointcut1()",
                 returning="result")
         public void afterReturning(JoinPoint joinPoint, Object result){
                
             String methodName = joinPoint.getSignature().getName();
             System.out.println(this.getClass().getSimpleName()+ " afterReturning execute:"+methodName+" end with result:"+result);
         }
            
         /**
         * 后置异常通知:在方法抛出异常之后执行,可以访问到异常信息,且可以指定出现特定异常信息时执行代码
         * @param joinPoint
         */
         @AfterThrowing(value = "pointcut1()",
                 throwing="exception")
         public void afterThrowing(JoinPoint joinPoint, Exception /**NullPointerException*/ exception){
                
             String methodName = joinPoint.getSignature().getName();
             System.out.println(this.getClass().getSimpleName()+ " afterThrowing execute:"+methodName+" occurs exception:"+exception);
         }
            
         /**
         * 环绕通知, 围绕着方法执行
         */
         @Around(value = "pointcut1()")
         public Object around(ProceedingJoinPoint joinPoint){
                
             String methodName = joinPoint.getSignature().getName();
             System.out.println(this.getClass().getSimpleName()+ " around:"+methodName+" execute start");
                
             Object result = null;
             try {
                 result = joinPoint.proceed();
             } catch (Throwable e) {
                 e.printStackTrace();
             }
             System.out.println(this.getClass().getSimpleName()+ " around:"+methodName+" execute end");
             return result;
         }
     }
    

    Spring AOP提供了 @Before,@After、@AfterReturning、@AfterThrowing、@Around注解来指定 通知类型。

     /**
         * 前置通知:在方法执行前执行的代码
         * @param joinPoint
         */
         @Before("execution(* com.bytebeats.spring4.aop.annotation.service.BankServiceImpl.*(..))")
         public void beforeExecute(JoinPoint joinPoint){
                
             String methodName = joinPoint.getSignature().getName();
             List<Object> args = Arrays.asList(joinPoint.getArgs());
                
             System.out.println(this.getClass().getSimpleName()+ " before execute:"+methodName+ " begin with "+args);
         }
    

    但是,为了复用切点,推荐使用@Pointcut来定义切点,然后在 @Before、@After等注解中引用定义好的切点,代码如下:

     @Pointcut("execution(* com.bytebeats.spring4.aop.annotation.service.BankServiceImpl.*(..))")
         public void pointcut1() {
         }
    
         /**
         * 前置通知:在方法执行前执行的代码
         * @param joinPoint
         */
         @Before(value = "pointcut1()")
         public void beforeExecute(JoinPoint joinPoint){
                
             String methodName = joinPoint.getSignature().getName();
             List<Object> args = Arrays.asList(joinPoint.getArgs());
                
             System.out.println(this.getClass().getSimpleName()+ " before execute:"+methodName+ " begin with "+args);
         }
    
         /**
         * 后置通知:在方法执行后执行的代码(无论该方法是否发生异常),注意后置通知拿不到执行的结果
         * @param joinPoint
         */
         @After(value = "pointcut1()")
         public void afterExecute(JoinPoint joinPoint){
                
             String methodName = joinPoint.getSignature().getName();
             System.out.println(this.getClass().getSimpleName()+ " after execute:"+methodName+" end!");
         }
    
  3. 启用注解扫描

    spring-aop-annotation.xml

     <?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/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
             http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    
         <!-- 自动扫描的包 -->
         <context:component-scan base-package="com.bytebeats.spring4.aop.annotation">
         </context:component-scan>
    
         <!-- 启用AspectJ自动代理 -->
         <aop:aspectj-autoproxy />
    
     </beans>
    

    到此,基于注解方式配置AOP已经结束了,接下来可以验证一下。

     package com.bytebeats.spring4.aop.annotation;
    
     import com.bytebeats.spring4.aop.annotation.service.BankService;
     import com.bytebeats.spring4.aop.xml.service.UserService;
     import org.springframework.context.support.ClassPathXmlApplicationContext;
    
     public class SpringAopAnnotationApp {
    
         public static void main(String[] args) {
                
             ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring-aop-annotation.xml");
    
             BankService bankService = ctx.getBean(BankService.class);
             bankService.transfer("jordan", "kobe", 2000);
    
             System.out.println("*********************");
             bankService.transfer("jordan", "kobe", 0);
    
             ctx.close();
         }
     }
    

    Spring AOP切入点表达式 切入点指示符用来指示切入点表达式目的,在Spring AOP中目前只有执行方法这一个连接点,表达式如下:

    execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
    括号中各个pattern分别表示 修饰符匹配(modifier-pattern?)、返回值匹配(ret-type-pattern)、类路径匹配(declaring-type-pattern?)、方法名匹配(name-pattern)、参数匹配((param-pattern))、异常类型匹配(throws-pattern?),其中后面跟着“?”的是可选项。

    我们来看几个例子:

    1. 匹配所有方法
     execution(* *(..))  
    

    2、匹配 com.bytebeats.spring4.aop.xml.service.UserService中所有的公有方法

     execution(public * com.bytebeats.spring4.aop.xml.service.UserService.*(..))
    

    3、匹配com.bytebeats.spring4.aop.xml.service 包及其子包下的所有方法

     execution(* com.bytebeats.spring4.aop.xml.service..*.*(..))  
    
    另外,@Pointcut 定义切点时,还可以使用 &&、   和 !,如下:
     @Pointcut("execution(* com.bytebeats.mq.MqProducer.*(..))")  
     private void mqSender(){
     }  
     @Pointcut("execution(* com.bytebeats.mq.MqConsumer.*(..))")  
     private void mqReceiver(){
     }
    
     @Pointcut("mqSender() || mqReceiver()")  
     private void mqTrace(){
     }
    

    源码下载 https://github.com/TiFG/spring4-in-action/tree/master/spring-ch2-aop