AOP(Aspect Oriented Programming),即面向切面編程,官方的解釋是:面向切面編程,通過預(yù)編譯方式和運(yùn)行期間動(dòng)態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。換一個(gè)相對(duì)好理解的說(shuō)法,就是可以把程序中重復(fù)的代碼(日志記錄、事務(wù)管理等)抽取出來(lái),在需要執(zhí)行的時(shí)候,使用動(dòng)態(tài)代理技術(shù),在不修改源碼的基礎(chǔ)上去執(zhí)行,實(shí)現(xiàn)對(duì)目標(biāo)對(duì)象方法、參數(shù)的攔截,可在目標(biāo)對(duì)象前后等位置添加上抽取出來(lái)的功能,實(shí)現(xiàn)其功能上的增強(qiáng)。以減少重復(fù)代碼,提高開發(fā)效率,方便維護(hù)。
AOP 也是 Spring 框架中的主要內(nèi)容,但 Spring AOP 只支持目標(biāo)對(duì)象方法的攔截、增強(qiáng),Spring AOP 常用的實(shí)現(xiàn)方式有基于XML和基于注解兩種。在學(xué)習(xí)這兩種方式前,我們有必要先了解 AOP 中的一些概念。
一、AOP 的相關(guān)概念
- 連接點(diǎn)(JoinPoint):在Spring中代表類中的定義的方法
- 切入點(diǎn)(Pointcut):指定要對(duì)哪些連接點(diǎn)(方法)進(jìn)行攔截、增強(qiáng),需要通過切入點(diǎn)表達(dá)式來(lái)指定哪些方法可以作為切入點(diǎn)。
- 通知(Advice):攔截到切入點(diǎn)后要做的事情就是通知,即要增強(qiáng)什么功能
- 目標(biāo)對(duì)象(Target):要被代理的目標(biāo)對(duì)象
- 織入(Weaving):把通知應(yīng)用到目標(biāo)對(duì)象,來(lái)創(chuàng)建增強(qiáng)的代理對(duì)象的過程,若目標(biāo)對(duì)象的類實(shí)現(xiàn)了接口,Spring 默認(rèn)采用JDK動(dòng)態(tài)代理實(shí)現(xiàn)織入,否則采用CGLIB動(dòng)態(tài)代理
- 代理對(duì)象(Proxy):通過織入產(chǎn)生的代理對(duì)象
- 切面(Aspect):切入點(diǎn)和通知的結(jié)合稱作切面
上邊提到的通知按照用途分為以下幾種:
- 前置通知:在切入點(diǎn)方法之前執(zhí)行
- 后置通知:在切入點(diǎn)方法正常還執(zhí)行完后執(zhí)行,和異常通知只會(huì)執(zhí)行其中一個(gè)
- 異常通知:當(dāng)切入點(diǎn)方法發(fā)生異常時(shí)執(zhí)行,和后置通知只會(huì)執(zhí)行其中一個(gè)
- 最終通知:無(wú)論切入點(diǎn)方法是否發(fā)生異常都會(huì)執(zhí)行,可以獲取方法的返回值
-
環(huán)繞通知:環(huán)繞通知更加靈活,可以不用配置上邊四種通知來(lái)實(shí)現(xiàn)切入點(diǎn)方法的增強(qiáng),讓開發(fā)者通過編碼的方式主動(dòng)控制增強(qiáng)代碼執(zhí)行的時(shí)機(jī)。但這要求開發(fā)者必須主動(dòng)調(diào)用切入點(diǎn)方法。Spring 提供了
ProceedingJoinPoint接口,該接口可以作為環(huán)繞通知的方法參數(shù),調(diào)用它的proceed()方法,就相當(dāng)于調(diào)用切入點(diǎn)方法。
注意:環(huán)繞通知和前邊四種通知不要一起使用。
還有一個(gè)知識(shí)點(diǎn)需要我們先了解下,那就是切入點(diǎn)表達(dá)式,先看一個(gè)切入點(diǎn)表達(dá)式:
execution(* com.shh.aop.CoffeeShop.sale(..))
它的作用就是匹配com.shh.aop包下,CoffeeShop類的sale()方法,實(shí)現(xiàn)攔截。先分析從這個(gè)切入點(diǎn)表達(dá)式可以看到的一些信息:
- execution:使用該關(guān)鍵字定義切入點(diǎn)表達(dá)式
- *:星號(hào)代表通配符,可以匹配返回值、包名、類名、方法
- (..):方法名后邊括號(hào)中的
..表示方法的任意參數(shù)(包括無(wú)參),也可以顯式的指定參數(shù)類型,例如int、java.lang.String等 - 切入點(diǎn)表達(dá)式中可以省略方法的權(quán)限修飾符
切入點(diǎn)表達(dá)式的定義很靈活,可以根據(jù)實(shí)際的需求變通,例如:
execution(* com.shh.aop.CoffeeShop.*(..))
表示會(huì)攔截com.shh.aop包下,CoffeeShop類的所有方法。
execution(* com.shh.aop.*.*(..))
表示會(huì)攔截com.shh.aop包下所有類的所有方法。
有了這些基礎(chǔ)知識(shí)的鋪墊,就更好理解后邊的內(nèi)容了。Spring AOP 使用的例子,會(huì)在之前 Java 動(dòng)態(tài)代理 中例子的基礎(chǔ)上擴(kuò)展。
二、基于xml的AOP使用
示例代碼要實(shí)現(xiàn)的功能大致是:有一個(gè)CoffeeShop實(shí)現(xiàn)類,其中sale()僅負(fù)責(zé)售賣咖啡,但我們希望增強(qiáng)sale()方法的功能,在sale()執(zhí)行前先向客戶問好,如果sale()正常執(zhí)行結(jié)束則去提示用戶付款,否則提示錯(cuò)誤信息,最后向用戶告別。
首先定義Shop接口:
public interface Shop {
void sale(String name);
}
CoffeeShop類實(shí)現(xiàn)Shop接口,重寫了要被攔截、增強(qiáng)的sale()方法,如果sale()方法接收到的參數(shù)為空,則直接拋出異常:
public class CoffeeShop implements Shop {
public void sale(String name) {
if (!StringUtils.isEmpty(name)) {
System.out.println("開始制作" + name + "......制作完成!");
} else {
throw new RuntimeException();
}
}
}
這樣被代理的類就定義好了,先放著后邊再用。然后定義通知類,也就是要對(duì)CoffeeShop類的sale()方法增強(qiáng)哪些功能:
public class Greet {
public void welcome() {
System.out.println("前置通知:歡迎!");
}
public void cashier() {
System.out.println("后置通知:請(qǐng)掃碼付款!");
}
public void soldOut() {
System.out.println("異常通知:商品名不能為空!");
}
public void goodbye() {
System.out.println("最終通知:再見!");
}
}
然后通過xml來(lái)配置AOP:
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--將CoffeeShop類交給IoC管理-->
<bean id="coffeeShop" class="com.shh.aop.CoffeeShop"/>
<!--將招呼的通知類交給IoC管理-->
<bean id="greet" class="com.shh.aop.Greet"/>
<!--Spring中基于xml的AOP配置-->
<!--1.使用<aop:pointcut>定義切入點(diǎn)表達(dá)式,注意要定義在<aop:aspect>前,如果定義在<aop:aspect>標(biāo)簽里只能當(dāng)前切面使用,不能公用-->
<!--2.使用<aop:config>標(biāo)簽開始配置AOP-->
<!--3.使用<aop:aspect>標(biāo)簽配置切面
id:切面的唯一標(biāo)識(shí)。
ref:需要引用通知類的bean id。-->
<!--4.使用<aop:before>標(biāo)簽配置前置通知,歡迎客戶
method:用Greet類的那個(gè)方法作為通知方法。
pointcut-ref:配置切入點(diǎn)表達(dá)式的引用,指定要對(duì)CoffeeShop類中的那些方法使用前置通知,實(shí)現(xiàn)增強(qiáng)。
使用<aop:after-returning>標(biāo)簽配置后置通知,提醒客戶支付
使用<aop:after-throwing>標(biāo)簽配置異常通知,商品信息有誤時(shí)的處理
使用<aop:after>標(biāo)簽配置最終通知,和客戶告別-->
<aop:config>
<aop:pointcut id="sale" expression="execution(* com.shh.aop.CoffeeShop.sale(..))"/>
<aop:aspect id="greetAdvice" ref="greet">
<aop:before method="welcome" pointcut-ref="sale"/>
<aop:after-returning method="cashier" pointcut-ref="sale"/>
<aop:after-throwing method="soldOut" pointcut-ref="sale"/>
<aop:after method="goodbye" pointcut-ref="sale"/>
</aop:aspect>
</aop:config>
</beans>
關(guān)鍵的說(shuō)明信息都在注釋里邊了,到這里編碼配置工作就結(jié)束了,接下來(lái)就是測(cè)試:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:aop.xml"})
public class AOPTest {
@Autowired
Shop coffeeShop;
@Test
public void saleTest() {
coffeeShop.sale("拿鐵");
}
}
輸出:

如果商品名為空:
coffeeShop.sale(""),則會(huì)有異常,是這樣的輸出結(jié)果:
這也符合我們開始設(shè)定的場(chǎng)景,經(jīng)過xml中的通知配置,額外的功能按約定好的規(guī)則自動(dòng)添加到了要被增強(qiáng)的方法前后。可以看出后置通知和異常通知只會(huì)執(zhí)行其中一個(gè)。
接下來(lái)使用環(huán)繞通知實(shí)現(xiàn)這個(gè)功能,首先修改通知類,只有一個(gè)greeting()方法:
public class Greet {
/**
* 環(huán)繞通知
*/
public Object greeting(ProceedingJoinPoint pjp) {
try {
System.out.println("前置通知:歡迎!");
// 獲取切入點(diǎn)方法的參數(shù)
Object[] params = pjp.getArgs();
// 主動(dòng)調(diào)用切入點(diǎn)方法(即執(zhí)行sale()方法)
Object result = pjp.proceed(params);
System.out.println("后置通知:請(qǐng)掃碼付款!");
return result;
} catch (Throwable throwable) {
System.out.println("異常通知:商品名不能為空!");
throw new RuntimeException(throwable);
} finally {
System.out.println("最終通知:再見!");
}
}
}
注意切入點(diǎn)方法的調(diào)用,即要增強(qiáng)的方法需要我們主動(dòng)調(diào)用,然后需要我們?cè)诤线m位置自行添加要增強(qiáng)的功能即可。
再修改xml配置文件,配置環(huán)繞通知:
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--將CoffeeShop類交給IoC管理-->
<bean id="coffeeShop" class="com.shh.aop.CoffeeShop"/>
<!--將招呼的通知類交給IoC管理-->
<bean id="greet" class="com.shh.aop.Greet"/>
<!--Spring中基于xml的AOP配置-->
<aop:config>
<aop:pointcut id="sale" expression="execution(* com.shh.aop.CoffeeShop.sale(..))"/>
<aop:aspect id="greetAdvice" ref="greet">
<aop:around method="greeting" pointcut-ref="sale"/>
</aop:aspect>
</aop:config>
</beans>
同樣可以實(shí)現(xiàn)上邊的效果。
三、基于注解的AOP使用
使用注解配置時(shí),就是要用對(duì)應(yīng)的注解,替換掉之前xml中的配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="coffeeShop" class="com.shh.aop.CoffeeShop"/>
<bean id="greet" class="com.shh.aop.Greet"/>
<aop:config>
<aop:pointcut id="sale" expression="execution(* com.shh.aop.CoffeeShop.sale(..))"/>
<aop:aspect id="greetAdvice" ref="greet">
<aop:before method="welcome" pointcut-ref="sale"/>
<aop:after-returning method="cashier" pointcut-ref="sale"/>
<aop:after-throwing method="soldOut" pointcut-ref="sale"/>
<aop:after method="goodbye" pointcut-ref="sale"/>
</aop:aspect>
</aop:config>
</beans>
-
<bean>可以用@Component注解代替 -
<aop:aspect>可以用@Aspect注解代替,來(lái)配置切面類 -
<aop:pointcut>可以用@Pointcut注解代替,來(lái)配置切點(diǎn) -
<aop:before>、<aop:after-returning>、<aop:after-throwing>、<aop:after>、<aop:around>分別對(duì)應(yīng)@Before、@AfterReturning、@AfterThrowing、@After、@Around注解,來(lái)配置各種通知
修改CoffeeShop類,添加@Component:
@Component
public class CoffeeShop implements Shop {
......
}
新建Greet2類,使用@Aspect、@Component,即切面類:
@Aspect
public class Greet2 {
/**
* 定義切入點(diǎn)(使用切入點(diǎn)表達(dá)式)
*/
@Pointcut("execution(* com.shh.aop.CoffeeShop.sale(..))")
public void sale() {
}
/**
* 定義切入點(diǎn)(使用注解,即要增強(qiáng)方法上使用的注解)
*/
@Pointcut("annotation(注解名)")
public void sale2() {
}
/**
* 注解的參數(shù)為切入點(diǎn)的引用
*/
@Before("sale()")
public void welcome() {
System.out.println("前置通知:歡迎!");
}
@AfterReturning(pointcut = "sale()", returning = "result")
public void cashier(Object result) {
System.out.println("后置通知:請(qǐng)掃碼付款!");
}
@AfterThrowing(pointcut = "sale()", throwing = "e")
public void soldOut(Exception e) {
System.out.println("異常通知:商品名不能為空!");
}
@After("sale()")
public void goodbye() {
System.out.println("最終通知:再見!");
}
}
到這里基于注解的AOP配置就基本完了,用一個(gè)切面類替換掉了之前xml中配置。
最后就是開啟 Spring 對(duì)基于注解AOP的支持,以及創(chuàng)建IoC容器時(shí)要掃描的包,有兩種方式可選:xml配置、java配置類。
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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!--配置創(chuàng)建IoC容器時(shí)要掃描的包-->
<context:component-scan base-package="com.shh.aop"/>
<!--開啟Spring對(duì)基于注解AOP的支持-->
<aop:aspectj-autoproxy/>
</beans>
java配置類的方式如下:
@Configuration
@ComponentScan(basePackages = "com.shh.aop")
@EnableAspectJAutoProxy
public class AOPConfig {
}
根據(jù)自己的實(shí)際需求選擇即可。
接下來(lái)測(cè)試一下:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {AOPConfig.class})
public class AOPTest {
@Autowired
Shop coffeeShop;
@Test
public void saleTest() {
coffeeShop.sale("拿鐵");
}
}

如果商品名為空:
coffeeShop.sale(""),則會(huì)有異常,是這樣的輸出結(jié)果:
注意,從圖中可以看出,后置通知、異常通知始終是最后輸出的,按照正常的邏輯應(yīng)該是最終通知最后輸出,而使用基于xml的AOP配置時(shí)確實(shí)正常的期望結(jié)果,這一點(diǎn)需要注意?。?!
但是如果使用基于注解的環(huán)繞通知?jiǎng)t不會(huì)用這樣的問題,畢竟環(huán)繞通知更加靈活,切入點(diǎn)方法和增強(qiáng)內(nèi)容的執(zhí)行順序可以由我們控制:
只需修改切面類,定義環(huán)繞通知的配置方法:
@Component
@Aspect
public class Greet2 {
/**
* 定義切入點(diǎn)
*/
@Pointcut("execution(* com.shh.aop.CoffeeShop.sale(..))")
public void sale() {
}
/**
* 環(huán)繞通知
*/
@Around("sale()")
public Object greeting(ProceedingJoinPoint pjp) {
try {
System.out.println("前置通知:歡迎!");
// 獲取切入點(diǎn)方法的參數(shù)
Object[] params = pjp.getArgs();
// 主動(dòng)調(diào)用切入點(diǎn)方法
Object result = pjp.proceed(params);
System.out.println("后置通知:請(qǐng)掃碼付款!");
return result;
} catch (Throwable throwable) {
System.out.println("異常通知:商品名不能為空!");
throw new RuntimeException(throwable);
} finally {
System.out.println("最終通知:再見!");
}
}
}
測(cè)試結(jié)果如下:


關(guān)于 Spring AOP 的內(nèi)容就先到這里了。