做文學(xué)網(wǎng)站用什么域名口碑營銷怎么做
前言(關(guān)于源碼航行)
在準(zhǔn)備面試和學(xué)習(xí)的過程中,我閱讀了還算多的源碼,比如 JUC、Spring、MyBatis,收獲了很多代碼的設(shè)計(jì)思想,也對平時(shí)調(diào)用的 API 有了更深入的理解;但過多散亂的筆記給我的整理復(fù)習(xí)帶來了比較大的麻煩。
📋 在 C 站零零散散發(fā)了 JUC 的源碼解析和集合源碼解析,收到了很多朋友的喜愛,這里我準(zhǔn)備將一些源碼解析的文章整合起來,為了方便閱讀和歸納在這里整合成目錄:源碼航行閱讀目錄,大家感興趣的話可以關(guān)注一下!
————————————————
第一篇:基礎(chǔ)知識(shí)介紹
這一部分我們來談一下關(guān)于 Spring AOP 的浮在表面上的知識(shí),比如什么是 AOP、它有什么好處、如何使用等等
為什么需要 AOP?
AOP(Aspect Oriented Programming),意為:面向切面編程,通過預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。AOP是OOP的延續(xù),是軟件開發(fā)中的一個(gè)熱點(diǎn),也是 Spring 框架中的一個(gè)重要內(nèi)容,是函數(shù)式編程的一種衍生范型。
利用AOP可以對業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開發(fā)的效率。
舉一個(gè)簡單的例子,如果我們想要在每次調(diào)用一個(gè)類中的方法之前輸出一個(gè)日志,那其實(shí)是挺容易實(shí)現(xiàn)的,畫出流程圖就是這個(gè)樣子:
那如果我們要在這些方法之前加一個(gè)鑒權(quán)呢?同樣,按照前面的方式,無非就是多 copy 一次嘛
就這樣,通過多重 copy 的方式,最終完成了這個(gè)業(yè)務(wù),而這也就代表著,每次擴(kuò)展新的方法,你都要去進(jìn)行 n 次復(fù)制粘貼;每次要更改鑒權(quán)或者日志的邏輯,你都要去進(jìn)行 n 次復(fù)制粘貼;每次。。。。。。
聽起來都讓人非常頭大,但如果我們將邏輯改成這樣呢?
我們將這兩個(gè)方法封裝成一個(gè)公共方法,每次在方法之前去調(diào)用這個(gè)公共的方法,不就實(shí)現(xiàn)了可維護(hù)性嗎,這其實(shí)就有一點(diǎn)切面的感覺了。
這樣,我們雖然每次都不用去復(fù)制代碼了,但還是需要在我們需要的位置去調(diào)用這個(gè)接口,而調(diào)用這個(gè)代碼的位置其實(shí)就是 切面。我們用具體的代碼來看一下,下面實(shí)現(xiàn)了一個(gè)統(tǒng)一的接口 BeforeAdvice,然后在每個(gè)方法之前去調(diào)用它。
public interface BeforeAdvice {void before();
}public class LoggingBeforeAdvice implements BeforeAdvice {@Overridepublic void before() {System.out.println("方法調(diào)用之前的日志");}
}public class MyService {private BeforeAdvice beforeAdvice = new LoggingBeforeAdvice();public void myMethodA() {beforeAdvice.before();// 業(yè)務(wù)邏輯System.out.println("執(zhí)行 myMethod1A");}public void myMethodB() {beforeAdvice.before();// 業(yè)務(wù)邏輯System.out.println("執(zhí)行 myMethodB");}
}
到這里,我們就自己實(shí)現(xiàn)了一個(gè)簡單的 AOP;但每次都這樣去創(chuàng)建一個(gè)這樣的類,其實(shí)也是非常復(fù)雜的,這時(shí)候我們就可以借助 Spring AOP 的力量,它幫我們實(shí)現(xiàn)了簡單、直觀、極其易于配置的切面編程方式。
AOP 概念辨析
在正式講解如何使用 Spring AOP 之前,我們來講點(diǎn)無聊的內(nèi)容,關(guān)于 AOP 的概念辨析;這部分內(nèi)容是一些術(shù)語,但是如果能理解它們,就會(huì)對 AOP 整個(gè)流程有較為清晰的把握。
連接點(diǎn)(Join Point):連接點(diǎn)是程序執(zhí)行中的一個(gè)點(diǎn),這個(gè)點(diǎn)可以是方法調(diào)用、方法執(zhí)行、異常拋出等。在 Spring AOP 中,連接點(diǎn)主要是指方法的調(diào)用或執(zhí)行。連接點(diǎn)是通知的實(shí)際應(yīng)用點(diǎn)。
切點(diǎn)(PointCut):由于連接點(diǎn)可能很多(比如一個(gè)類中的所有方法),想要把所有連接點(diǎn)羅列出現(xiàn)顯然有些困難;切點(diǎn)則定義了在應(yīng)用通知的連接點(diǎn)的集合。切點(diǎn)通過切點(diǎn)表達(dá)式(例如:execution(* com.example.service.*.*(..))
)來指定匹配的方法和類。切點(diǎn)表達(dá)式用于篩選連接點(diǎn),使得通知只在特定的連接點(diǎn)上執(zhí)行。
通知(Advice):通知是在切點(diǎn)處執(zhí)行的代碼。通知定義了具體的橫切邏輯,決定了在方法執(zhí)行的什么階段(之前、之后、環(huán)繞等)插入橫切邏輯。通知有五種類型,我們會(huì)在下一部分進(jìn)行詳細(xì)的了解;通知就是在 何時(shí) 執(zhí)行 怎樣 的邏輯。
切面(Aspect):切面是 AOP 的核心模塊,它封裝了跨越多個(gè)類的關(guān)注點(diǎn),例如日志記錄、事務(wù)管理或安全控制。切面通過通知(Advice)和切點(diǎn)(Pointcut)來定義在何時(shí)、何地應(yīng)用這些關(guān)注點(diǎn);可以將切面看作是切點(diǎn)(Pointcut)和通知(Advice)的組合。切面定義了在何處(切點(diǎn))以及何時(shí)(通知)應(yīng)用橫切邏輯。
五種通知類型
在 Spring AOP 中,通知(Advice)是指在程序執(zhí)行過程中插入的代碼,它定義了在何時(shí)以及在什么情況下進(jìn)行切面的操作。通知是切面中的實(shí)際動(dòng)作部分,是橫切關(guān)注點(diǎn)的具體實(shí)現(xiàn);直觀來說就是要插入的那一組方法。
除了上面提到的在執(zhí)行方法之前執(zhí)行的 Before Advice,還有其他四種類型的通知,也就是說 Spring AOP 為我們提供了五個(gè)插入代碼的位置選擇。
前置通知(Before Advice)
在目標(biāo)方法執(zhí)行之前執(zhí)行的通知??梢杂脕韴?zhí)行日志記錄、安全檢查等。
@Aspect
@Component
public class LoggingAspect {@Before("execution(* com.example.service.*.*(..))")public void beforeAdvice() {System.out.println("前置通知:方法調(diào)用之前執(zhí)行");}
}
后置通知(After Advice)
在目標(biāo)方法執(zhí)行之后執(zhí)行的通知,無論方法是成功返回還是拋出異常。常用于清理資源等。
@Aspect
@Component
public class LoggingAspect {@After("execution(* com.example.service.*.*(..))")public void afterAdvice() {System.out.println("后置通知:方法調(diào)用之后執(zhí)行");}
}
返回后通知(After Returning Advice)
在目標(biāo)方法成功返回結(jié)果之后執(zhí)行的通知。可以用來記錄返回值或?qū)Ψ祷刂颠M(jìn)行處理。
@Aspect
@Component
public class LoggingAspect {@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")public void afterReturningAdvice(Object result) {System.out.println("返回后通知:方法返回值為 " + result);}
}
拋出異常后通知(After Throwing Advice)
在目標(biāo)方法拋出異常后執(zhí)行的通知。可以用來記錄異常信息或執(zhí)行異常處理邏輯。
@Aspect
@Component
public class LoggingAspect {@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "exception")public void afterThrowingAdvice(Exception exception) {System.out.println("拋出異常后通知:異常為 " + exception.getMessage());}
}
環(huán)繞通知(Around Advice)
環(huán)繞通知在目標(biāo)方法執(zhí)行的前后都執(zhí)行,可以完全控制目標(biāo)方法的執(zhí)行,包括決定是否執(zhí)行目標(biāo)方法,以及在目標(biāo)方法執(zhí)行前后添加自定義邏輯。環(huán)繞通知最為強(qiáng)大和靈活。
@Aspect
@Component
public class LoggingAspect {@Around("execution(* com.example.service.*.*(..))")public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println("環(huán)繞通知:方法調(diào)用之前");Object result = joinPoint.proceed(); // 執(zhí)行目標(biāo)方法System.out.println("環(huán)繞通知:方法調(diào)用之后");return result;}
}
這五種通知方式的執(zhí)行順序是這樣的:
- 前置通知(Before Advice)
- **環(huán)繞通知(Around Advice)**的前半部分
- 目標(biāo)方法執(zhí)行
- **環(huán)繞通知(Around Advice)**的后半部分
- 返回后通知(After Returning Advice)(如果目標(biāo)方法成功返回)
- 拋出異常后通知(After Throwing Advice)(如果目標(biāo)方法拋出異常)
- 后置通知(After Advice)
切點(diǎn)表達(dá)式
前面提到過,切面直觀來講就是插入方法的位置;在前面五種通知類型中,我們已經(jīng)看到了如何通過注解選擇方法的執(zhí)行位置,但是諸如
* com.example.service.*.*(..))
這樣,定位方法位置的格式其實(shí)是沒有提及的,這一部分重點(diǎn)來講一下如何配置方法的位置。
切點(diǎn)則表示一組 joint point,這些 joint point 或是通過邏輯關(guān)系組合起來,或是通過通配、正則表達(dá)式等方式集中起來,它定義了相應(yīng)的 Advice 將要發(fā)生的地方。
切點(diǎn)表達(dá)式由以下幾部分組成:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
其中,各部分的含義如下,注意上面的問號(hào)(?)表示可選
- execution:指定切點(diǎn)類型為方法執(zhí)行。
- modifiers-pattern:可選,方法的訪問修飾符,如
public
、protected
、private
。通常省略不寫,表示任意訪問修飾符。 - ret-type-pattern:方法的返回類型模式,例如
void
、String
、``(任意返回類型)。 - declaring-type-pattern:可選,方法所在類的全限定名稱模式,如
com.example.service.*
。指定要匹配的類或包。 - name-pattern:方法名稱模式,如
Service
、get*
。支持通配符 ``(匹配任意字符序列)和..
(匹配任意數(shù)量和類型的參數(shù))。 - param-pattern:方法參數(shù)模式,如
()
(無參數(shù))、(*)
(一個(gè)任意類型的參數(shù))、(..)
(任意數(shù)量和類型的參數(shù))。 - throws-pattern:可選,方法可能拋出的異常模式。
下面來做幾個(gè)小練習(xí)
1)指定 com.example.service
包下的所有類的所有方法:
@Pointcut("execution(* com.example.service.*.*(..))")
private void serviceMethods() {}
2)com.example.service
包下所有類的方法中第一個(gè)參數(shù)為 String
類型的方法
@Pointcut("execution(* com.example.service.*.*(String, ..))")
private void stringParamMethods() {}
通過上面的練習(xí),相信大家對切點(diǎn)表達(dá)式的書寫方式有了一定的掌握,下面我們來看看,除了 execution
方法執(zhí)行切點(diǎn),Spring 還為我們提供了哪些指定切點(diǎn)的方式
within:限定匹配特定類型的連接點(diǎn)。within(com.example.service.*)
匹配 com.example.service
包及其子包中所有方法。
this:限定匹配特定類型的 bean。this(com.example.service.MyService)
匹配實(shí)現(xiàn) com.example.service.MyService
接口的 bean。
target:限定匹配特定類型的目標(biāo)對象。target(com.example.service.MyService)
匹配目標(biāo)對象是 com.example.service.MyService
的連接點(diǎn)。
args:限定匹配特定參數(shù)類型的方法。args(String, ..)
匹配第一個(gè)參數(shù)是 String
類型的方法。
@annotation:限定匹配特定注解的方法。@annotation(org.springframework.transaction.annotation.Transactional)
匹配標(biāo)注有 @Transactional
注解的方法。
因?yàn)?within、this 和 target,都可以通過 execution 作為一定程度上的替代,所以這里我們重點(diǎn)關(guān)注一下匹配特定注解的方式,即 @annotation
即可。
但同時(shí),這些表達(dá)式其實(shí)是可以共用的,比如通過這樣的方式:
@Before("execution(* com.example.service.UserService.getUserById(int)) && args(userId)")public void logBeforeGetUserById(JoinPoint joinPoint, int userId) {System.out.println("Before calling getUserById with userId: " + userId);}
上面的 userId
參數(shù)是通過切點(diǎn)表達(dá)式中的 args(userId)
指定的,所以在方法體內(nèi)可以直接使用 userId
參數(shù)來獲取方法執(zhí)行時(shí)的具體值;但如果僅僅使用 joinPoint
的話就需要 getArgs()
再拿取參數(shù)了;這里只涉及寫法上的偏好,我們平時(shí)使用的大部分的內(nèi)容通過 execution 和 @annotation 都是可以實(shí)現(xiàn)的。
正式使用
通過前面的介紹,我們已經(jīng)辨析了 AOP 的基本概念,了解了控制何時(shí)執(zhí)行邏輯的通知類型(Advice),定義在什么位置執(zhí)行的切點(diǎn)表達(dá)式(PointCut),下面我們正式來嘗試使用 AOP 來解決一些現(xiàn)實(shí)的問題。
就舉一個(gè)前面提到的日志記錄的 AOP 吧
定義一個(gè)簡單的服務(wù)類
@Service
public class UserService {public String getUserById(int userId) {// 模擬方法體return "User: " + userId;}
}
創(chuàng)建日志記錄的切面類
@Aspect
@Component
public class LoggingAspect {@Before("execution(* com.example.service.UserService.getUserById(int))")public void logBeforeGetUserById(JoinPoint joinPoint) {// 獲取方法參數(shù)Object[] args = joinPoint.getArgs();System.out.println("Before calling getUserById with userId: " + args[0]);}@AfterReturning(pointcut = "execution(* com.example.service.UserService.getUserById(int))", returning = "result")public void logAfterReturningGetUserById(JoinPoint joinPoint, Object result) {System.out.println("After returning from getUserById with result: " + result);}
}
然后我們?nèi)プ鲆粋€(gè)簡單的測試,可以看到如下的輸出:
Before calling getUserById with userId: 123
After returning from getUserById with result: User: 123
User retrieved: User: 123
關(guān)于 JoinPoint 和 ProceedingJoinPoint
在 Spring AOP 中,
JoinPoint
和ProceedingJoinPoint
是兩個(gè)重要的接口,用于在切面中獲取方法執(zhí)行時(shí)的信息和控制方法執(zhí)行;我們在書寫切面邏輯的時(shí)候,需要的大部分參數(shù)或者方法信息等,都是從這里面獲取的。
JoinPoint 接口
JoinPoint
接口是 Spring AOP 提供的一個(gè)核心接口,用于描述正在執(zhí)行的連接點(diǎn)(join point),它可以用來獲取方法的簽名、參數(shù)等信息,但是不能直接控制方法的執(zhí)行流程。
常用方法
Signature getSignature(); // 獲取代表被通知方法簽名的對象,可以進(jìn)一步獲取方法名、聲明類型等信息。Object[] getArgs(); // 獲取被通知方法的參數(shù)對象數(shù)組。Object getTarget(); // 獲取目標(biāo)對象,即被通知的目標(biāo)類實(shí)例。Object getThis(); // 獲取代理對象的引用,即代理對象本身。Object[] getArgs(); // 獲取調(diào)用方法時(shí)傳遞的參數(shù)
其中有個(gè)特殊一點(diǎn)的是 Signature,方法簽名接口:
public interface Signature {String toString(); // 返回方法的字符串表示形式。String toShortString(); // 返回方法的簡短字符串表示形式。String toLongString(); // 返回方法的長字符串表示形式。String getName();// 獲取方法名。 int getModifiers(); // 獲取方法的修飾符,返回一個(gè)整數(shù),具體取值需要通過 java.lang.reflect.Modifier 類來解析。Class getDeclaringType(); // 獲取聲明該方法的類的 Class 對象。String getDeclaringTypeName(); // 獲取聲明該方法的類的全限定名。
}
大家可以自己寫個(gè)方法測試一下,這里就不過多贅述了。
ProceedingJoinPoint 接口
ProceedingJoinPoint
接口繼承自 JoinPoint
接口,它擴(kuò)展了 JoinPoint
接口,提供了控制方法執(zhí)行流程的能力。通常在 Around Advice 中使用 ProceedingJoinPoint
來調(diào)用目標(biāo)方法,并可以控制是否繼續(xù)執(zhí)行該方法,以及在執(zhí)行前后進(jìn)行額外的處理。
常用方法
Object proceed() throws Throwable; // 繼續(xù)執(zhí)行連接點(diǎn)(即目標(biāo)方法),返回方法的返回值。Object proceed(Object[] args) throws Throwable; // 按照給定的參數(shù)繼續(xù)執(zhí)行連接點(diǎn)。
由于是繼承,所以 JoinPoint 提供的方法,也都可以使用。
區(qū)別和用途
- JoinPoint 主要用于獲取方法的元數(shù)據(jù)信息,如方法名、參數(shù)等,不具備控制方法執(zhí)行流程的能力。
- ProceedingJoinPoint 繼承自
JoinPoint
,可以控制方法的執(zhí)行流程,在 Around Advice 中使用,可以決定是否繼續(xù)執(zhí)行目標(biāo)方法,以及在執(zhí)行前后進(jìn)行額外的處理。