其實, 接觸了這麼久的 AOP, 我感受, AOP 給人難以理解的一個關鍵點是它的概念比較多, 並且坑爹的是, 這些概念通過了中文翻譯後, 變得面目全非, 相同的一個術語, 在不一樣的翻譯下, 含義總有着各類莫名其妙的差異. 鑑於此, 我在本章的開頭, 着重爲爲你們介紹一個 Spring AOP 的各項術語的基本含義. 爲了術語傳達的準確性, 我在接下來的敘述中, 能使用英文術語的地方, 儘可能使用英文.html
AOP(Aspect-Oriented Programming), 即 面向切面編程, 它與 OOP( Object-Oriented Programming, 面向對象編程) 相輔相成, 提供了與 OOP 不一樣的抽象軟件結構的視角.
在 OOP 中, 咱們以類(class)做爲咱們的基本單元, 而 AOP 中的基本單元是 Aspect(切面)java
aspect
由 pointcount
和 advice
組成, 它既包含了橫切邏輯的定義, 也包括了鏈接點的定義. Spring AOP就是負責實施切面的框架, 它將切面所定義的橫切邏輯織入到切面所指定的鏈接點中.
AOP的工做重心在於如何將加強織入目標對象的鏈接點上, 這裏包含兩個工做:express
能夠簡單地認爲, 使用 @Aspect 註解的類就是切面.編程
由 aspect 添加到特定的 join point(即知足 point cut 規則的 join point) 的一段代碼.
許多 AOP框架, 包括 Spring AOP, 會將 advice 模擬爲一個攔截器(interceptor), 而且在 join point 上維護多個 advice, 進行層層攔截.
例如 HTTP 鑑權的實現, 咱們能夠爲每一個使用 RequestMapping 標註的方法織入 advice, 當 HTTP 請求到來時, 首先進入到 advice 代碼中, 在這裏咱們能夠分析這個 HTTP 請求是否有相應的權限, 若是有, 則執行 Controller, 若是沒有, 則拋出異常. 這裏的 advice 就扮演着鑑權攔截器的角色了.數組
a point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution.app
程序運行中的一些時間點, 例如一個方法的執行, 或者是一個異常的處理.
在 Spring AOP 中, join point 老是方法的執行點, 即只有方法鏈接點.
框架
匹配 join point 的謂詞(a predicate that matches join points).
Advice 是和特定的 point cut 關聯的, 而且在 point cut 相匹配的 join point 中執行.
在 Spring 中, 全部的方法均可以認爲是 joinpoint, 可是咱們並不但願在全部的方法上都添加 Advice, 而 pointcut 的做用就是提供一組規則(使用 AspectJ pointcut expression language 來描述) 來匹配joinpoint, 給知足規則的 joinpoint 添加 Advice.
函數
在 Spring AOP 中, 全部的方法執行都是 join point. 而 point cut 是一個描述信息, 它修飾的是 join point, 經過 point cut, 咱們就能夠肯定哪些 join point 能夠被織入 Advice. 所以 join point 和 point cut 本質上就是兩個不一樣緯度上的東西.
advice 是在 join point 上執行的, 而 point cut 規定了哪些 join point 能夠執行哪些 advice
this
爲一個類型添加額外的方法或字段. Spring AOP 容許咱們爲 目標對象
引入新的接口(和對應的實現). 例如咱們可使用 introduction 來爲一個 bean 實現 IsModified 接口, 並以此來簡化 caching 的實現.編碼
織入 advice 的目標對象. 目標對象也被稱爲 advised object
.
由於 Spring AOP 使用運行時代理的方式來實現 aspect, 所以 adviced object 老是一個代理對象(proxied object)
注意, adviced object 指的不是原來的類, 而是織入 advice 後所產生的代理類.
一個類被 AOP 織入 advice, 就會產生一個結果類, 它是融合了原類和加強邏輯的代理類.
在 Spring AOP 中, 一個 AOP 代理是一個 JDK 動態代理對象或 CGLIB 代理對象.
將 aspect 和其餘對象鏈接起來, 並建立 adviced object 的過程.
根據不一樣的實現技術, AOP織入有三種方式:
Spring AOP 默認使用標準的 JDK 動態代理(dynamic proxy)技術來實現 AOP 代理, 經過它, 咱們能夠爲任意的接口實現代理.
若是須要爲一個類實現代理, 那麼可使用 CGLIB 代理.
當一個業務邏輯對象沒有實現接口時, 那麼Spring AOP 就默認使用 CGLIB 來做爲 AOP 代理了. 即若是咱們須要爲一個方法織入 advice, 可是這個方法不是一個接口所提供的方法, 則此時 Spring AOP 會使用 CGLIB 來實現動態代理. 鑑於此, Spring AOP 建議基於接口編程, 對接口進行 AOP 而不是類.
看完了上面的理論部分知識, 我相信仍是會有很多朋友感受到 AOP 的概念仍是很模糊, 對 AOP 中的各類概念理解的還不是很透徹. 其實這很正常, 由於 AOP 中的概念是在是太多了, 我當時也是花了老大勁才梳理清楚的.
下面我以一個簡單的例子來比喻一下 AOP 中 aspect, jointpoint, pointcut 與 advice 之間的關係.
讓咱們來假設一下, 從前有一個叫爪哇的小縣城, 在一個月黑風高的晚上, 這個縣城中發生了命案. 做案的兇手十分狡猾, 現場沒有留下什麼有價值的線索. 不過萬幸的是, 剛從隔壁回來的老王剛好在這時候無心中發現了兇手行兇的過程, 可是因爲天色已晚, 加上兇手蒙着面, 老王並無看清兇手的面目, 只知道兇手是個男性, 身高約七尺五寸. 爪哇縣的縣令根據老王的描述, 對守門的士兵下命令說: 凡是發現有身高七尺五寸的男性, 都要抓過來審問. 士兵固然不敢違背縣令的命令, 只好把進出城的全部符合條件的人都抓了起來.
來讓咱們看一下上面的一個小故事和 AOP 到底有什麼對應關係.
首先咱們知道, 在 Spring AOP 中 join point 指代的是全部方法的執行點, 而 point cut 是一個描述信息, 它修飾的是 join point, 經過 point cut, 咱們就能夠肯定哪些 join point 能夠被織入 Advice. 對應到咱們在上面舉的例子, 咱們能夠作一個簡單的類比, join point 就至關於 爪哇的小縣城裏的百姓, point cut 就至關於 老王所作的指控, 即兇手是個男性, 身高約七尺五寸, 而 advice 則是縣令所下的命令 凡是發現有身高七尺五寸的男性, 都要抓過來審問.
爲何能夠這樣類比呢?
兇手是個男性, 身高約七尺五寸
, 把符合條件的人抓起來. 在這裏 兇手是個男性, 身高約七尺五寸
就是一個修飾謂語, 它限定了兇手的範圍, 知足此修飾規則的百姓都是嫌疑人, 都須要抓起來審問.抓過來審問
這個動做就是對做用於那些知足 男性, 身高約七尺五寸
的爪哇的小縣城裏的百姓
.或則咱們也能夠從語法的角度來簡單類比一下. 咱們在學英語時, 常常會接觸什麼 定語
, 被動句
之類的概念, 那麼能夠作一個不嚴謹的類比, 即 joinpoint
能夠認爲是一個 賓語
, 而 pointcut
則能夠類比爲修飾 joinpoint
的定語, 那麼整個 aspect
就能夠描述爲: 知足 pointcut 規則的 joinpoint 會被添加相應的 advice 操做.
@AspectJ 是一種使用 Java 註解來實現 AOP 的編碼風格.
@AspectJ 風格的 AOP 是 AspectJ Project 在 AspectJ 5 中引入的, 而且 Spring 也支持@AspectJ 的 AOP 風格.
@AspectJ 能夠以 XML 的方式或以註解的方式來使能, 而且不論以哪一種方式使能@ASpectJ, 咱們都必須保證 aspectjweaver.jar 在 classpath 中.
@Configuration @EnableAspectJAutoProxy public class AppConfig { }
<aop:aspectj-autoproxy/>
當使用註解 @Aspect 標註一個 Bean 後, 那麼 Spring 框架會自動收集這些 Bean, 並添加到 Spring AOP 中, 例如:
@Component @Aspect public class MyTest { }
注意, 僅僅使用@Aspect 註解, 並不能將一個 Java 對象轉換爲 Bean, 所以咱們還須要使用相似 @Component 之類的註解.
注意, 若是一個 類被@Aspect 標註, 則這個類就不能是其餘 aspect 的 **advised object** 了, 由於使用 @Aspect 後, 這個類就會被排除在 auto-proxying 機制以外.
一個 pointcut 的聲明由兩部分組成:
在@AspectJ 風格的 AOP 中, 咱們使用一個方法來描述 pointcut, 即:
@Pointcut("execution(* com.xys.service.UserService.*(..))") // 切點表達式 private void dataAccessOperation() {} // 切點前面
這個方法必須無返回值.
這個方法自己就是 pointcut signature, pointcut 表達式使用@Pointcut 註解指定.
上面咱們簡單地定義了一個 pointcut, 這個 pointcut 所描述的是: 匹配全部在包 com.xys.service.UserService 下的全部方法的執行.
AspectJ5 的切點表達式由標誌符(designator)和操做參數組成. 如 "execution(* greetTo(..))" 的切點表達式, execution 就是 標誌符, 而圓括號裏的 *** greetTo(..)** 就是操做參數
匹配 join point 的執行, 例如 "execution(* hello(..))" 表示匹配全部目標類中的 hello() 方法. 這個是最基本的 pointcut 標誌符.
匹配特定包下的全部 join point, 例如 within(com.xys.*)
表示 com.xys 包中的全部鏈接點, 即包中的全部類的全部方法. 而 within(com.xys.service.*Service)
表示在 com.xys.service 包中全部以 Service 結尾的類的全部的鏈接點.
this 的做用是匹配一個 bean, 這個 bean(Spring AOP proxy) 是一個給定類型的實例(instance of). 而 target 匹配的是一個目標對象(target object, 即須要織入 advice 的原始的類), 此對象是一個給定類型的實例(instance of).
匹配 bean 名字爲指定值的 bean 下的全部方法, 例如:
bean(*Service) // 匹配名字後綴爲 Service 的 bean 下的全部方法 bean(myService) // 匹配名字爲 myService 的 bean 下的全部方法
匹配參數知足要求的的方法.
例如:
@Pointcut("within(com.xys.demo2.*)") public void pointcut2() { } @Before(value = "pointcut2() && args(name)") public void doSomething(String name) { logger.info("---page: {}---", name); }
@Service public class NormalService { private Logger logger = LoggerFactory.getLogger(getClass()); public void someMethod() { logger.info("---NormalService: someMethod invoked---"); } public String test(String name) { logger.info("---NormalService: test invoked---"); return "服務一切正常"; } }
當 NormalService.test 執行時, 則 advice doSomething
就會執行, test 方法的參數 name 就會傳遞到 doSomething
中.
經常使用例子:
// 匹配只有一個參數 name 的方法 @Before(value = "aspectMethod() && args(name)") public void doSomething(String name) { } // 匹配第一個參數爲 name 的方法 @Before(value = "aspectMethod() && args(name, ..)") public void doSomething(String name) { } // 匹配第二個參數爲 name 的方法 Before(value = "aspectMethod() && args(*, name, ..)") public void doSomething(String name) { }
匹配由指定註解所標註的方法, 例如:
@Pointcut("@annotation(com.xys.demo1.AuthChecker)") public void pointcut() { }
則匹配由註解 AuthChecker
所標註的方法.
// 匹配指定包中的全部的方法 execution(* com.xys.service.*(..)) // 匹配當前包中的指定類的全部方法 execution(* UserService.*(..)) // 匹配指定包中的全部 public 方法 execution(public * com.xys.service.*(..)) // 匹配指定包中的全部 public 方法, 而且返回值是 int 類型的方法 execution(public int com.xys.service.*(..)) // 匹配指定包中的全部 public 方法, 而且第一個參數是 String, 返回值是 int 類型的方法 execution(public int com.xys.service.*(String name, ..))
// 匹配指定包中的全部的方法, 但不包括子包 within(com.xys.service.*) // 匹配指定包中的全部的方法, 包括子包 within(com.xys.service..*) // 匹配當前包中的指定類中的方法 within(UserService) // 匹配一個接口的全部實現類中的實現的方法 within(UserDao+)
// 匹配以指定名字結尾的 Bean 中的全部方法 bean(*Service)
// 匹配以 Service 或 ServiceImpl 結尾的 bean bean(*Service || *ServiceImpl) // 匹配名字以 Service 開頭, 而且在包 com.xys.service 中的 bean bean(*Service) && within(com.xys.service.*)
advice 是和一個 pointcut 表達式關聯在一塊兒的, 而且會在匹配的 join point 的方法執行的前/後/周圍 運行. pointcut 表達式能夠是簡單的一個 pointcut 名字的引用, 或者是完整的 pointcut 表達式
.
下面咱們以幾個簡單的 advice 爲例子, 來看一下一個 advice 是如何聲明的.
/** * @author xiongyongshun * @version 1.0 * @created 16/9/9 13:13 */ @Component @Aspect public class BeforeAspectTest { // 定義一個 Pointcut, 使用 切點表達式函數 來描述對哪些 Join point 使用 advise. @Pointcut("execution(* com.xys.service.UserService.*(..))") public void dataAccessOperation() { } }
@Component @Aspect public class AdviseDefine { // 定義 advise @Before("com.xys.aspect.PointcutDefine.dataAccessOperation()") public void doBeforeAccessCheck(JoinPoint joinPoint) { System.out.println("*****Before advise, method: " + joinPoint.getSignature().toShortString() + " *****"); } }
這裏, @Before 引用了一個 pointcut, 即 "com.xys.aspect.PointcutDefine.dataAccessOperation()" 是一個 pointcut 的名字.
若是咱們在 advice 在內置 pointcut, 則能夠:
@Component @Aspect public class AdviseDefine { // 將 pointcut 和 advice 同時定義 @Before("within(com.xys.service..*)") public void doAccessCheck(JoinPoint joinPoint) { System.out.println("*****doAccessCheck, Before advise, method: " + joinPoint.getSignature().toShortString() + " *****"); } }
around advice 比較特別, 它能夠在一個方法的以前以前和以後添加不一樣的操做, 而且甚至能夠決定什麼時候, 如何, 是否調用匹配到的方法.
@Component @Aspect public class AdviseDefine { // 定義 advise @Around("com.xys.aspect.PointcutDefine.dataAccessOperation()") public Object doAroundAccessCheck(ProceedingJoinPoint pjp) throws Throwable { StopWatch stopWatch = new StopWatch(); stopWatch.start(); // 開始 Object retVal = pjp.proceed(); stopWatch.stop(); // 結束 System.out.println("invoke method: " + pjp.getSignature().getName() + ", elapsed time: " + stopWatch.getTotalTimeMillis()); return retVal; } }
around advice 和前面的 before advice 差很少, 只是咱們把註解 @Before 改成了 @Around 了.