前言
之前的源碼解析章節(jié)揣炕,本人講解了Spring IOC 的核心部分的源碼稠腊。如果你熟悉Spring AOP的使用的話,在了解Spring IOC的核心源碼之后架忌,學(xué)習(xí)Spring AOP 的源碼,應(yīng)該可以說是水到渠成叹放,不會(huì)有什么困難挠羔。
但是直接開始講Spring AOP的源碼,本人又覺得有點(diǎn)突兀褥赊,所以便有了這一章。Spring AOP 的入門使用介紹:包括Spring AOP的一些概念性介紹和配置使用方法拌喉。
這里先貼一下思維導(dǎo)圖俐银。
AOP 是什么
AOP : 面向切面編程(Aspect Oriented Programming)
Aspect是一種新的模塊化機(jī)制,用來描述分散在對象田藐、類或函數(shù)中的橫切關(guān)注點(diǎn)(crosscutting concern)。從關(guān)注點(diǎn)中分離出橫切關(guān)注點(diǎn)是面向切面的程序設(shè)計(jì)的核心概念汽久。分離關(guān)注點(diǎn)使解決特定領(lǐng)域問題的代碼從業(yè)務(wù)邏輯中獨(dú)立出來,業(yè)務(wù)邏輯的代碼中不再含有針對特定領(lǐng)域問題代碼的調(diào)用景醇,業(yè)務(wù)邏輯同特定領(lǐng)域問題的關(guān)系通過切面來封裝、維護(hù)吧寺,這樣原本分散在整個(gè)應(yīng)用程序中的變動(dòng)就可以很好地管理起來。
最近在看李智慧的《大型網(wǎng)站技術(shù)架構(gòu)》一書中散劫,作者提到,開發(fā)低耦合系統(tǒng)是軟件設(shè)計(jì)的終極目標(biāo)之一赖条。AOP這種面向切面編程的的方式就體現(xiàn)了這樣的理念。將一些重復(fù)的纬乍、和業(yè)務(wù)主邏輯不相關(guān)的功能性代碼(日志記錄、安全管理等)通過切面模塊化地抽離出來進(jìn)行封裝蕾额,實(shí)現(xiàn)關(guān)注點(diǎn)分離彼城、模塊解耦,使得整個(gè)系統(tǒng)更易于維護(hù)管理募壕。
這樣分而治之的設(shè)計(jì),讓我感覺到了一種美感舱馅。
AOP 要實(shí)現(xiàn)的是在我們原來寫的代碼的基礎(chǔ)上,進(jìn)行一定的包裝代嗤,如在方法執(zhí)行前、方法返回后干毅、方法拋出異常后等地方進(jìn)行一定的攔截處理或者叫增強(qiáng)處理。
AOP 的實(shí)現(xiàn)并不是因?yàn)?Java 提供了什么神奇的鉤子硝逢,可以把方法的幾個(gè)生命周期告訴我們绅喉,而是我們要實(shí)現(xiàn)一個(gè)代理叫乌,實(shí)際運(yùn)行的實(shí)例其實(shí)是生成的代理類的實(shí)例。
名詞概念
前面提到過革屠,Spring AOP 延用了 AspectJ 中的概念,使用了 AspectJ 提供的 jar 包中的注解屠阻。也就是Spring AOP里面的概念和術(shù)語,并不是Spring獨(dú)有的国觉,而是和AOP相關(guān)的虾啦。
概念可以草草看過,在看了之后的章節(jié)之后再回來看會(huì)對概念理解的更深傲醉。
術(shù)語 | 概念 |
---|---|
Aspect |
切面是Pointcut 和Advice 的集合,一般單獨(dú)作為一個(gè)類硬毕。Pointcut 和Advice 共同定義了關(guān)于切面的全部內(nèi)容,它是什么時(shí)候吐咳,在何時(shí)和何處完成功能。 |
Joinpoint |
這表示你的應(yīng)用程序中可以插入AOP方面的一點(diǎn)童谒。也可以說,這是應(yīng)用程序中使用Spring AOP框架采取操作的實(shí)際位置饥伊。 |
Advice |
這是在方法執(zhí)行之前或之后采取的實(shí)際操作。 這是在Spring AOP框架的程序執(zhí)行期間調(diào)用的實(shí)際代碼片段蔫饰。 |
Pointcut |
這是一組一個(gè)或多個(gè)切入點(diǎn),在切點(diǎn)應(yīng)該執(zhí)行Advice 篓吁。 您可以使用表達(dá)式或模式指定切入點(diǎn),后面示例會(huì)提到越除。 |
Introduction |
引用允許我們向現(xiàn)有的類添加新的方法或者屬性 |
Weaving |
創(chuàng)建一個(gè)被增強(qiáng)對象的過程。這可以在編譯時(shí)完成(例如使用AspectJ編譯器)翼雀,也可以在運(yùn)行時(shí)完成。Spring和其他純Java AOP框架一樣狼渊,在運(yùn)行時(shí)完成織入类垦。 |
PS:在整理概念的時(shí)候有個(gè)疑問,為什么網(wǎng)上這么多中文文章把a(bǔ)dvice 翻譯成“通知”呢蚤认??砰琢?概念上說得通嗎?陪汽??我更愿意翻譯成“增強(qiáng)”(并發(fā)中文網(wǎng)ifeve.com 也是翻譯成增強(qiáng))
還有一些注解挚冤,表示Advice的類型,或者說增強(qiáng)的時(shí)機(jī)澳骤,看過之后的示例之后會(huì)更加的清楚。
術(shù)語 | 概念 |
---|---|
Before |
在方法被調(diào)用之前執(zhí)行增強(qiáng) |
After |
在方法被調(diào)用之后執(zhí)行增強(qiáng) |
After-returning |
在方法成功執(zhí)行之后執(zhí)行增強(qiáng) |
After-throwing |
在方法拋出指定異常后執(zhí)行增強(qiáng) |
Around |
在方法調(diào)用的前后執(zhí)行自定義的增強(qiáng)行為(最靈活的方式) |
使用方式
Spring 2.0 之后宴凉,Spring AOP有了兩種配置方式表悬。
schema-based:Spring 2.0 以后使用 XML 的方式來配置,使用 命名空間
<aop />
@AspectJ 配置:Spring 2.0 以后提供的注解方式蟆沫。這里雖然叫做 @AspectJ,但是這個(gè)和 AspectJ 其實(shí)沒啥關(guān)系饭庞。
PS:個(gè)人比較鐘情于@AspectJ 這種方式,使用下來是最方面的绸狐。也可能是因?yàn)槲矣X得XML方式配置的Spring Bean很不簡潔卤恳、寫起來不好看吧寒矿,所以有點(diǎn)排斥吧。23333~
本文主要針對注解方式講解拆融,并且給出對應(yīng)的DEMO;之后的源碼解析也會(huì)以注解的這種方式為范例講解Spring AOP的源碼(整個(gè)源碼解析看完镜豹,會(huì)對其他方式觸類旁通,因?yàn)樵矶际且粯拥模?/p>
如果對其他配置方式感興趣的同學(xué)可以google其他的學(xué)習(xí)資料趟脂。
來一條分割線搞旭,正式開始
1. 開啟@AspectJ
注解配置方式
開啟@AspectJ
的注解配置方式,有兩種方式
-
在XML中配置:
<aop:aspectj-autoproxy/>
-
使用
@EnableAspectJAutoProxy
注解@Configuration @EnableAspectJAutoProxy public class Config { }
開啟了上述配置之后肄渗,所有在容器中,被@AspectJ
注解的 bean 都會(huì)被 Spring 當(dāng)做是 AOP 配置類翎嫡,稱為一個(gè) Aspect。
NOTE:這里有個(gè)要注意的地方惑申,@AspectJ 注解只能作用于Spring Bean 上面,所以你用 @Aspect 修飾的類要么是用 @Component注解修飾圈驼,要么是在 XML中配置過的。
比如下面的寫法绩脆,
// 有效的AOP配置類
@Aspect
@Component
public class MyAspect {
//....
}
// 如果沒有在XML配置過,那這個(gè)就是無效的AOP配置類
@Aspect
public class MyAspect {
//....
}
2. 配置 Pointcut (增強(qiáng)的切入點(diǎn))
Pointcut 在大部分地方被翻譯成切點(diǎn)惕味,用于定義哪些方法需要被增強(qiáng)或者說需要被攔截。
在Spring 中名挥,我們可以認(rèn)為 Pointcut 是用來匹配Spring 容器中所有滿足指定條件的bean的方法主守。
比如下面的寫法榄融,
// 指定的方法
@Pointcut("execution(* testExecution(..))")
public void anyTestMethod() {}
下面完整列舉一下 Pointcut 的匹配方式:
-
execution:匹配方法簽名
這個(gè)最簡單的方式就是上面的例子蹋艺,
"execution(* testExecution(..))"
表示的是匹配名為testExecution
的方法,*
代表任意返回值捎谨,(..)
表示零個(gè)或多個(gè)任意參數(shù)涛救。
-
within:指定所在類或所在包下面的方法(Spring AOP 獨(dú)有)
// service 層 // ".." 代表包及其子包 @Pointcut("within(ric.study.demo.aop.svc..*)") public void inSvcLayer() {}
-
@annotation:方法上具有特定的注解
// 指定注解 @Pointcut("@annotation(ric.study.demo.aop.HaveAop)") public void withAnnotation() {}
-
bean(idOrNameOfBean):匹配 bean 的名字(Spring AOP 獨(dú)有)
// controller 層 @Pointcut("bean(testController)") public void inControllerLayer() {}
上述是日常使用中常見的幾種配置方式
有更細(xì)的匹配需求的,可以參考這篇文章:https://www.baeldung.com/spring-aop-pointcut-tutorial
關(guān)于 Pointcut 的配置检吆,Spring 官方有這么一段建議:
When working with enterprise applications, you often want to refer to modules of the application and particular sets of operations from within several aspects. We recommend defining a "SystemArchitecture" aspect that captures common pointcut expressions for this purpose. A typical such aspect would look as follows:
意思就是程储,如果你是在開發(fā)企業(yè)級應(yīng)用,Spring 建議你使用 SystemArchitecture
這種切面配置方式章鲤,即將一些公共的PointCut 配置全部寫在這個(gè)一個(gè)類里面維護(hù)。官網(wǎng)文檔給的例子像下面這樣(它文中使用 XML 配置的败徊,所以沒加@Component注解)
package com.xyz.someapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.someapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.web..*)")
public void inWebLayer() {}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.someapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.service..*)")
public void inServiceLayer() {}
/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.someapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.dao..*)")
public void inDataAccessLayer() {}
/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.someapp.abc.service and com.xyz.def.service) then
* the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
* could be used instead.
*/
@Pointcut("execution(* com.xyz.someapp.service.*.*(..))")
public void businessService() {}
/**
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
public void dataAccessOperation() {}
}
上面這個(gè) SystemArchitecture 很好理解皱蹦,該 Aspect 定義了一堆的 Pointcut,隨后在任何需要 Pointcut 的地方都可以直接引用沪哺。
配置切點(diǎn),代表著我們想讓程序攔截哪一些方法辜妓,但程序需要怎么對攔截的方法進(jìn)行增強(qiáng),就是后面要介紹的配置 Advice嫌拣。
3. 配置Advice
注意,實(shí)際開發(fā)過程當(dāng)中异逐,Aspect 類應(yīng)該遵守單一職責(zé)原則,不要把所有的Advice配置全部寫在一個(gè)Aspect類里面腥例。
這里是為了演示方便辅甥,所以寫在了一起燎竖。
先直接上示例代碼,里面包含了Advice 的幾種配置方式(上文名詞概念小節(jié)中有提到)夏块。
/**
* 注:實(shí)際開發(fā)過程當(dāng)中,Advice應(yīng)遵循單一職責(zé)脐供,不應(yīng)混在一起
*
* @author Richard_yyf
* @version 1.0 2019/10/28
*/
@Aspect
@Component
public class GlobalAopAdvice {
@Before("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ... 實(shí)現(xiàn)代碼
}
// 實(shí)際使用過程當(dāng)中 可以像這樣把Advice 和 Pointcut 合在一起,直接在Advice上面定義切入點(diǎn)
@Before("execution(* ric.study.demo.dao.*.*(..))")
public void doAccessCheck() {
// ... 實(shí)現(xiàn)代碼
}
// 在方法
@AfterReturning("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ... 實(shí)現(xiàn)代碼
}
// returnVal 就是相應(yīng)方法的返回值
@AfterReturning(
pointcut="ric.study.demo.aop.SystemArchitecture.dataAccessOperation()",
returning="returnVal")
public void doAccessCheck(Object returnVal) {
// ... 實(shí)現(xiàn)代碼
}
// 異常返回的時(shí)候
@AfterThrowing("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ... 實(shí)現(xiàn)代碼
}
// 注意理解它和 @AfterReturning 之間的區(qū)別政己,這里會(huì)攔截正常返回和異常的情況
@After("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// 通常就像 finally 塊一樣使用掏愁,用來釋放資源。
// 無論正常返回還是異常退出果港,都會(huì)被攔截到
}
// 這種最靈活,既能做 @Before 的事情赦肃,也可以做 @AfterReturning 的事情
@Around("ric.study.demo.aop.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// target 方法執(zhí)行前... 實(shí)現(xiàn)代碼
Object retVal = pjp.proceed();
// target 方法執(zhí)行后... 實(shí)現(xiàn)代碼
return retVal;
}
}
在某些場景下,我們想在@Before的時(shí)候他宛,去獲取方法的入?yún)ⅲ热邕M(jìn)行一些日志的記錄厅各,我們可以通過 org.aspectj.lang.JoinPoint
來實(shí)現(xiàn)。上文中的ProceedingJoinPoint
就是其子類队塘。
@Before("...")
public void logArgs(JoinPoint joinPoint) {
System.out.println("方法執(zhí)行前宜鸯,打印入?yún)ⅲ? + Arrays.toString(joinPoint.getArgs()));
}
再舉個(gè)與之對應(yīng)的,方法返參打恿苄洹:
@AfterReturning( pointcut="...", returning="returnVal")
public void logReturnVal(Object returnVal) {
System.out.println("方法執(zhí)行后,打印返參:" + returnVal));
}
快速Demo
介紹完上述的配置過程之后,我們用一個(gè)快速的Demo來實(shí)際演示一遍陌凳。這里把順序變一下;
1. 編寫 目標(biāo)類
package ric.study.demo.aop.svc;
public interface TestSvc {
void process();
}
@Service("testSvc")
public class TestSvcImpl implements TestSvc {
@Override
public void process() {
System.out.println("test svc is working");
}
}
public interface DateSvc {
void printDate(Date date);
}
@Service("dateSvc")
public class DateSvcImpl implements DateSvc {
@Override
public void printDate(Date date) {
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
}
}
2. 配置 Pointcut
@Aspect
@Component
public class PointCutConfig {
@Pointcut("within(ric.study.demo.aop.svc..*)")
public void inSvcLayer() {}
}
3. 配置Advice
/**
* @author Richard_yyf
* @version 1.0 2019/10/29
*/
@Component
@Aspect
public class ServiceLogAspect {
// 攔截合敦,打印日志验游,并且通過JoinPoint 獲取方法參數(shù)
@Before("ric.study.demo.aop.PointCutConfig.inSvcLayer()")
public void logBeforeSvc(JoinPoint joinPoint) {
System.out.println("在service 方法執(zhí)行前 打印第 1 次日志");
System.out.println("攔截的service 方法的方法簽名: " + joinPoint.getSignature());
System.out.println("攔截的service 方法的方法入?yún)? " + Arrays.toString(joinPoint.getArgs()));
}
// 這里是Advice和Pointcut 合在一起配置的方式
@Before("within(ric.study.demo.aop.svc..*)")
public void logBeforeSvc2() {
System.out.println("在service的方法執(zhí)行前 打印第 2 次日志");
}
}
4. 開啟@AspectJ
注解配置方式,并啟動(dòng)
這里為了圖方便批狱,把配置類和啟動(dòng)類寫在了一起,
/**
* @author Richard_yyf
* @version 1.0 2019/10/28
*/
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("ric.study.demo.aop")
public class Boostrap {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Boostrap.class);
TestSvc svc = (TestSvc) context.getBean("testSvc");
svc.process();
System.out.println("==================");
DateSvc dateSvc = (DateSvc) context.getBean("dateSvc");
dateSvc.printDate(new Date());
}
}
5. 輸出
在service 方法執(zhí)行前 打印第 1 次日志
攔截的service 方法的方法簽名: void ric.study.demo.aop.svc.TestSvcImpl.process()
攔截的service 方法的方法入?yún)? []
在service的方法執(zhí)行前 打印第 2 次日志
test svc is working
==================
在service 方法執(zhí)行前 打印第 1 次日志
攔截的service 方法的方法簽名: void ric.study.demo.aop.svc.DateSvcImpl.printDate(Date)
攔截的service 方法的方法入?yún)? [Mon Nov 04 18:11:34 CST 2019]
在service的方法執(zhí)行前 打印第 2 次日志
2019-11-04 18:11:34
JDK 動(dòng)態(tài)代理和 Cglib
前面有提到過,Spring AOP在目標(biāo)類有實(shí)現(xiàn)接口的時(shí)候盐肃,會(huì)使用JDK 動(dòng)態(tài)代理來生成代理類,我們結(jié)合上面的DEMO看看砸王,
如果我們想不管是否有實(shí)現(xiàn)接口,都是強(qiáng)制使用Cglib的方式來實(shí)現(xiàn)怎么辦谦铃?
Spring 提供給了我們對應(yīng)的配置方式,也就是proxy-target-class
.
注解方式:
//@EnableAspectJAutoProxy(proxyTargetClass = true) // 這樣子就是默認(rèn)使用CGLIB
XML方式:
<aop:config proxy-target-class="true">
改了之后驹闰,
小結(jié)
本文詳細(xì)介紹了Spring AOP的起源、名詞概念以及基于注解的使用方式嘹朗。
本文按照作者的寫作習(xí)慣,是源碼解析章節(jié)的前置學(xué)習(xí)章節(jié)屹培。在下一章中,我們會(huì)以注解方式為入口褪秀,介紹Spring AOP 的源碼設(shè)計(jì),解讀相關(guān)核心源碼(整個(gè)源碼解析看完媒吗,會(huì)對其他方式觸類旁通,因?yàn)樵矶际且粯拥模?/p>
感興趣的可以翻到【前言】部分蝴猪,再看一下思維導(dǎo)圖膊爪。
如果本文有幫助到你嚎莉,希望能點(diǎn)個(gè)贊,這是對我的最大動(dòng)力趋箩。
本文由博客一文多發(fā)平臺 OpenWrite 發(fā)布!