以前的源碼解析章節,本人講解了Spring IOC 的核心部分的源碼。若是你熟悉Spring AOP的使用的話,在瞭解Spring IOC的核心源碼以後,學習Spring AOP 的源碼,應該能夠說是水到渠成,不會有什麼困難。java
可是直接開始講Spring AOP的源碼,本人又以爲有點突兀,因此便有了這一章。Spring AOP 的入門使用介紹:包括Spring AOP的一些概念性介紹和配置使用方法。web
這裏先貼一下思惟導圖。spring
AOP : 面向切面編程(Aspect Oriented Programming)Aspect是一種新的模塊化機制,用來描述分散在對象、類或函數中的橫切關注點(crosscutting concern)。從關注點中分離出橫切關注點是面向切面的程序設計的核心概念。分離關注點使解決特定領域問題的代碼從業務邏輯中獨立出來,業務邏輯的代碼中再也不含有針對特定領域問題代碼的調用,業務邏輯同特定領域問題的關係經過切面來封裝、維護,這樣本來分散在整個應用程序中的變更就能夠很好地管理起來。express
最近在看李智慧的《大型網站技術架構》一書中,做者提到,開發低耦合系統是軟件設計的終極目標之一。AOP這種面向切面編程的的方式就體現了這樣的理念。將一些重複的、和業務主邏輯不相關的功能性代碼(日誌記錄、安全管理等)經過切面模塊化地抽離出來進行封裝,實現關注點分離、模塊解耦,使得整個系統更易於維護管理。編程
這樣分而治之的設計,讓我感受到了一種美感。安全
AOP 要實現的是在咱們原來寫的代碼的基礎上,進行必定的包裝,如在方法執行前、方法返回後、方法拋出異常後等地方進行必定的攔截處理或者叫加強處理。架構
AOP 的實現並非由於 Java 提供了什麼神奇的鉤子,能夠把方法的幾個生命週期告訴咱們,而是咱們要實現一個代理,實際運行的實例實際上是生成的代理類的實例。併發
前面提到過,Spring AOP 延用了 AspectJ 中的概念,使用了 AspectJ 提供的 jar 包中的註解。也就是Spring AOP裏面的概念和術語,並非Spring獨有的,而是和AOP相關的。app
概念能夠草草看過,在看了以後的章節以後再回來看會對概念理解的更深。框架
術語 | 概念 |
---|---|
Aspect |
切面是Pointcut 和Advice 的集合,通常單獨做爲一個類。Pointcut 和Advice 共同定義了關於切面的所有內容,它是何時,在什麼時候和何處完成功能。 |
Joinpoint |
這表示你的應用程序中能夠插入AOP方面的一點。也能夠說,這是應用程序中使用Spring AOP框架採起操做的實際位置。 |
Advice |
這是在方法執行以前或以後採起的實際操做。 這是在Spring AOP框架的程序執行期間調用的實際代碼片斷。 |
Pointcut |
這是一組一個或多個切入點,在切點應該執行Advice 。 您可使用表達式或模式指定切入點,後面示例會提到。 |
Introduction |
引用容許咱們向現有的類添加新的方法或者屬性 |
Weaving |
建立一個被加強對象的過程。這能夠在編譯時完成(例如使用AspectJ編譯器),也能夠在運行時完成。Spring和其餘純Java AOP框架同樣,在運行時完成織入。 |
PS:在整理概念的時候有個疑問,爲何網上這麼多中文文章把advice 翻譯成「通知」呢???概念上說得通嗎???我更願意翻譯成「加強」(併發中文網ifeve.com 也是翻譯成加強)
還有一些註解,表示Advice的類型,或者說加強的時機,看過以後的示例以後會更加的清楚。
術語 | 概念 |
---|---|
Before |
在方法被調用以前執行加強 |
After |
在方法被調用以後執行加強 |
After-returning |
在方法成功執行以後執行加強 |
After-throwing |
在方法拋出指定異常後執行加強 |
Around |
在方法調用的先後執行自定義的加強行爲(最靈活的方式) |
Spring 2.0 以後,Spring AOP有了兩種配置方式。
<aop />
PS:我的比較鍾情於@AspectJ 這種方式,使用下來是最方面的。也多是由於我以爲XML方式配置的Spring Bean很不簡潔、寫起來很差看吧,因此有點排斥吧。23333~
本文主要針對註解方式講解,而且給出對應的DEMO;以後的源碼解析也會以註解的這種方式爲範例講解Spring AOP的源碼(整個源碼解析看完,會對其餘方式舉一反三,由於原理都是同樣的)
若是對其餘配置方式感興趣的同窗能夠google其餘的學習資料。
來一條分割線,正式開始
@AspectJ
註解配置方式開啓@AspectJ
的註解配置方式,有兩種方式
在XML中配置:
<aop:aspectj-autoproxy/>
使用@EnableAspectJAutoProxy
註解
@Configuration @EnableAspectJAutoProxy public class Config { }
開啓了上述配置以後,全部在容器中,被@AspectJ
註解的 bean 都會被 Spring 當作是 AOP 配置類,稱爲一個 Aspect。
NOTE:這裏有個要注意的地方,@AspectJ 註解只能做用於Spring Bean 上面,因此你用 @Aspect 修飾的類要麼是用 @Component註解修飾,要麼是在 XML中配置過的。
好比下面的寫法,
// 有效的AOP配置類 @Aspect @Component public class MyAspect { //.... } // 若是沒有在XML配置過,那這個就是無效的AOP配置類 @Aspect public class MyAspect { //.... }
Pointcut 在大部分地方被翻譯成切點,用於定義哪些方法須要被加強或者說須要被攔截。
在Spring 中,咱們能夠認爲 Pointcut 是用來匹配Spring 容器中全部知足指定條件的bean的方法。
好比下面的寫法,
// 指定的方法 @Pointcut("execution(* testExecution(..))") public void anyTestMethod() {}
下面完整列舉一下 Pointcut 的匹配方式:
這個最簡單的方式就是上面的例子,"execution(* testExecution(..))"
表示的是匹配名爲testExecution
的方法,*
表明任意返回值,(..)
表示零個或多個任意參數。
within:指定所在類或所在包下面的方法(Spring AOP 獨有)
// 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 獨有)
// controller 層 @Pointcut("bean(testController)") public void inControllerLayer() {}
上述是平常使用中常見的幾種配置方式有更細的匹配需求的,能夠參考這篇文章:https://www.baeldung.com/spri...
關於 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:
意思就是,若是你是在開發企業級應用,Spring 建議你使用 SystemArchitecture
這種切面配置方式,即將一些公共的PointCut 配置所有寫在這個一個類裏面維護。官網文檔給的例子像下面這樣(它文中使用 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() {} }
上面這個 SystemArchitecture 很好理解,該 Aspect 定義了一堆的 Pointcut,隨後在任何須要 Pointcut 的地方均可以直接引用。
配置切點,表明着咱們想讓程序攔截哪一些方法,但程序須要怎麼對攔截的方法進行加強,就是後面要介紹的配置 Advice。
注意,實際開發過程中,Aspect 類應該遵照單一職責原則,不要把全部的Advice配置所有寫在一個Aspect類裏面。
這裏是爲了演示方便,因此寫在了一塊兒。
先直接上示例代碼,裏面包含了Advice 的幾種配置方式(上文名詞概念小節中有提到)。
/** * 注:實際開發過程中,Advice應遵循單一職責,不該混在一塊兒 * * @author Richard_yyf * @version 1.0 2019/10/28 */ @Aspect @Component public class GlobalAopAdvice { @Before("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()") public void doAccessCheck() { // ... 實現代碼 } // 實際使用過程中 能夠像這樣把Advice 和 Pointcut 合在一塊兒,直接在Advice上面定義切入點 @Before("execution(* ric.study.demo.dao.*.*(..))") public void doAccessCheck() { // ... 實現代碼 } // 在方法 @AfterReturning("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()") public void doAccessCheck() { // ... 實現代碼 } // returnVal 就是相應方法的返回值 @AfterReturning( pointcut="ric.study.demo.aop.SystemArchitecture.dataAccessOperation()", returning="returnVal") public void doAccessCheck(Object returnVal) { // ... 實現代碼 } // 異常返回的時候 @AfterThrowing("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()") public void doRecoveryActions() { // ... 實現代碼 } // 注意理解它和 @AfterReturning 之間的區別,這裏會攔截正常返回和異常的狀況 @After("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()") public void doReleaseLock() { // 一般就像 finally 塊同樣使用,用來釋放資源。 // 不管正常返回仍是異常退出,都會被攔截到 } // 這種最靈活,既能作 @Before 的事情,也能夠作 @AfterReturning 的事情 @Around("ric.study.demo.aop.SystemArchitecture.businessService()") public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // target 方法執行前... 實現代碼 Object retVal = pjp.proceed(); // target 方法執行後... 實現代碼 return retVal; } }
在某些場景下,咱們想在@Before的時候,去獲取方法的入參,好比進行一些日誌的記錄,咱們能夠經過 org.aspectj.lang.JoinPoint
來實現。上文中的ProceedingJoinPoint
就是其子類。
@Before("...") public void logArgs(JoinPoint joinPoint) { System.out.println("方法執行前,打印入參:" + Arrays.toString(joinPoint.getArgs())); }
再舉個與之對應的,方法返參打印:
@AfterReturning( pointcut="...", returning="returnVal") public void logReturnVal(Object returnVal) { System.out.println("方法執行後,打印返參:" + returnVal)); }
介紹完上述的配置過程以後,咱們用一個快速的Demo來實際演示一遍。這裏把順序變一下;
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)); } }
@Aspect @Component public class PointCutConfig { @Pointcut("within(ric.study.demo.aop.svc..*)") public void inSvcLayer() {} }
/** * @author Richard_yyf * @version 1.0 2019/10/29 */ @Component @Aspect public class ServiceLogAspect { // 攔截,打印日誌,而且經過JoinPoint 獲取方法參數 @Before("ric.study.demo.aop.PointCutConfig.inSvcLayer()") public void logBeforeSvc(JoinPoint joinPoint) { System.out.println("在service 方法執行前 打印第 1 第二天志"); System.out.println("攔截的service 方法的方法簽名: " + joinPoint.getSignature()); System.out.println("攔截的service 方法的方法入參: " + Arrays.toString(joinPoint.getArgs())); } // 這裏是Advice和Pointcut 合在一塊兒配置的方式 @Before("within(ric.study.demo.aop.svc..*)") public void logBeforeSvc2() { System.out.println("在service的方法執行前 打印第 2 第二天志"); } }
@AspectJ
註解配置方式,並啓動這裏爲了圖方便,把配置類和啓動類寫在了一塊兒,
/** * @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()); } }
在service 方法執行前 打印第 1 第二天志 攔截的service 方法的方法簽名: void ric.study.demo.aop.svc.TestSvcImpl.process() 攔截的service 方法的方法入參: [] 在service的方法執行前 打印第 2 第二天志 test svc is working ================== 在service 方法執行前 打印第 1 第二天志 攔截的service 方法的方法簽名: void ric.study.demo.aop.svc.DateSvcImpl.printDate(Date) 攔截的service 方法的方法入參: [Mon Nov 04 18:11:34 CST 2019] 在service的方法執行前 打印第 2 第二天志 2019-11-04 18:11:34
前面有提到過,Spring AOP在目標類有實現接口的時候,會使用JDK 動態代理來生成代理類,咱們結合上面的DEMO看看,
若是咱們想不論是否有實現接口,都是強制使用Cglib的方式來實現怎麼辦?
Spring 提供給了咱們對應的配置方式,也就是proxy-target-class
.
註解方式: //@EnableAspectJAutoProxy(proxyTargetClass = true) // 這樣子就是默認使用CGLIB XML方式: <aop:config proxy-target-class="true">
改了以後,
本文詳細介紹了Spring AOP的起源、名詞概念以及基於註解的使用方式。
本文按照做者的寫做習慣,是源碼解析章節的前置學習章節。在下一章中,咱們會以註解方式爲入口,介紹Spring AOP 的源碼設計,解讀相關核心源碼(整個源碼解析看完,會對其餘方式舉一反三,由於原理都是同樣的)。
感興趣的能夠翻到【前言】部分,再看一下思惟導圖。
若是本文有幫助到你,但願能點個贊,這是對個人最大動力。