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

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





