用最通俗的方法講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 ─── <目標物件>
到這裡你應該理解什麼是面向切面了,如果你還是不理解,不怕,可能是我的表達方式不適合你,全網有很多對於面向物件的優秀理解. 雖然篩選好文章需要時間,我可以幫你找出來,但其實在篩選的過程中,更是