1. 程式人生 > >用最通俗的方法講spring [一] ──── AOP

用最通俗的方法講spring [一] ──── AOP

@[TOC](用最通俗的方法講spring [一] ──── AOP)

寫這個系列的目的(可以跳過不看)

自己寫這個系列的目的,是因為自己是個比較笨的人,我曾一度懷疑自己的智商不適合幹程式設計這個行業.因為在我自學的過程中,看過無數別人的文章,部落格,心得.可那一大堆的術語,技術基礎,根本就難以閱讀理解.再加上網上文章良莠不齊,很多文章都是寫給已經懂的人看的. 一個例子: Q:什麼是控制反轉. A:將對在自身物件中的一個內建物件的控制反轉,反轉後不再由自己本身的物件進行控制這個內建物件的建立,而是由第三方系統去控制這個內建物件的建立. ??? 這種專業的解釋,在我看來就不是給學習的人看的,而是給已經有相當經驗的人讀的. 難道不能用更加感性的解釋這些東西嗎? 程式語言作為一門語言,我認為一定程度上是相當感性的東西,既然我們互相說話時可以聽懂,那麼,這些技術就可以在一定程度上,完全可以用感性的方式解釋.

AOP

AOP是面向切面. 得,說了白說. 什麼是面向切面?這是個問題,但我們先不著急回答.

一. 舉個例子

我們的程式碼結構,大多都是MVC模式.那我們用MVC舉個簡單的例子. MVC模式的話,最常用的spring + springMVC + Mybatis,大多數是下面的樣子.

┌───────────────┐
|   view        | 使用者介面
├───────────────┤
|   Controller  | 控制層
├───────────────┤
|   Service     | 服務層,也叫業務層
├───────────────┤
|   Dao         | 持久層,也叫資料訪問層
├───────────────┤
|   Database    | 資料庫
└───────────────┘

外加Model作為資料載體在各個層之間傳來傳去 只要是接觸過JavaWeb開發的,上面都看的懂,如果這個不懂暫時還是不要深究這些知識,先以框架應用為主.

這個Service是個UserService,作用只有一個:儲存使用者.

┌────────────────────┐
|   view             |
├────────────────────┤
|   Controller       |
├────────────────────┤
|   UserService.add  | ───> 新增使用者
├────────────────────┤
|   Dao              |
├────────────────────┤
|   Database         |
└────────────────────┘

現在,有需求要我們在Service中增加日誌,開發這個功能的同事不在了,經理要求我們來做這個需求. 那麼根據面向物件的理念. 呼叫service方法前要呼叫一個日誌方法,呼叫完後要呼叫一個日誌方法,那麼程式碼中就要增加兩行程式碼. 於是,程式碼成了這樣:

┌────────────────────┐
|   view             |
├────────────────────┤
|   Controller       |
├────────────────────┤
|   log.info...      | ───> 呼叫日誌
|   UserService.add  | ───> 新增使用者
|   log.info...      | ───> 呼叫日誌
├────────────────────┤
|   Dao              |
├────────────────────┤
|   Database         |
└────────────────────┘

這麼寫正確嗎? 答案是當然正確,需求解決了,日誌正常儲存了,領導很高興.

第二天,又有需求要控制service的事務,OK,然後程式碼變成了這樣.

┌────────────────────┐
|   view             |
├────────────────────┤
|   Controller       |
├────────────────────┤
|   Transaction      | ───> 事務管理
|   log.info...      | ───> 呼叫日誌
|   UserService.add  | ───> 新增使用者
|   log.info...      | ───> 呼叫日誌
|   Transaction      | ───> 事務管理
├────────────────────┤
|   Dao              |
├────────────────────┤
|   Database         |
└────────────────────┘

第三天,專案經理要求我們記錄方法的執行所用時間,小意思,然後程式碼變成了這樣

┌────────────────────┐
|   view             |
├────────────────────┤
|   Controller       |
├────────────────────┤
|   beginTime        | ───> 開始時間
|   Transaction      | ───> 事務管理
|   log.info...      | ───> 呼叫日誌
|   UserService.add  | ───> 新增使用者
|   log.info...      | ───> 呼叫日誌
|   Transaction      | ───> 事務管理
|   endTime          | ───> 結束時間
├────────────────────┤
|   Dao              |
├────────────────────┤
|   Database         |
└────────────────────┘

第四天,客戶要求我們實現新增使用者時的許可權校驗,好說,程式碼變成了這樣

┌────────────────────┐
|   view             |
├────────────────────┤
|   Controller       |
├────────────────────┤
|   permissions      | ───> 校驗許可權
|   beginTime        | ───> 開始時間
|   Transaction      | ───> 事務管理
|   log.info...      | ───> 呼叫日誌
|   UserService.add  | ───> 新增使用者
|   log.info...      | ───> 呼叫日誌
|   Transaction      | ───> 事務管理
|   endTime          | ───> 結束時間
├────────────────────┤
|   Dao              |
├────────────────────┤
|   Database         |
└────────────────────┘

/*
不知道你們怎麼想,但我現在,仍然覺得這種寫法挺好的.
通俗易懂,可讀性強到不知道哪裡去了.
當然有個前提,這個專案只有這一個方法.
*/

第五天,開發UserService的同事回來一看,一臉懵逼,明明走的時候只有一行,現在卻有一大堆不知道什麼意思的程式碼存在. 得了,他在此基礎上開發,增加各種方法,功能. 隨著需求的變動,慢慢的,一百個service都變成了上面那個樣子. 接著記錄日誌的功能有了變動,你發現會影響到呼叫的類,搜了一下整個專案,發現有特麼幾千個地方都呼叫了這個方法. 你心中一萬頭草泥馬掠過,然後提出了辭職. 接著有個新人接手了這個專案,開啟一看,這映入眼簾的一大坨屎一樣的是特麼什麼東西?? 硬著頭皮改了幾版,每個版本都有BUG,領導一怒之下,專案作廢了,重金找了幾個人重新做了.

==那麼問題來了== 現在的需求搞砸了可以辭職,專案爛了可以重做. 那世界上第一個遇到這個問題的人,是怎麼解決的?技術就是這技術,重做不還是一樣會遇到這些問題嗎?那我還怎麼拓展功能呢?難道所有專案最終都會走向這種絕境? 於是前人開始探索:

┌───────────────┐
|   Controller  |
|               ┼───────> 拓展方法寫在這?
├───────────────┤
|   Service     |
├───────────────┤
|               ┼───────> 拓展方法寫在這?
|    Dao        |
└───────────────┘

這不都是一回事嗎?根本不解決問題.

我等凡夫俗子解決不了,但世界上有的是自帶外掛的人. 於是,有那麼幾個人就找到了解決辦法! 既然我要拓展方法,就要在你的方法裡寫程式碼,那麼我能不能在不改你程式碼的基礎上來拓展呢?

==比如在這個地方!==

┌───────────────┐
|   Controller  |
├───────────────┼───────> 比如,寫在這!
|   Service     |
└───────────────┘

what the hell ?????? 你是讓我在一個莫須有的地方寫程式碼?

當然不是,大神的意思是程式碼要這樣寫.

┌───────────────┐
|   Controller  |      ┌───────────────┐
├───────────────┼──────┤ log.info...   |
|   Service     |      └───────────────┘
└───────────────┘

但執行的時候,會是這樣

┌───────────────┐
|   Controller  |
├───────────────┤
|   log.info... |
|   Service     |
└───────────────┘

what the hell ??????????????????????????????? 這是為什麼?

==答案很簡單:== 首先你既然打算了解AOP,那你應該是有一些Java基礎的. 我們都知道Java檔案是沒有用的,這就相當於一個txt檔案,當程式真正執行的時候,是把Java檔案通過編譯器編譯成class檔案來執行的. 那麼我們好像看到重點了,編譯器! 既然最終執行的是class檔案,而class檔案又是編譯器生成的,那我們是不是可以在編譯器上動動手腳呢? 沒錯,想要達成上面的效果,就需要一個不同的編譯器. 把日誌這段程式碼編譯到service中 當然了,這個編譯器不需要我們開發,而是早有人搞定了.(若是我能開發,我早就去谷歌開發者大會上做演講了= =) 這就是大名鼎鼎的 :==ajc編譯器==(他屬於AspectJ框架,可以單獨使用此框架寫個例子,可以加深理解)

1. 想象一下,有這麼兩坨東西

┌────────────────────┐
|   Controller.java  |
├────────────────────┤           ┌─────────────────┐
|   Service.java     |           |  log.info.java  |
└────────────────────┘           └─────────────────┘

2. 然後開始編譯 : 編譯器開始把Service.java中的程式碼編織成一個class檔案.
3. ajc編譯器開始編譯 : ajc開始把log.info.java編織插入到service中.

┌────────────────────┐
|   Controller.java  |    _________________
├────────────────────┤ _/ log.info.java   |
|   Service.java     |  \ ________________|
└────────────────────┘

4. 就像上面,把log.info織入到這個這個方法中.
5. 最終形成的calss檔案,就是這樣

┌───────────────┐
|   Controller  |
├───────────────┤
|   log.info... |
|   Service     |
└───────────────┘

成了,問題解決了.有了這個編譯器,前面的問題有迎刃而解. ==ps : ajc編譯器並不是唯一解決辦法==

那麼問題又又來了,==面向切面是什麼?== 也許你已經有答案了.

┌───────────────┐
|   Controller  |        ┌──────────────────────────────────┐
├───────────────┼──────> | 我們在這個角度寫程式碼,就是面向切面.  |
|   Service     |        |                                  |
├───────────────┼──────> | 我們在這個角度寫程式碼,就是面向切面.  |
|      Dao      |        └──────────────────────────────────┘
└───────────────┘

我們站在那條線的角度寫程式碼,就是面向切面. 那條線,是不是將原本的程式碼分成了兩個部分呢? 更形象一點

		┌───────────────┐
		|   Controller  |
		└───────────────┘
───────────────────────────────────── 這條線,像不像把整個程式碼切開的一條線呢?那麼這條線,就叫切面
───────────────────────────────────── 如 : 日誌呼叫
───────────────────────────────────── 如 : 呼叫時間
───────────────────────────────────── 如 : 事務管理
		┌───────────────┐
		|   Service     |
		├───────────────┤
		|      Dao      |
		└───────────────┘

上面的每一個如,就是一個切面類,也相當於一個切面,畢竟沒有人規定我們必須只有一個切面不是?

面向切面是一種人們面對難題時的解決方案,他並不是一個新的語言或是新的技術. 他消除了面向物件的一系列程式設計陋習.或者說解決了面向物件的一些缺點.

到了這裡,我相信有些人還是不太懂面向切面的意義或者作用是什麼,沒關係,不要氣餒,你一定比我當時可聰明多了.請繼續往下看.

	 這張圖可能並不太能直觀的表達面向切面意義有多麼重大.
	 ┌───────────────┐
	 |   Controller  |
	 └───────────────┘
	 ─────────────────────────
	 ┌───────────────┐
	 |   Service     |
	 ├───────────────┤
	 |      Dao      |
	 └───────────────┘
	
	 那麼,這張圖呢?

     ┌────────────────┬────────────────┬────────────────┬────────────────┬────────────────┬────────────────┐
     |   Controller1  |   Controller2  |   Controller3  |   Controller4  |   Controller5  |   Controller6  |
     └────────────────┴────────────────┴────────────────┴────────────────┴────────────────┴────────────────┘
─────────────────────── 面向切面的方法,可以織入到所有的service中,而不需要改變service的程式碼 ─────────────────────────
─────────────────────── 許可權校驗 ───────────────────────────────────────────────────────────────────────────────
─────────────────────── 日誌呼叫 ───────────────────────────────────────────────────────────────────────────────
─────────────────────── 呼叫時間 ───────────────────────────────────────────────────────────────────────────────
─────────────────────── 事務管理 ───────────────────────────────────────────────────────────────────────────────
     ┌────────────────┬────────────────┬────────────────┬────────────────┬────────────────┬────────────────┐
     |   Service1     |   Service2     |   Service3     |   Service4     |   Service5     |   Service6     |
     └────────────────┴────────────────┴────────────────┴────────────────┴────────────────┴────────────────┘
─────────────────────── 面向切面的方法,可以織入到所有的Dao中,而不需要改變Dao的程式碼 ─────────────────────────────────
─────────────────────── 日誌呼叫 ───────────────────────────────────────────────────────────────────────────────
─────────────────────── 呼叫時間 ───────────────────────────────────────────────────────────────────────────────
     ┌────────────────┬────────────────┬────────────────┬────────────────┬────────────────┬────────────────┐
     |      Dao1      |      Dao2      |      Dao3      |      Dao4      |      Dao5      |      Dao6      |
     └────────────────┴────────────────┴────────────────┴────────────────┴────────────────┴────────────────┘
	 
	 我只需要宣告一個類,就可以該類中的方法插入到任意類的位置
	 

有些人說,用代理模式就可以解決上面所說的程式設計時的一系列問題. 沒錯! 面向切面 ─── 是思想. 代理模式 ─── 是思想的實現.

二. 畫個圖片

上面畫過了

三. 寫個程式碼

只講概念,後面講例子

四. 說個人話

切面的幾大概念

1.切面 (Aspect) :

拓展的類 概念 : Aspect 宣告類似於Java中的類宣告,在Aspect中會包含著一些切點以及相應的增強. 人話 : 建立一個類,這個類中的<增強>將插入到<目標物件>程式碼中.

2.連線點 (Joint point) :

呼叫點 概念 : 表示在程式中明確定義的點,典型的包括方法呼叫,對類成員的訪問以及異常處理程式塊的執行等等,它自身還可以巢狀其它 joint point. 人話 : 被<切點>所包含的內容, 如切點為 userService.add()方法 : 連線點就是userService.add() 如切點為 com.xxx包下的所有service : com.xxx包下的所有service的任意一個方法 如切點為 com.xxx包下的所有add開頭的方法 : com.xxx包下的所有add開頭的方法

3.切點 (Pointcut):

在哪裡增強 概念 : 表示一組<連線點>,這些 joint point 或是通過邏輯關係組合起來,或是通過通配、正則表示式等方式集中起來,它定義了相應的增強將要發生的地方.或是我們要織入的地方 人話 : 要往哪裡插入 如 userService.add()方法 或 com.xxx包下的所有service方法 或 com.xxx包下的所有add開頭的方法

4.增強 (Advice) :

拓展方法 概念 : Advice 定義了在<切點>裡面定義的程式點具體要做的操作,它通過 before、after 和 around 來區別是在每個<連線點>之前、之後還是代替執行的程式碼. 人話 : 切面類中需要定義的,連線點方法呼叫前的拓展,之後的拓展,或者之前和之後的拓展.

5.目標物件 (Target) :

被增強的目標 概念 : 增強(Advice) 的目標物件.. 人話 : 被增強的目標

6.織入 (Weaving):

插入的動作 概念 : 將 Aspect 和其他物件連線起來, 並建立 Adviced object 的過程. 人話 : 將切面類插入進去

例如:
我們要在service的add方法裡裡增加日誌
┌───────────────┐
|   Controller  |
└───────────────┘
─────────────────────────────────────    <-- com.xxx.service.UService  ─── <切點>
┌───────────────┐      ┌────────────┐    <-- log.info所在的類           ─── <切面>
| UService.add  |      |  log.info  |    <-- log.info方法就是           ─── <增強>
├───────────────┤      └────────────┘
|      Dao      |
└───────────────┘

UService.add的呼叫就是   ─── <連線點>
UService                ─── <目標物件>

到這裡你應該理解什麼是面向切面了,如果你還是不理解,不怕,可能是我的表達方式不適合你,全網有很多對於面向物件的優秀理解. 雖然篩選好文章需要時間,我可以幫你找出來,但其實在篩選的過程中,更是