AOP概念
AOP的定義:AOP,Aspect Oriented Programming的縮寫,意為面向切面編程,是通過預(yù)編譯或運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)程序功能處理的統(tǒng)一維護(hù)的一種技術(shù)。
系統(tǒng)中往往存在非常多的Controller匹配各種請求?,F(xiàn)在,如果我們想在每一個(gè)Controller的RequestMapping請求識(shí)別到的時(shí)候做個(gè)記錄。正常的處理邏輯是在每個(gè)Request請求到的時(shí)候?qū)憘€(gè)Log。然后執(zhí)行相應(yīng)的方法。但是這樣的話要把所有的RequestMapping方法都加上這個(gè)Log,顯然非常麻煩。面向切面就是希望只寫一遍Log就讓所有的請求都能夠執(zhí)行。橫向的將代碼嵌入到各個(gè)請求中。如下圖那樣,并不影響原本程序的原有邏輯。

接下來看一下幾個(gè)名詞
切面(Aspect):橫切關(guān)注點(diǎ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í)行自定義的行為。
連接點(diǎn)(Join point):通知執(zhí)行的時(shí)機(jī)。例如高速路上有非常多的出口,這些出口都相當(dāng)于連接點(diǎn)。
切入點(diǎn)(Poincut):通知所要織入的具體位置。還是以高速路為例子,眾多出口(連接點(diǎn))中,我們只需要找到一個(gè)出口,這個(gè)出口就是切入點(diǎn)。
引入(Introduction):允許我們向現(xiàn)有的類添加新方法和屬性。
織入(Weaving):把切面用到目標(biāo)對象并創(chuàng)建新的代理對象的過程。
下圖為《Spring實(shí)戰(zhàn)》中關(guān)于各個(gè)名詞結(jié)合的圖。

Spring對AOP的支持
- 基于代理的Spring AOP
- 純POJO切面
- @AspectJ注解驅(qū)動(dòng)的切面
- 注入式AspectJ切面(適用于Spring各版本)
注:Spring只支持方法級別的連接點(diǎn)
通過切點(diǎn)選擇連接點(diǎn)
Spring支持Aspectj的指示器中只有execution指示器是實(shí)際執(zhí)行匹配的。
首先,我們有一個(gè)發(fā)送短信的接口
public interface MessageService {
void sendMessage(String msg);
}
我們希望在MessageService進(jìn)行sendService方法時(shí)候觸發(fā)通知。就有了如下的表達(dá)式。
execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))
- 表示可以返回任意類型,接下來是全限定類名,方法。方法中的參數(shù)為 .. 表明可以使用任意參數(shù)。
使用注解創(chuàng)建切面
我們定義一個(gè)切面記錄方法的開始執(zhí)行的時(shí)間與執(zhí)行結(jié)束的時(shí)間,以及失敗的時(shí)間,此外還有@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());
}
}
顯然,上面同一個(gè)切入點(diǎn)表達(dá)式寫了三遍非常難看。因此,可以使用@Pointcut注解定義命名的切點(diǎn)
@Aspect
public class TimeLogging {
// 定義命名的切點(diǎn)
@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注解的自動(dòng)代理,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)于在通知方法中同時(shí)編寫前置和后置通知。
更改切面類如下,ProceedingJoinPoint作為參數(shù)。通過它能夠調(diào)用被通知的方法。顯然,環(huán)繞通知可以輕易實(shí)現(xiàn)其它幾個(gè)注解所實(shí)現(xiàn)的功能,但是如果proceed()方法沒有調(diào)用,被通知的方法將無法訪問。不過可以通過多次調(diào)用達(dá)到重試場景。
@Aspect
public class TimeLogging {
// 定義命名的切點(diǎn)
@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注解,做個(gè)標(biāo)記,暫不展開
使用XML聲明切面
Spring的aop命名空間
- <aop:advisor>:定義AOP通知器
- <aop:after>:定義AOP后置通知(不管被通知的方法是否執(zhí)行成功)
- <aop:after-returning>
- <aop:after-throwing>
- <aop:around>
- <aop:aspect>:定義一個(gè)切面
- <aop:aspectj-autoproxy>:啟用@AspectJ注解驅(qū)動(dòng)的切面
- <aop:before>
- <aop:config>:頂層的AOP配置元素。大多數(shù)的<aop:*>元素必須包含
在<aop:config>元素內(nèi) - <aop:declareparents>:以透明的方式為被通知的對象引入額外的接口
- <aop:pointcut>:定義一個(gè)切點(diǎn)
創(chuàng)建一個(gè)切面類,不加任何注解
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>可以聲明多個(gè)通知。同樣的,寫多遍切點(diǎn)表達(dá)式很麻煩,可以使用<aop:pointcut>指定切點(diǎn),然后通知通過pointcut-ref屬性來引用命名切點(diǎn),如下:
<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不能滿足需求的時(shí)候,需要專項(xiàng)更為強(qiáng)大的AspectJ。