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的意涵,例如原來的商務流程是很單純的:
Cross-cutting concerns若直接撰寫在負責某商務的物件之流程中,會使得維護程式的成本增高,例如若您今天要將物件中的記錄功能修改或是移除該服務,則必須修改所 有撰寫曾記錄服務的程式碼,然後重新編譯,另一方面,Cross-cutting concerns混雜於商務邏輯之中,使得商務物件本身的邏輯或程式的撰寫更為複雜。
現在為了要加入日誌(Logging)與安全(Security)檢查等服務,物件的程式碼中若被硬生生的寫入相關的Logging、Security程式片段,則可使用以下圖解表示出Cross-cutting與Cross-cutting concerns的概念:
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相關名詞具體的使用圖片來加以表示,有助於您對每一個名詞的理解與認識:
從代理機制初探 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可以專心於它的職責,可以從圖解的方式來更進一步看出代理機制的運作流程。
這是靜態代理的基本範例,然而如您所看到的,代理物件的一個介面只服務於一種類型的物件,而且如果要代理的方法很多,您勢必要為每個方法進行代理,靜態代理在程式規模稍大時就必定無法勝任,在這邊介紹靜態代理的目的,是在讓您了解代理的基本原理。
動態代理
在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面向动词领域。
-
理解
面向切面编程的基本思想是在极少影响原程序的代码的前提下,在程序中的某些地方,使用某些方式,不可见的(即不在原程序中添加其他代码)为原程序切入一些额外的功能。 优点 减少代码间的耦合性,使功能具有拔插性,保证自己代码的清洁型。 能够让你只关注自己的代码,不需要关注切面是如何实现的。
-
名词解释
这里使用电力公司下的去各家各户查电表的工人为例。
通知(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):
引入允许你添加一个新的方法给已经存在的类。
-
Spring AOP的支持
Spring建议在Java中书写AOP Spring是在运行阶段才将切面编织进bean中,是使用代理类。 Spring只支持方法级别的连接点。
-
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(); } }
测试结果完全可以预料到:
关掉手机
找到座位
表演开始,跳舞,跳舞。
好好好!!鼓掌
注解方式
-
定义目标对象
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; } }
-
定义切面
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!"); }
-
启用注解扫描
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?),其中后面跟着“?”的是可选项。我们来看几个例子:
- 匹配所有方法
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