聊聊SpringAOP那些不為人知的秘密
點擊關(guān)注公眾號,Java干貨及時送達(dá)

引出AOP
SpringAOP是Spring框架中非常重要的一個概念,AOP,意為面向切面編程。
AOP是OOP的延續(xù),是軟件開發(fā)中的一個熱點,也是Spring框架中的一個重要內(nèi)容,是函數(shù)式編程的一種衍生范型。利用AOP可以對業(yè)務(wù)邏輯的各個部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發(fā)的效率。
來看一個例子,首先我們創(chuàng)建一個接口:
public?interface?CalculateService?{
????int?add(int?x,?int?y);
????int?reduce(int?x,?int?y);
????int?multi(int?x,?int?y);
????int?division(int?x,?int?y);
}
然后創(chuàng)建實現(xiàn)類:
@Service
public?class?CalculateServiceImpl?implements?CalculateService?{
????@Override
????public?int?add(int?x,?int?y)?{
????????System.out.println(x?+?"?+?"?+?y?+?"?=?"?+?(x?+?y));
????????return?x?+?y;
????}
????@Override
????public?int?reduce(int?x,?int?y)?{
????????System.out.println(x?+?"?-?"?+?y?+?"?=?"?+?(x?-?y));
????????return?x?-?y;
????}
????@Override
????public?int?multi(int?x,?int?y)?{
????????System.out.println(x?+?"?*?"?+?y?+?"?=?"?+?(x?*?y));
????????return?x?*?y;
????}
????@Override
????public?int?division(int?x,?int?y)?{
????????System.out.println(x?+?"?/?"?+?y?+?"?=?"?+?(x?/?y));
????????return?x?/?y;
????}
}
此時我們從容器中獲取這個組件并調(diào)用計算方法:
public?static?void?main(String[]?args)?throws?Exception?{
????ApplicationContext?context?=?new?AnnotationConfigApplicationContext(MyConfiguration.class);
????CalculateService?calculateService?=?context.getBean("calculateServiceImpl",?CalculateService.class);
????calculateService.add(1,1);
????calculateService.reduce(1,1);
????calculateService.multi(1,1);
????calculateService.division(1,1);
}
運行結(jié)果:
1?+?1?=?2
1?-?1?=?0
1?*?1?=?1
1?/?1?=?1
現(xiàn)在需求變了,我們需要在輸出語句的前后分別打印當(dāng)前系統(tǒng)的時間,如果讓你實現(xiàn),你會怎么做呢?最笨的辦法就是硬編碼,直接在每個方法里添加打印時間的代碼即可:
@Service
public?class?CalculateServiceImpl?implements?CalculateService?{
????@Override
????public?int?add(int?x,?int?y)?{
????????System.out.println("計算前的時間:"?+?LocalDateTime.now());
????????System.out.println(x?+?"?+?"?+?y?+?"?=?"?+?(x?+?y));
????????System.out.println("計算后的時間:"?+?LocalDateTime.now());
????????return?x?+?y;
????}
????@Override
????public?int?reduce(int?x,?int?y)?{
????????System.out.println("計算前的時間:"?+?LocalDateTime.now());
????????System.out.println(x?+?"?-?"?+?y?+?"?=?"?+?(x?-?y));
????????System.out.println("計算后的時間:"?+?LocalDateTime.now());
????????return?x?-?y;
????}
????@Override
????public?int?multi(int?x,?int?y)?{
????????System.out.println("計算前的時間:"?+?LocalDateTime.now());
????????System.out.println(x?+?"?*?"?+?y?+?"?=?"?+?(x?*?y));
????????System.out.println("計算后的時間:"?+?LocalDateTime.now());
????????return?x?*?y;
????}
????@Override
????public?int?division(int?x,?int?y)?{
????????System.out.println("計算前的時間:"?+?LocalDateTime.now());
????????System.out.println(x?+?"?/?"?+?y?+?"?=?"?+?(x?/?y));
????????System.out.println("計算后的時間:"?+?LocalDateTime.now());
????????return?x?/?y;
????}
}
運行結(jié)果:
計算前的時間:2022-01-21T14:35:21.806
1?+?1?=?2
計算后的時間:2022-01-21T14:35:21.806
計算前的時間:2022-01-21T14:35:21.806
1?-?1?=?0
計算后的時間:2022-01-21T14:35:21.806
計算前的時間:2022-01-21T14:35:21.806
1?*?1?=?1
計算后的時間:2022-01-21T14:35:21.806
計算前的時間:2022-01-21T14:35:21.806
1?/?1?=?1
計算后的時間:2022-01-21T14:35:21.806
這樣雖然實現(xiàn)了需求,但是不夠優(yōu)雅,而且如果接口方法有變動,我們就需要修改實現(xiàn)類的代碼,那么有沒有一種辦法能夠?qū)⑦@些打印時間的需求抽離出來,然后讓其在指定的方法執(zhí)行前后分別執(zhí)行呢?SpringAOP就能夠幫助我們完成這一想法。
SpringAOP改造代碼實現(xiàn)
@Aspect
@Component
public?class?CalculateAspectJ?{
????@Before("execution(*?com.wwj.spring.demo.aop.CalculateService.add(..))")
????public?void?printBefore(){
????????System.out.println("計算前的時間:"?+?LocalDateTime.now());
????}
}
這段代碼里面涉及到的知識點比較多,下面我會一一介紹,先來看看效果:
計算前的時間:2022-01-21T14:45:41.579
1?+?1?=?2
1?-?1?=?0
1?*?1?=?1
1?/?1?=?1
看輸出結(jié)果好像打印時間只在add方法生效了,這是為什么呢?我們主要的關(guān)注點就是下面的這個組件:
@Aspect
@Component
public?class?CalculateAspectJ?{
????@Before("execution(int?com.wwj.spring.demo.aop.CalculateService.add(..))")
????public?void?printBefore(){
????????System.out.println("計算前的時間:"?+?LocalDateTime.now());
????}
}
對于傳統(tǒng)的OOP編程,我們的開發(fā)流程是從上至下的,比如轉(zhuǎn)賬操作,我們需要在取款、查詢業(yè)務(wù)、轉(zhuǎn)賬三個操作中驗證用戶的信息是否正確:
而AOP打破了這種限定,它以一種橫向的方式進(jìn)行編程,就像砍樹一樣,如下圖:
可以看到經(jīng)過AOP的改造后,原先要寫三遍的驗證用戶代碼只需要寫一次了,它就像一根針,把代碼織入到了業(yè)務(wù)中。再回過頭來看看剛才的組件:
@Aspect
@Component
public?class?CalculateAspectJ?{
????@Before("execution(int?com.wwj.spring.demo.aop.CalculateService.add(int,int))")
????public?void?printBefore(){
????????System.out.println("計算前的時間:"?+?LocalDateTime.now());
????}
}
其中@Aspect注解用于聲明當(dāng)前類為一個切面,當(dāng)一個類被聲明為切面后,Spring便會將該類切入到某個切點中,而切點就是我們需要改造的方法,那么如何指定切面作用于哪些切點上呢,我們需要借助切點表達(dá)式:
execution(int?com.wwj.spring.demo.aop.CalculateService.add(int,int))
切點表達(dá)式以execution開頭,值為方法的全名,包括返回值、包名、方法名、參數(shù),Spring將根據(jù)切點表達(dá)式去匹配需要切入的方法,不過一般情況下切點表達(dá)式并不會寫得這么精確,通常配合通配符一起使用,如:
execution(*?com.wwj.spring.demo.aop.CalculateService.*(..))
它表示匹配CalculateService接口下任意返回值任意參數(shù)的任意方法,也就是說,該接口下的所有方法都將被處理,當(dāng)我們使用通配符方式配置時,運行結(jié)果如下:
計算前的時間:2022-01-21T16:07:23.250
1?+?1?=?2
計算前的時間:2022-01-21T16:07:23.250
1?-?1?=?0
計算前的時間:2022-01-21T16:07:23.250
1?*?1?=?1
計算前的時間:2022-01-21T16:07:23.250
1?/?1?=?1
通知類型
將代碼邏輯織入到業(yè)務(wù)中的流程還有一個專業(yè)的概念,叫通知,從上面的運行結(jié)果我們不難發(fā)現(xiàn),切面只在方法執(zhí)行之前生效了,這是因為我們使用了@Before注解,它表示的是通知類型中的前置通知,Spring中共有5種通知類型:
@Before:前置通知,在目標(biāo)方法執(zhí)行前執(zhí)行 @After:后置通知,在目標(biāo)方法執(zhí)行后執(zhí)行,無論是否出現(xiàn)異常 @AfterReturning:返回通知,在目標(biāo)方法執(zhí)行后執(zhí)行,出現(xiàn)異常則不執(zhí)行 @AfterThrowing:異常通知,在目標(biāo)方法出現(xiàn)異常后執(zhí)行 @Around:環(huán)繞通知,圍繞方法執(zhí)行,它能實現(xiàn)以上四種通知的效果
由此可知,若是想在目標(biāo)方法執(zhí)行之后實現(xiàn)某些功能,則需要使用后置通知,添加一個配置:
@After("execution(*?com.wwj.spring.demo.aop.CalculateService.*(..))")
public?void?printAfter()?{
????System.out.println("計算前的時間:"?+?LocalDateTime.now());
}
運行結(jié)果:
計算前的時間:2022-01-21T16:14:00.002
1?+?1?=?2
計算后的時間:2022-01-21T16:14:00.002
計算前的時間:2022-01-21T16:14:00.002
1?-?1?=?0
計算后的時間:2022-01-21T16:14:00.002
計算前的時間:2022-01-21T16:14:00.002
1?*?1?=?1
計算后的時間:2022-01-21T16:14:00.002
計算前的時間:2022-01-21T16:14:00.002
1?/?1?=?1
計算后的時間:2022-01-21T16:14:00.002
其它幾種類型的通知用法也是如此,只需改變注解名字即可,不過在每種通知中都有一些其它細(xì)節(jié),下面我們一一介紹。
前置通知
前置通知@Before,它會在目標(biāo)方法執(zhí)行之前執(zhí)行,所以按道理我們可以在前置通知中獲取目標(biāo)方法的一些信息,比如方法名、方法入?yún)⒌?,好在Spring已經(jīng)考慮到了,為我們提供了JoinPoint來獲取,來看例子:
@Before("execution(*?com.wwj.spring.demo.aop.CalculateService.*(..))")
public?void?printBefore(JoinPoint?joinPoint)?{
????String?methodName?=?joinPoint.getSignature().getName();
????List
運行結(jié)果:
執(zhí)行前置通知,方法名:add,方法入?yún)?[1,?1]
1?+?1?=?2
執(zhí)行前置通知,方法名:reduce,方法入?yún)?[1,?1]
1?-?1?=?0
執(zhí)行前置通知,方法名:multi,方法入?yún)?[1,?1]
1?*?1?=?1
執(zhí)行前置通知,方法名:division,方法入?yún)?[1,?1]
1?/?1?=?1
但是在前置通知中是無法獲取到目標(biāo)方法的返回值的,因為此時目標(biāo)方法還未執(zhí)行。
后置通知
后置通知會在目標(biāo)方法執(zhí)行后執(zhí)行,所以也可以獲取到目標(biāo)方法的信息:
@After("execution(*?com.wwj.spring.demo.aop.CalculateService.*(..))")
public?void?printAfter(JoinPoint?joinPoint)?{
????String?methodName?=?joinPoint.getSignature().getName();
????List
運行結(jié)果:
1?+?1?=?2
執(zhí)行后置通知,方法名:add,方法入?yún)?[1,?1]
1?-?1?=?0
執(zhí)行后置通知,方法名:reduce,方法入?yún)?[1,?1]
1?*?1?=?1
執(zhí)行后置通知,方法名:multi,方法入?yún)?[1,?1]
1?/?1?=?1
執(zhí)行后置通知,方法名:division,方法入?yún)?[1,?1]
那么后置通知能否獲取到目標(biāo)方法的返回值呢?其實也是不可以的,因為后置通知無論目標(biāo)方法是否出現(xiàn)異常都會執(zhí)行,所以它也是無法獲取到方法的返回值的。
返回通知
返回通知會在目標(biāo)方法成功執(zhí)行后執(zhí)行,所以它不光能夠獲取到目標(biāo)方法的方法名、方法入?yún)⒌刃畔ⅲ材軌颢@取到方法的返回值:
@AfterReturning(value?=?"execution(*?com.wwj.spring.demo.aop.CalculateService.*(..))"
????????????????,?returning?=?"result")
public?void?printAfterReturning(JoinPoint?joinPoint,?Object?result)?{
????String?methodName?=?joinPoint.getSignature().getName();
????List
在@AfterReturning中配置returning屬性,然后在方法入?yún)⒅卸x一個與其名字相同的變量,Spring將會自動把目標(biāo)方法的返回值注入進(jìn)來,運行結(jié)果如下:
1?+?1?=?2
執(zhí)行返回通知,方法名:add,方法入?yún)?[1,?1],返回值:2
1?-?1?=?0
執(zhí)行返回通知,方法名:reduce,方法入?yún)?[1,?1],返回值:0
1?*?1?=?1
執(zhí)行返回通知,方法名:multi,方法入?yún)?[1,?1],返回值:1
1?/?1?=?1
執(zhí)行返回通知,方法名:division,方法入?yún)?[1,?1],返回值:1
異常通知
異常通知會在目標(biāo)方法出現(xiàn)異常后執(zhí)行,所以異常通知也是無法獲取到目標(biāo)方法的返回值的,但是異常通知可以獲取到目標(biāo)方法出現(xiàn)的異常信息:
@AfterThrowing(value?=?"execution(*?com.wwj.spring.demo.aop.CalculateService.*(..))"
???????????????,?throwing?=?"e")
public?void?printAfterThrowing(JoinPoint?joinPoint,?Exception?e)?{
????String?methodName?=?joinPoint.getSignature().getName();
????List
指定@AfterThrowing注解的throwing屬性,即可得到目標(biāo)方法出現(xiàn)的異常信息,我們故意產(chǎn)生一個異常,讓除法操作的除數(shù)為0,查看運行結(jié)果:
1?+?1?=?2
1?-?1?=?0
1?*?1?=?1
執(zhí)行異常通知,方法名:division,方法入?yún)?[1,?0],異常:java.lang.ArithmeticException:?/?by?zero
環(huán)繞通知
最后是環(huán)繞通知,環(huán)繞通知是圍繞著目標(biāo)方法執(zhí)行的,所以它能夠?qū)崿F(xiàn)前面4個通知的所有功能,如下:
@Around("execution(*?com.wwj.spring.demo.aop.CalculateService.*(..))")
public?Object?printAround(ProceedingJoinPoint?joinPoint)?{
????Object?result?=?null;
????String?methodName?=?joinPoint.getSignature().getName();
????List
運行結(jié)果:
執(zhí)行前置通知,方法名:add,方法入?yún)?[1,?1]
1?+?1?=?2
執(zhí)行返回通知,方法名:add,方法入?yún)?[1,?1],返回值:2
執(zhí)行后置通知,方法名:add,方法入?yún)?[1,?1]
執(zhí)行前置通知,方法名:reduce,方法入?yún)?[1,?1]
1?-?1?=?0
執(zhí)行返回通知,方法名:reduce,方法入?yún)?[1,?1],返回值:0
執(zhí)行后置通知,方法名:reduce,方法入?yún)?[1,?1]
執(zhí)行前置通知,方法名:multi,方法入?yún)?[1,?1]
1?*?1?=?1
執(zhí)行返回通知,方法名:multi,方法入?yún)?[1,?1],返回值:1
執(zhí)行后置通知,方法名:multi,方法入?yún)?[1,?1]
執(zhí)行前置通知,方法名:division,方法入?yún)?[1,?0]
執(zhí)行異常通知,方法名:division,方法入?yún)?[1,?0],異常:java.lang.ArithmeticException:?/?by?zero
執(zhí)行后置通知,方法名:division,方法入?yún)?[1,?0]
異常通知需要注意幾點,首先必須有返回值,其次方法入?yún)镻roceedingJoinPoint而不是JoinPoint,result = joinPoint.proceed();表示執(zhí)行目標(biāo)方法,在目標(biāo)方法執(zhí)行前后分別執(zhí)行對應(yīng)的通知邏輯。
自己實現(xiàn)通知
不知道大家在看到環(huán)繞通知時有沒有發(fā)現(xiàn)它有點像JDK的動態(tài)代理,那能不能借助JDK的動態(tài)代理來自己實現(xiàn)一下通知呢?代碼如下:
public?class?MyInvocationHandler?implements?InvocationHandler?{
????private?Object?target;
????public?MyInvocationHandler(Object?target)?{
????????this.target?=?target;
????}
????@Override
????public?Object?invoke(Object?proxy,?Method?method,?Object[]?args)?throws?Throwable?{
????????Object?result?=?null;
????????try?{
????????????System.out.println("執(zhí)行前置通知,方法名:"?+?method.getName()?+?",方法入?yún)?"?+?Arrays.asList(args));
????????????result?=?method.invoke(target,?args);
????????????System.out.println("執(zhí)行返回通知,方法名:"?+?method.getName()?+?",方法入?yún)?"?+?Arrays.asList(args)?+?",返回值:"?+?result);
????????}?catch?(Throwable?e)?{
????????????System.out.println("執(zhí)行異常通知,方法名:"?+?method.getName()?+?",方法入?yún)?"?+?Arrays.asList(args)?+?",異常:"?+?e);
????????}?finally?{
????????????System.out.println("執(zhí)行后置通知,方法名:"?+?method.getName()?+?",方法入?yún)?"?+?Arrays.asList(args));
????????}
????????return?result;
????}
}
public?static?void?main(String[]?args)?throws?Exception?{
????ApplicationContext?context?=?new?AnnotationConfigApplicationContext(MyConfiguration.class);
????CalculateService?calculateService?=?context.getBean("calculateServiceImpl",?CalculateService.class);
????MyInvocationHandler?myInvocationHandler?=?new?MyInvocationHandler(calculateService);
????calculateService?=?(CalculateService)?Proxy.newProxyInstance(calculateService.getClass().getClassLoader(),?calculateService.getClass().getInterfaces(),?myInvocationHandler);
????calculateService.add(1,?1);
????calculateService.reduce(1,?1);
????calculateService.multi(1,?1);
????calculateService.division(1,?0);
}
運行結(jié)果:
執(zhí)行前置通知,方法名:add,方法入?yún)?[1,?1]
1?+?1?=?2
執(zhí)行返回通知,方法名:add,方法入?yún)?[1,?1],返回值:2
執(zhí)行后置通知,方法名:add,方法入?yún)?[1,?1]
執(zhí)行前置通知,方法名:reduce,方法入?yún)?[1,?1]
1?-?1?=?0
執(zhí)行返回通知,方法名:reduce,方法入?yún)?[1,?1],返回值:0
執(zhí)行后置通知,方法名:reduce,方法入?yún)?[1,?1]
執(zhí)行前置通知,方法名:multi,方法入?yún)?[1,?1]
1?*?1?=?1
執(zhí)行返回通知,方法名:multi,方法入?yún)?[1,?1],返回值:1
執(zhí)行后置通知,方法名:multi,方法入?yún)?[1,?1]
執(zhí)行前置通知,方法名:division,方法入?yún)?[1,?0]
執(zhí)行異常通知,方法名:division,方法入?yún)?[1,?0],異常:java.lang.reflect.InvocationTargetException
執(zhí)行后置通知,方法名:division,方法入?yún)?[1,?0]
借助JDK的動態(tài)代理,我們也能夠?qū)崿F(xiàn)通知,事實上,SpringAOP底層的實現(xiàn)就是JDK的動態(tài)代理,不過動態(tài)代理有局限性,就是目標(biāo)方法所在的類必須實現(xiàn)了接口。
為此,SpringAOP還引入了另外一種動態(tài)代理方式:CgLib,CgLib是通過繼承的方式實現(xiàn)的代理,所以它能夠適應(yīng)任何場景。
? ? ?
往 期 推 薦
1、Windows新功能太“社死”!教你一鍵快速禁用 2、發(fā)現(xiàn)競爭對手代碼中的低級Bug后,我被公司解雇并送上了法庭 3、為什么說技術(shù)人一定要有產(chǎn)品思維 4、操作系統(tǒng)聯(lián)合創(chuàng)始人反目成仇,這個Linux發(fā)行版危在旦夕 5、Java8八年不倒、IntelliJ IDEA力壓Eclipse 點分享
點收藏
點點贊
點在看





