為什么需要AOP
?
AOP
(面向切面編程)和OOP
(面向?qū)ο缶幊?一樣芳来,也是一種編程思想。具體來(lái)說(shuō),AOP
是OOP
的一種有效補(bǔ)充资溃,以求解決OOP
中的一些弊端。在OOP
的思想下烈炭,我們可以很輕松的將一些業(yè)務(wù)需求抽象成一個(gè)個(gè)類(lèi)溶锭,形成可重用的模塊。但是遇到系統(tǒng)需求時(shí)符隙,往往捉襟見(jiàn)肘趴捅,造成大量的重復(fù)代碼,比如我們最常見(jiàn)的打印日志和權(quán)限驗(yàn)證的需求霹疫。
上圖中上拱绑,
Class A
、Class B
丽蝎、Class C
這三個(gè)不同的類(lèi)欺栗,卻都需要在某個(gè)方法執(zhí)行前進(jìn)行權(quán)限驗(yàn)證,在執(zhí)行后進(jìn)行日志記錄征峦。這樣橫跨了多個(gè)類(lèi)的共同需求迟几,我們稱(chēng)為橫切關(guān)注點(diǎn)
。在這里顯然varify()
和log()
在多個(gè)類(lèi)中重復(fù)栏笆,當(dāng)然重復(fù)代碼還不是最主要的問(wèn)題类腮,當(dāng)我們需要修改verify()
或log()
方法時(shí),我們要在A蛉加、B蚜枢、C
三個(gè)類(lèi)中都進(jìn)行修改缸逃,當(dāng)類(lèi)的數(shù)目越來(lái)越多,就會(huì)牽一發(fā)而動(dòng)全身厂抽。那么有人會(huì)說(shuō)需频,我們可以把verify()
和log()
抽象成一個(gè)類(lèi),如果需要進(jìn)行修改時(shí),就在這個(gè)類(lèi)中進(jìn)行挪丢。這個(gè)方案似乎可行,但是仍然存在問(wèn)題
- 因?yàn)楹芏鄷r(shí)候橫切關(guān)注點(diǎn)的邏輯和業(yè)務(wù)邏輯糾纏在一起乾蓬,并不是很好的進(jìn)行抽取族奢。
- 假設(shè)我們想將
log()
調(diào)整到方法執(zhí)行之前铜跑,或者說(shuō)在方法執(zhí)行前也添加log()
打印日志囤锉,那我們還是需要去大量的類(lèi)中手動(dòng)添加代碼驱入,這個(gè)方法治標(biāo)不治本。 - 如果能把所有的橫切關(guān)注點(diǎn)的邏輯直接抽離出來(lái),讓程序員專(zhuān)注于業(yè)務(wù)代碼就好了正卧,這樣子代碼的可讀性也會(huì)大大提高窘行。
AOP
就是為了幫助我們解決上述問(wèn)題而生的捏顺,具體來(lái)說(shuō)就是
- 幫助我們把橫切關(guān)注點(diǎn)從多個(gè)類(lèi)中抽取出來(lái),形成
Aspect
(切面)- 程序運(yùn)行時(shí)/編譯時(shí)逛艰,幫我們把這些橫切邏輯重新插入到每個(gè)類(lèi)中對(duì)應(yīng)的位置(
pointcut
)脸甘,這個(gè)過(guò)程叫做weaver
(織入)。
這種在運(yùn)行時(shí)铆遭,動(dòng)態(tài)地將代碼切入到類(lèi)的指定方法枚荣、指定位置上的編程思想就是面向切面的編程(AOP
)衙伶。AOP
是一種編程思想蹦漠,而Spring AOP
則是AOP
思想的具體實(shí)現(xiàn)车海。
Spring AOP的使用
在具體應(yīng)用之前研铆,讓我們先熟悉AOP
下的一些術(shù)語(yǔ)
術(shù)語(yǔ) | 解釋 |
---|---|
jointpoint |
系統(tǒng)運(yùn)行前,AOP 的功能模塊需要織入到OOP 的功能模塊中去州叠,jointpoint 就是指能夠進(jìn)行織入操作的執(zhí)行點(diǎn) |
pointcut |
切點(diǎn)棵红,一次織入過(guò)程中, 具體的jointpoint信息,比如要在A() 方法處織入橫切邏輯咧栗,那么A() 就是pointcut
|
advice |
通知逆甜,代表具體的橫切邏輯虱肄,可以類(lèi)比OOP 中的method ,注意:advice 還指明了執(zhí)行橫切邏輯的時(shí)間的交煞,比如在A() 執(zhí)行方法之前執(zhí)行咏窿,還是在其之后執(zhí)行等 |
aspect |
切面,point + advice = aspect 素征, 在哪些切點(diǎn)(切點(diǎn)是個(gè)集合)上執(zhí)行何種橫切邏輯(比如打印日志)就是一個(gè)切面 |
在不同的AOP
實(shí)現(xiàn)中集嵌,jointpoint
的粒度不同,在Spring AOP
中御毅,這個(gè)jointpoint
是方法級(jí)別的根欧,也就是只提供方法攔截,但即便這樣端蛆,也足以滿(mǎn)足80%的業(yè)務(wù)需求了凤粗。advice
除了定義了橫切邏輯,還定義了橫切邏輯執(zhí)行的時(shí)機(jī)欺税,在Spring AOP
中有前置侈沪、后置、返回晚凿、異常亭罪、環(huán)繞五種Advice
,例如前置型Advice
,表示在pointcut
前執(zhí)行橫切邏輯歼秽,下面會(huì)舉例詳細(xì)說(shuō)明应役。
前置Advice
首先讓我們定義一個(gè)People
類(lèi),它包含一個(gè)eatFruit
表示吃水果的這個(gè)行為燥筷,我們將嘗試以這個(gè)訪(fǎng)問(wèn)為pointcut
箩祥,來(lái)進(jìn)行織入工作。然后我們來(lái)定義Advice
肆氓,在Spring AOP
中袍祖,Advice
是實(shí)現(xiàn)了對(duì)應(yīng)接口的類(lèi),如果我們要實(shí)現(xiàn)一個(gè)前置型的Advice
谢揪,就要實(shí)現(xiàn)MethodBeforeAdvice
中的方法蕉陋。在這里我們定義了一個(gè)名為BeforeEat
的前置型Advice
,表示吃之前要執(zhí)行的橫切邏輯拨扶。
- people 類(lèi)
public class People {
public void eatFruit(){
System.out.println("正在吃水果");
}
}
- BeforeEat 類(lèi)
public class BeforeEat implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("eat方法的前置通知: 我要開(kāi)始吃了凳鬓!");
}
}
接下來(lái)讓我們把這兩個(gè)類(lèi)注入到Spring IOC
容器中,交由Spring
管理患民。
<bean id="people" class="aop.People">
</bean>
<bean id="beforeEat" class="aop.BeforeEat">
</bean>
之后最重要的是告訴Spring
缩举,pointcut
是哪些方法?,和pointcut
關(guān)聯(lián)Advice
是哪一個(gè)仅孩,讓我們完善aop config
托猩。
<aop:config>
<aop:pointcut expression="execution(public void aop.People.eatFruit())" id="pointcut"></aop:pointcut>
<aop:advisor advice-ref="beforeEat" pointcut-ref="pointcut"></aop:advisor>
</aop:config>
<aop:pointcut>
表示pointcut
是People
類(lèi)的eatFruit
方。之前我們有提到過(guò)point
+ advice
= aspect
杠氢,而<aop:advisor>
標(biāo)簽中的就可以理解為aspect
,它關(guān)聯(lián)了與advice
對(duì)應(yīng)的pointcut
站刑。下面讓我們調(diào)用下People
類(lèi)的eatFruit()
方法看看是什么效果。
執(zhí)行前需要先導(dǎo)入aspectJweaver.jar包
- 調(diào)用
earFruit()
方法
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
People people = (People)applicationContext.getBean("people");
people.eatFruit();
}
- 執(zhí)行結(jié)果
eat方法的前置通知: 我要開(kāi)始吃了鼻百!
正在吃水果
可以發(fā)現(xiàn)橫切邏輯在方法執(zhí)行前被調(diào)用了绞旅。
之前我們說(shuō)過(guò), pointcut
在這里可以看作要被織入橫切邏輯的具體位置(方法)的集合,因此pointcut
內(nèi)部可以包含多種方法温艇,讓我們?cè)?code>People類(lèi)中添加一個(gè)drinkSomething
方法因悲。
public class People {
public void eatFruit(){
System.out.println("正在吃水果");
}
public void drinkSomething(String sth){
System.out.println("正在喝"+sth);
}
}
把這個(gè)方法也加入到當(dāng)前的pointcut
中去。
<aop:config>
<aop:pointcut expression="execution(public void aop.People.eatFruit()) or
execution(public void aop.People.drinkSomething(String)) " id="pointcut"></aop:pointcut>
<aop:advisor advice-ref="beforeEat" pointcut-ref="pointcut"></aop:advisor>
</aop:config>
pointcut
中兩個(gè)方法用or
連接勺爱。運(yùn)行結(jié)果是在這2
個(gè)方法調(diào)用前都會(huì)執(zhí)行橫切邏輯BeforeEat
eat方法的前置通知: 我要開(kāi)始吃了晃琳!
正在吃水果
eat方法的前置通知: 我要開(kāi)始吃了!
正在喝牛奶
Process finished with exit code 0
可以看到pointcut
中的expression
是支持集合的交并補(bǔ)運(yùn)算的琐鲁,此外還支持通配符的方式卫旱,來(lái)指代一類(lèi)方法。比如我們可以修改<aop:config>
為:
<aop:config>
<aop:pointcut expression="execution(public void * (String))" id="pointcut"></aop:pointcut>
<aop:advisor advice-ref="beforeEat" pointcut-ref="pointcut"></aop:advisor>
</aop:config>
就表示任何以String
為參數(shù)(不限方法名)的方法围段,在這里也就只有drinkSomething(String sth)
滿(mǎn)足條件顾翼,嘗試運(yùn)行發(fā)現(xiàn)也的確只在這個(gè)方法前執(zhí)行了橫切邏輯。通過(guò)通配符和集合運(yùn)算的方式奈泪,可以容易的指定一類(lèi)具體的的方法為pointcut
适贸。
正在吃水果
eat方法的前置通知: 我要開(kāi)始吃了!
正在喝牛奶
Process finished with exit code 0
現(xiàn)在讓我們?cè)倩氐?code>Advice類(lèi)的定義上涝桅,看看接口方法中的參數(shù)都代表了什么拜姿。
public class BeforeEat implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println(method+" " + Arrays.toString(objects) + " " + o);
System.out.println("eat方法的前置通知: 我要開(kāi)始吃了!");
}
}
執(zhí)行結(jié)果
public void aop.People.drinkSomething(java.lang.String) [牛奶] aop.People@424e1977
eat方法的前置通知: 我要開(kāi)始吃了冯遂!
正在喝牛奶
Process finished with exit code 0
可以發(fā)現(xiàn)method
即與橫切邏輯advice
關(guān)聯(lián)的具體方法蕊肥,在這里就是public void aop.People.drinkSomething(java.lang.String)
, Object[] objects
則是傳入該方法的參數(shù),object
則是執(zhí)行橫切邏輯的方法所屬的對(duì)象實(shí)例蛤肌,這里就是IOC
中id=people
的這個(gè)bean
壁却。
后置Advice
后置型Advice
與前置型Advice
正相反,表示在pointcut
之后執(zhí)行橫切邏輯寻定。我們編寫(xiě)一個(gè)名為AfterEat
的后置型Advice
儒洛。
public class AfterEat implements AfterReturningAdvice {
@Override
public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
System.out.println("吃完了精耐,洗洗手狼速。");
}
}
為其編寫(xiě)xml
配置。
<bean id="afterEat" class="aop.AfterEat">
</bean>
<aop:config>
<aop:pointcut expression="execution(public void aop.People.eatFruit()) or
execution(public void aop.People.drinkSomething(String)) " id="pointcut"></aop:pointcut>
<aop:advisor advice-ref="afterEat" pointcut-ref="pointcut"></aop:advisor>
</aop:config>
執(zhí)行結(jié)果
public void aop.People.eatFruit() [] aop.People@1190200a
eat方法的前置通知: 我要開(kāi)始吃了卦停!//前置
正在吃水果
吃完了向胡,洗洗手恼蓬。//后置
public void aop.People.drinkSomething(java.lang.String) [牛奶] aop.People@1190200a
eat方法的前置通知: 我要開(kāi)始吃了!//前置
正在喝牛奶
吃完了僵芹,洗洗手处硬。//后置
注意到AfterReturningAdvice
接口中的afterReturning
方法中的參數(shù)與前置Advice
有差別,讓我們嘗試打印一下拇派。
public class AfterEat implements AfterReturningAdvice {
@Override
public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
System.out.println(method+" " + Arrays.toString(objects) + " " + o + " " + o1);
System.out.println("吃完了荷辕,洗洗手。");
}
}
輸出結(jié)果
public void aop.People.eatFruit() [] null aop.People@1190200
可以看到o1
輸出的是對(duì)象實(shí)例件豌,而o
輸出的值是null
, 那么o
代表什么呢疮方?讓我們修改drinkSomething(String)
的返回值為int
,再打印一次
public int drinkSomething(String sth){
System.out.println("正在喝"+sth);
return 0;
}
public int aop.People.drinkSomething(java.lang.String) [牛奶] 0 aop.People@1190200a
發(fā)現(xiàn)o
的值變?yōu)?code>0,也就是說(shuō)其代表了橫切邏輯執(zhí)行前這個(gè)方法的返回值茧彤。
異常Advice
異常Advice
指的是當(dāng)pointcut
中的方法拋出異常時(shí)骡显,將會(huì)執(zhí)行的橫切邏輯。
- 編寫(xiě)異常
Advice
public class WhenException implements ThrowsAdvice {
/*
* <pre class="code">public void afterThrowing(Exception ex)</pre>
* <pre class="code">public void afterThrowing(RemoteException)</pre>
* <pre class="code">public void afterThrowing(Method method, Object[] args, Object target, Exception ex)</pre>
* <pre class="code">public void afterThrowing(Method method, Object[] args, Object target, ServletException ex)</pre>
*/
}
ThrowsAdvice
這個(gè)接口并沒(méi)有要求我們實(shí)現(xiàn)任何接口方法曾掂,而是在文檔里給出了一些示例惫谤,還告訴我們Method method, Object[] args, Object target
,這3個(gè)打包在一起的參數(shù)是可選的珠洗,如果你想獲得更詳細(xì)的信息溜歪,就加上它們。
- 實(shí)現(xiàn)異常
Advice
類(lèi)
public class WhenException implements ThrowsAdvice {
public void afterThrowing(Exception ex) {
System.out.println("異常Advice : 發(fā)生了異常");
System.out.println(ex.getMessage());
}
}
編寫(xiě)app config
<bean id="whenException" class="aop.WhenException"></bean>
<aop:config>
<aop:pointcut expression="execution(public void aop.People.eatFruit()) or
execution(public int aop.People.drinkSomething(String)) " id="pointcut"></aop:pointcut>
<aop:advisor advice-ref="whenException" pointcut-ref="pointcut"></aop:advisor>
</aop:config>
再在drinkSomething()
方法里故意引起一個(gè)異常险污。
public int drinkSomething(String sth){
System.out.println("正在喝"+sth);
int a = 1 / 0;
return 0;
}
執(zhí)行結(jié)果
public int aop.People.drinkSomething(java.lang.String) [牛奶] aop.People@4c39bec8
eat方法的前置通知: 我要開(kāi)始吃了痹愚!
正在喝牛奶
異常Advice : 發(fā)生了異常
/ by zero
環(huán)繞Advice
截至目前為止,我們已經(jīng)實(shí)驗(yàn)了前置
蛔糯、后置
拯腮、異常
三種Advice。它們執(zhí)行的時(shí)機(jī)如下蚁飒。
環(huán)繞型
Advice动壤,可以實(shí)現(xiàn)以上三種Advice
的所有功能,即可以同時(shí)在上述的所有位置執(zhí)行橫切邏輯淮逻。
- 實(shí)現(xiàn)一個(gè)環(huán)繞型
Advice
public class AroundEat implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
System.out.println("環(huán)繞Advice: 方法執(zhí)行前" );// 前置
Object result = invocation.proceed();// pointcut中方法的執(zhí)行
System.out.println("環(huán)繞Advice: 方法執(zhí)行后" );// 后置
} catch (Exception e) {
System.out.println("環(huán)繞Advice: 發(fā)生異常");
}
return null;
}
}
這里的關(guān)鍵是Object result = invocation.proceed()琼懊;
,這里就相當(dāng)于執(zhí)行我們定義在pointcut
中的方法,因此在這行語(yǔ)句前面執(zhí)行的邏輯爬早,相當(dāng)于前置advice
哼丈,在這行語(yǔ)句后面執(zhí)行的邏輯,相當(dāng)于后置advice
筛严。捕捉到異常后實(shí)現(xiàn)的邏輯就相當(dāng)于異常advice
醉旦。
為其配置aop
,進(jìn)行驗(yàn)證。
<bean id="aroundEat" class="aop.AroundEat"></bean>
<aop:config>
<aop:pointcut expression="execution(public void aop.People.eatFruit()) or
execution(public int aop.People.drinkSomething(String)) " id="pointcut"></aop:pointcut>
<aop:advisor advice-ref="aroundEat" pointcut-ref="pointcut"></aop:advisor>
</aop:config>
- 運(yùn)行結(jié)果
環(huán)繞Advice: 方法執(zhí)行前
正在吃水果
環(huán)繞Advice: 方法執(zhí)行后
環(huán)繞Advice: 方法執(zhí)行前
正在喝牛奶
環(huán)繞Advice: 發(fā)生異常
利用注解的形式實(shí)現(xiàn)AOP
Spring AOP
车胡,也提供了基于注解的形式實(shí)現(xiàn)AOP
, 較XML
配置的方法更加簡(jiǎn)單直觀檬输,我們來(lái)利用注解實(shí)現(xiàn)AOP
,以前置Advice
為例,將之前的BeforeEat
改進(jìn)為基于注解的方式匈棘。
@Component("beforeEatAnnotation")
@Aspect
public class BeforeEatAnnotation {
@Before("execution(public void aop.People.eatFruit())") //定義切點(diǎn)
void before(){
System.out.println("采用注解形式實(shí)現(xiàn)的前置通知");
}
@AfterReturning("execution(public void aop.People.eatFruit())")
void after(){
System.out.println("采用注解形式實(shí)現(xiàn)的后置通知");
}
}
和我們之前基于XML
的配置一樣丧慈,我們要定義具體的pointcut
并且把其和關(guān)聯(lián)的Advice
綁定起來(lái),在這個(gè)類(lèi)里我們可以在任意方法前加上@Before注解,表示該方法是一個(gè)前置advice
主卫,然后在其括號(hào)內(nèi)注明pointcut
逃默,這樣pointcut
和advice
很自然的關(guān)聯(lián)在一起了,所以也無(wú)需之前的<aop:advisor>
來(lái)指明兩者關(guān)系了簇搅。@Aspect
代表這個(gè)類(lèi)表示一個(gè)切面笑旺。@Component
把這個(gè)類(lèi)交由Spring
管理,注意配置自動(dòng)掃描馍资。
最后筒主,我還需要在xml
中配置aop
自動(dòng)代理。
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
實(shí)驗(yàn)結(jié)果
采用注解形式實(shí)現(xiàn)的前置通知
正在吃水果
采用注解形式實(shí)現(xiàn)的后置通知
Process finished with exit code 0
之前利用接口的方式來(lái)實(shí)現(xiàn)AOP
可以很容易的獲得目標(biāo)對(duì)象鸟蟹,方法名乌妙、參數(shù)等信息,利用注解的方式也可以實(shí)現(xiàn)建钥,這里需要借助一個(gè)特殊的JoinPoint
類(lèi)藤韵。
@Component("adviceByAnnotation")
@Aspect
public class AdviceByAnnotation {
@Before("execution(public void aop.People.eatFruit())") //定義切點(diǎn)
void before(JoinPoint joinPoint){
System.out.println(joinPoint.getTarget() + " " + Arrays.toString(joinPoint.getArgs()) + " " + joinPoint.getSignature());
System.out.println("采用注解形式實(shí)現(xiàn)的前置通知");
}
@AfterReturning(pointcut="execution(public void aop.People.eatFruit())", returning = "returningValue")
void after(JoinPoint joinPoint, Object returningValue){
System.out.println("返回值為" + returningValue);
System.out.println("采用注解形式實(shí)現(xiàn)的后置通知");
}
}
可以發(fā)現(xiàn)pointcut
中的特定方法的有關(guān)信息都已經(jīng)被包裝到JoinPoint
類(lèi)中去了。對(duì)于以@AfterReturning標(biāo)注的后置Advice
熊经,還可以指明獲取返回值泽艘。
實(shí)驗(yàn)結(jié)果如下
aop.People@140c9f39 [] void aop.People.eatFruit()
采用注解形式實(shí)現(xiàn)的前置通知
正在吃水果
返回值為null
采用注解形式實(shí)現(xiàn)的后置通知
類(lèi)似的我們還可以實(shí)現(xiàn)基于注解的異常Advice
和環(huán)繞Advice
以及最終Advice
。
@After("execution(public int aop.People.drinkSomething(String))")
void after(){
System.out.println("最終通知镐依,無(wú)論有沒(méi)有發(fā)生異常匹涮,都會(huì)執(zhí)行");
}
//異常通知
@AfterThrowing("execution(public int aop.People.drinkSomething(String))")
void afterException(){
System.out.println("采用注解形式的異常通知");
}
//環(huán)繞通知
@Around("execution(public int aop.People.drinkSomething(String))")
void around(ProceedingJoinPoint proceedingJoinPoint) {
try {
System.out.println("采用注解形式的環(huán)繞通知[前置]");
proceedingJoinPoint.proceed();
System.out.println("采用注解形式的環(huán)繞通知[后置]");
}catch (Throwable e) {
System.out.println("采用注解形式的環(huán)繞通知[異常]");
} finally {
System.out.println("采用注解形式的環(huán)繞通知[最終]");
}
}
環(huán)繞Advice
里,proceedingJoinPoint.proceed();
就是真正執(zhí)行了pointcut
集合中某個(gè)具體方法槐壳。注意這里區(qū)別最終和后置的區(qū)別然低,后置Advice
如果發(fā)生異常則不會(huì)被執(zhí)行,而最終Advice
是一定會(huì)被執(zhí)行的务唐。
執(zhí)行結(jié)果如下
采用注解形式的環(huán)繞通知[前置]
正在喝牛奶
采用注解形式的環(huán)繞通知[異常]
采用注解形式的環(huán)繞通知[最終]