引言
本文是Spring
原理分析的第三篇博文,主要闡述Spring AOP
相關概念,同時從源碼層面分析AOP
實現原理。對於AOP
原理的理解有利於加深對Spring
框架的深入理解。同時我也希望可以探究Spring
框架在處理AOP
的解決思路,學習框架的時候,有時候需要站在設計者的角度上去考慮,如果自己是設計者遇到同樣需要解決的問題自己會怎麼去處理,然後再對照實際框架中的處理方式,這樣可以發現自己考慮不足之處。
本文側重於找到Spring
框架處理AOP
的起點,至於涉及到的動態代理相關的問題將在下一篇文中着重介紹。
- 提問題
- AOP概念
- AOP代碼示例
- 源碼分析
- 總結
一、提問題
到底什麼是AOP
?Spring
框架到底在什麼階段進行數據織入的?AOP
在實際項目開發中到底有什麼作用以及怎樣將AOP
的編程思想運用到我們的實際項目開發中?
二、AOP概念
AOP(Aspect Orient Programming)
,我們通常稱爲面向切面編程。所謂切面是相對於面向對象來說的。面向對象是將實物抽象爲對象,這是個縱向的概念。而面向切面是一個橫向的概念,它更加關注那些散落在代碼中公用的不涉及具體業務邏輯的通用處理方式,例如日誌、權限驗證以及統一異常處理等等。它是對於面向對象編程思想的一種結構化補充。核心思想就是將與業務邏輯無關的進行統一的框架織入,不對原有代碼以及業務邏輯造成侵入。
Spring AOP
在運行時,能夠動態地將代碼切入到指定的類的指定方法、指定位置上的編程思想就是面向切面編程,這種切入的特點是不影響原來的業務邏輯。但是像AspectJ
可以在編譯階段以及類加載階段進行織入。
一些概念的說明:
- 切面
所謂切面,按照自己的理解可以把它看作爲一把刀,將他橫切於其他物品,通過@Aspect
來將類定義一個切面,它就是切點與通知的結合,如下圖所示。
- 切點
本質上來說,就是需要定義一個切入點表達式,使得可以在增強處理中使用到。通過切點定位和篩選特定的連接點。它關注通知需要織入的一個或者多個連接點,切入點包括兩部分:
(1)切入點表達式:指定切入點與哪些方法進行匹配;
(2)切入點名稱:方法簽名 - 連接點
連接點是一個相對虛擬的概念,可以將它理解爲切點的集合,也就是Spring
允許我們進行通知操作的地方,比如方法、異常拋出的地方,如果使用aspectj
則也支持在構造器中或者屬性中允許通知。 - 通知
通俗地說,通知就是我們需要實現的功能,可以分爲前置、後置、異常、最終以及環繞通知這五類。例如日誌,權限等業務邏輯。
- 前置通知:在目標方法或者連接點被調用前執行通知操作;
- 後置通知:在某些連接點執行完成之後進行通知操作;
- 異常通知:在方法拋出異常退出當前時進行通知操作;
- 最終通知:當切入點退出時無論是方法正常執行結束還是異常拋出後退出執行的通知操作;
- 環繞通知:包圍一個切點的通知,如方法調用。這是最強大的一種通知類型。環繞通知可以在方法調用前後完成自定義的行爲。它也會選擇是否繼續執行連接點或直接返回它自己的返回值或拋出異常來結束執行。
三、AOP代碼示例
1.定義一個普通業務邏輯方法
@Component("userDao")
public class UserDao {
/**
* @Description: 查詢
* @Return
* @Author taomeng
* @Version V1.0.0
* @CreateDate: 2018/9/23 11:31
*/
public void query() {
System.out.println("query user data!!!");
}
}
2.定義一個切面類,同時在這個切面類中將切點等進行定義。
/**
* @Auther: taomeng
* @Date: 2018/9/17 23:32
* @Description: 定義切面類
*/
@Configuration
@Aspect
@ComponentScan(basePackages = {"com.tm.springrun.module"})
@ImportResource(locations = {"classpath:spring-context.xml"})
public class AopConfig {
/**
* @Description:定義切點
* @Return
* @Author taomeng
* @Version V1.0.0
* @CreateDate: 2018/9/23 11:32
*/
@Pointcut("execution(* com.tm.springrun.module.dao.UserDao.query())")
public void declareJointPointExpression(){}
@Before("declareJointPointExpression()")
public void beforeMethod(JoinPoint joinPoint) {
String method = joinPoint.getSignature().getName();
System.out.println("The method of before:" + method);
}
@After("declareJointPointExpression()")
public void afterMethod(JoinPoint joinPoint) {
String method = joinPoint.getSignature().getName();
System.out.println("The method of after:" + method );
}
}
3.在Spring Context
中獲取Bean
的實例,同時執行Bean
實例的方法。
/**
* @Auther: taomeng
* @Date: 2018/9/17 23:37
* @Description: 測試AOP
*/
public class Test {
public static void main(String[] args) {
//1.加載spring環境
AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(AopConfig.class);
//2.獲取spring context中的bean的實例
UserDao userDao = (UserDao)annotationConfigApplicationContext.getBean("userDao");
//3.執行bean中的方法
userDao.query();
}
}
同時需要在配置文件中開啓AOP,如下所示:
<aop:aspectj-autoproxy/>
四、源碼分析
對Spring AOP
進行源碼分析,就是要找到Spring
框架中處理AOP
的源頭以及在什麼階段進行 數據織入的,帶着這樣的問題去做源碼分析纔可以做到有的放矢,要不然那麼多源碼看下去就像無頭蒼蠅一樣不知道如何下手。
如同第二節示例代碼所示, 在方法執行時,通知已經被執行了。那麼Spring AOP
應該是在前兩個步驟中起作用的,要麼是在Bean
定義時候,要麼就是在獲取Bean
的時候進行的。如下圖所示,我們希望通過debug的方式找到AOP
在什麼階段開始起作用。
以下爲尋找AOP
起作用起點的過程,首先我們猜測Spring
在處理AOP
的時候是在獲取bean
的時候進行數據的織入的,所以在獲取bean的時候進行斷點跟蹤。
進入斷點,進入AbstractBeanFactory
類中的getBean
方法。
進入getBean
方法,在這個方法中
此時發現對象已經被代理了,所以需要到getSingleton(beanName)
這個方法中去查看。
在這個我們可以發現,Spring
框架存放bean名稱以及對應的對象的數據結構實際上是一個ConcurrentHashMap
。如下所示:
/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
我們可以看一下在這個map
中的存放了Spring
中加載的對象,這也是Spring IOC
的核心。既然在這個map
中獲取到了bean
,那肯定是在某個地方將對象進行put
操作。全文搜索後發現,在DefaultSingletonBeanRegistry
這個類中的addSingleton
方法中進行對象put
操作。
從上圖可知,在對象put
操作時,userDao
對象已經被代理了。這就說明在獲取到bean
的時候,對應的對象已經是被代理過的。所以實際數據織入並不是在獲取bean
的時候進行的,而是在bean
加載到Spring
環境中的時候就已經完成了。
那接下來我們需要找出調用addSingleton
方法的地方,
從下圖可以看出來,此時的mdb
還是原生的,那說明此時還沒有進行數據織入,是在下面的方法中進行的。
在doCreateBean
方法中,我們跟蹤到initializeBean
的方法,此時發現bean
已經被數據織入了,繼續進入方法。
繼續查看initializeBean
方法內部,如下圖所示:
再繼續查看
再進入方法進行查看
至此,我們終於跟蹤到實現代理的最初部分,其中根據條件判斷是jdk代理還是cglib代理。
四、總結
Spring
源碼很複雜,如果不是帶着目標去看,很難抓住重點。在尋找數據織入的部分,需要一步一步進行,每找到一個部分就需要在對應的部分打上斷點,同時去掉之前的斷點,不斷迭代深入,直到獲取到最終的起點,關於代理的這部分內容會放到下一篇文章中進行詳細闡述。
根據第三章中的源碼分析,我們明確了Spring
框架進行數據織入的起點,如下圖所示,是在Spring
框架加載Bean
的時候進行的。
Spring AOP
相關知識樹如下圖所示:
(ps:該圖片來源於網絡)