AOP概念
AOP的定義:AOP,Aspect Oriented Programming的縮寫筑悴,意為面向切面編程鸳劳,是通過預(yù)編譯或運行期動態(tài)代理實現(xiàn)程序功能處理的統(tǒng)一維護(hù)的一種技術(shù)。
系統(tǒng)中往往存在非常多的Controller匹配各種請求〖吒現(xiàn)在,如果我們想在每一個Controller的RequestMapping請求識別到的時候做個記錄格遭。正常的處理邏輯是在每個Request請求到的時候?qū)憘€Log哈街。然后執(zhí)行相應(yīng)的方法。但是這樣的話要把所有的RequestMapping方法都加上這個Log拒迅,顯然非常麻煩骚秦。面向切面就是希望只寫一遍Log就讓所有的請求都能夠執(zhí)行。橫向的將代碼嵌入到各個請求中璧微。如下圖那樣作箍,并不影響原本程序的原有邏輯。
接下來看一下幾個名詞
切面(Aspect):橫切關(guān)注點可以被模塊化為特殊的類被稱為切面(aspect)前硫。
通知(Advice):通知定義了切面是什么以及合適使用胞得。通常有五種類型:
- 前置通知(Before):在目標(biāo)方法被調(diào)用前調(diào)用通知功能;
- 后置通知(After):在目標(biāo)方法完成之后調(diào)用通知
- 返回通知(After-returning):在目標(biāo)方法成功執(zhí)行后調(diào)用通知屹电;
- 異常通知(After-throwing):在目標(biāo)方法拋出異常后調(diào)用通知阶剑;
- 環(huán)繞通知(Around):通知包裹了被通知的方法,在通知的方法調(diào)用之前和調(diào)用之后執(zhí)行自定義的行為危号。
連接點(Join point):通知執(zhí)行的時機(jī)个扰。例如高速路上有非常多的出口,這些出口都相當(dāng)于連接點葱色。
切入點(Poincut):通知所要織入的具體位置递宅。還是以高速路為例子,眾多出口(連接點)中,我們只需要找到一個出口办龄,這個出口就是切入點烘绽。
引入(Introduction):允許我們向現(xiàn)有的類添加新方法和屬性。
織入(Weaving):把切面用到目標(biāo)對象并創(chuàng)建新的代理對象的過程俐填。
下圖為《Spring實戰(zhàn)》中關(guān)于各個名詞結(jié)合的圖安接。
Spring對AOP的支持
- 基于代理的Spring AOP
- 純POJO切面
- @AspectJ注解驅(qū)動的切面
- 注入式AspectJ切面(適用于Spring各版本)
注:Spring只支持方法級別的連接點
通過切點選擇連接點
Spring支持Aspectj的指示器中只有execution指示器是實際執(zhí)行匹配的。
首先英融,我們有一個發(fā)送短信的接口
public interface MessageService {
void sendMessage(String msg);
}
我們希望在MessageService進(jìn)行sendService方法時候觸發(fā)通知盏檐。就有了如下的表達(dá)式。
execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))
- 表示可以返回任意類型驶悟,接下來是全限定類名胡野,方法。方法中的參數(shù)為 .. 表明可以使用任意參數(shù)痕鳍。
使用注解創(chuàng)建切面
我們定義一個切面記錄方法的開始執(zhí)行的時間與執(zhí)行結(jié)束的時間硫豆,以及失敗的時間,此外還有@AfterRetrun和@Around
@Aspect
public class TimeLogging {
@Before("execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))")
public void recordBeforeExecute() {
System.out.println("start to send message at " + LocalDateTime.now());
}
@After("execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))")
public void recordAfterExecute() {
System.out.println("message has sent seccess at " + LocalDateTime.now());
}
@AfterThrowing("execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))")
public void recordFailure() {
System.out.println("fail to send message at " + LocalDateTime.now());
}
}
顯然笼呆,上面同一個切入點表達(dá)式寫了三遍非常難看熊响。因此,可以使用@Pointcut注解定義命名的切點
@Aspect
public class TimeLogging {
// 定義命名的切點
@Pointcut("execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))")
public void sendMessage() {}
@Before("sendMessage()")
public void recordBeforeExecute() {
System.out.println("start to send message at " + LocalDateTime.now());
}
@After("sendMessage()")
public void recordAfterExecute() {
System.out.println("message has sent seccess at " + LocalDateTime.now());
}
@AfterThrowing("sendMessage()")
public void recordFailure() {
System.out.println("fail to send message at " + LocalDateTime.now());
}
}
接下來需要配置Bean及啟用AspectJ注解的自動代理诗赌,JavaConfig可以使用@EnableAspectJ-AutoProxy注解啟用汗茄;Xml可以使用Spring aop命名空間中的<aop:aspectj-autoproxy>元素。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/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="messageService" class="com.dqzhou.spring.service.impl.MessageServiceImpl"/>
<bean id="timeLogging" class="com.dqzhou.spring.aspectj.TimeLogging"/>
<aop:aspectj-autoproxy/>
</beans>
最后铭若,測試一下調(diào)用messageService的sendMessage方法
public class SpringApplicationContext {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
MessageService messageService = (MessageService) applicationContext.getBean("messageService");
messageService.sendMessage("hello world!");
}
}
結(jié)果如下剔难,Spring通過代理的方式增強(qiáng)方法
start to send message at 2019-10-28T00:16:11.746
hello world!
message has sent seccess at 2019-10-28T00:16:11.748
創(chuàng)建環(huán)繞通知
環(huán)繞通知相當(dāng)于在通知方法中同時編寫前置和后置通知。
更改切面類如下奥喻,ProceedingJoinPoint作為參數(shù)偶宫。通過它能夠調(diào)用被通知的方法。顯然环鲤,環(huán)繞通知可以輕易實現(xiàn)其它幾個注解所實現(xiàn)的功能纯趋,但是如果proceed()方法沒有調(diào)用,被通知的方法將無法訪問冷离。不過可以通過多次調(diào)用達(dá)到重試場景吵冒。
@Aspect
public class TimeLogging {
// 定義命名的切點
@Pointcut("execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))")
public void sendMessage() {}
@Around("sendMessage()")
public void watchMessage(ProceedingJoinPoint joinPoint) {
try {
System.out.println("start to send message at " + LocalDateTime.now());
joinPoint.proceed();
System.out.println("message has sent seccess at " + LocalDateTime.now());
} catch (Throwable throwable) {
System.out.println("fail to send message at " + LocalDateTime.now());
}
}
}
通過切面引入新功能
之前,Spring AOP通過代理已經(jīng)能夠?qū)Ρ煌ㄖ姆椒ㄟM(jìn)行增強(qiáng)西剥。那么痹栖,有沒有可能在不破壞,沒有嵌入原有類的結(jié)構(gòu)上增加新的方法瞭空。
@DeclareParents注解揪阿,做個標(biāo)記疗我,暫不展開
使用XML聲明切面
Spring的aop命名空間
- <aop:advisor>:定義AOP通知器
- <aop:after>:定義AOP后置通知(不管被通知的方法是否執(zhí)行成功)
- <aop:after-returning>
- <aop:after-throwing>
- <aop:around>
- <aop:aspect>:定義一個切面
- <aop:aspectj-autoproxy>:啟用@AspectJ注解驅(qū)動的切面
- <aop:before>
- <aop:config>:頂層的AOP配置元素。大多數(shù)的<aop:*>元素必須包含
在<aop:config>元素內(nèi) - <aop:declareparents>:以透明的方式為被通知的對象引入額外的接口
- <aop:pointcut>:定義一個切點
創(chuàng)建一個切面類南捂,不加任何注解
public class LogAspect {
public void logBeforeExecute() {
System.out.println("method ready to execute at" + LocalDateTime.now());
}
public void logAfterExecute() {
System.out.println("method has executed at" + LocalDateTime.now());
}
public void logFailure() {
System.out.println("execute fail at " + LocalDateTime.now());
}
public void logAroundMessage(ProceedingJoinPoint joinPoint) {
try {
System.out.println("method ready to execute at" + LocalDateTime.now());
joinPoint.proceed();
System.out.println("method has executed at" + LocalDateTime.now());
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
配置xml
<bean id="logAspect" class="com.dqzhou.spring.aop.aspectj.LogAspect"/>
<aop:config>
<aop:aspect ref="logAspect">
<aop:before pointcut="execution(* com.dqzhou.spring.aop.service.MessageService.sendMessage(..))" method="logBeforeExecute"/>
<aop:after pointcut="execution(* com.dqzhou.spring.aop.service.MessageService.sendMessage(..))" method="logAfterExecute"/>
</aop:aspect>
</aop:config>
大多數(shù)aop配置必須在<aop:config>元素的上下文使用吴裤。然后<aop:config>可以聲明多個通知。同樣的溺健,寫多遍切點表達(dá)式很麻煩麦牺,可以使用<aop:pointcut>指定切點,然后通知通過pointcut-ref屬性來引用命名切點鞭缭,如下:
<aop:config>
<aop:aspect ref="logAspect">
<aop:pointcut expression="execution(* com.dqzhou.spring.aop.service.MessageService.sendMessage(..))" id="logMessage"/>
<aop:before pointcut-ref="logMessage" method="logBeforeExecute"/>
<aop:after pointcut-ref="logMessage" method="logAfterExecute"/>
</aop:aspect>
</aop:config>
如果使用環(huán)繞通知剖膳,xml配置如下:
<aop:config>
<aop:aspect ref="logAspect">
<aop:pointcut expression="execution(* com.dqzhou.spring.aop.service.MessageService.sendMessage(..))" id="logMessage"/>
<aop:around pointcut-ref="logMessage" method="logAroundMessage"/>
</aop:aspect>
</aop:config>
總結(jié)
面向?qū)ο缶幊掏ㄟ^AOP把應(yīng)用各處的行為放入可重用的模塊中。減少了代碼的冗余岭辣。Spring提供了AOP的框架吱晒,可以通過XML或注解的方式快速進(jìn)行配置。但是如果SpringAOP不能滿足需求的時候易结,需要專項更為強(qiáng)大的AspectJ枕荞。