AOP 那點事兒

今天我要和你們分享的是 AOP(Aspect-Oriented Programming)這個東西,名字與 OOP 僅差一個字母,其實它是對 OOP 編程方式的一種補充,並不是是取而代之。翻譯過來就是「面向方面編程」,可我更傾向於翻譯爲「面向切面編程」。它聽起有些的神祕,爲何呢?當你看完這篇文章的時候,就就知道,咱們作的很重要的工做就是去寫這個「切面」 。那麼什麼是「切面」呢? 程序員

沒錯!就是用一把刀來切一坨面。注意,相對於面而言,咱們必定是橫着來切它,這簡稱爲「橫切」。能夠把一段代碼想象成一坨面,一樣也能夠用一把刀來橫切它,下面要作的就是如何去實現這把刀! spring

須要澄清的是,這個概念不是由 Rod Johnson(老羅)提出的。其實很早之前就有了,目前最知名最強大的 Java 開源項目就是 AspectJ 了,然而它的前身是 AspectWerkz(該項目已經在 2005 年中止更新),這纔是 AOP 的老祖宗。老羅(一個頭發禿得和我老爸有一拼的天才)寫了一個叫作 Spring 框架,今後一炮走紅,成爲了 Spring 之父。他在本身的 IOC 的基礎之上,又實現了一套 AOP 的框架,後來彷彿發現本身愈來愈走進深淵裏,在不能自拔的時候,有人建議他仍是集成 AspectJ 吧,他在萬般無奈之下才接受了該建議。因而,咱們如今用得最多的想必就是 Spring + AspectJ 這種 AOP 框架了。 數據庫

那麼 AOP 究竟是什麼?如何去使用它?本文將逐步帶您進入 AOP 的世界,讓您感覺到史無前例的暢快! 編程

不過在開始講解 AOP 以前,我想有必要回憶一下這段代碼: 架構

1. 寫死代碼 框架

先來一個接口: ide

1 publicinterfaceGreeting {
2  
3     voidsayHello(String name);
4 }
還有一個實現類:
01 publicclassGreetingImplimplementsGreeting {
02  
03     @Override
04     publicvoidsayHello(String name) {
05         before();
06         System.out.println("Hello! "+ name);
07         after();
08     }
09  
10     privatevoidbefore() {
11         System.out.println("Before");
12     }
13  
14     privatevoidafter() {
15         System.out.println("After");
16     }
17 }

before() 與 after() 方法寫死在 sayHello() 方法體中了,這樣的代碼的味道很是很差。若是哪位仁兄大量寫了這樣的代碼,確定要被你的架構師罵個夠嗆。 性能

好比:咱們要統計每一個方法的執行時間,以對性能做出評估,那是否是要在每一個方法的一頭一尾都作點手腳呢? this

再好比:咱們要寫一個 JDBC 程序,那是否是也要在方法的開頭去鏈接數據庫,方法的末尾去關閉數據庫鏈接呢? spa

這樣的代碼只會把程序員累死,把架構師氣死!

必定要想辦法對上面的代碼進行重構,首先給出三個解決方案:

2. 靜態代理

最簡單的解決方案就是使用靜態代理模式了,咱們單獨爲 GreetingImpl 這個類寫一個代理類:

01 publicclassGreetingProxyimplementsGreeting {
02  
03     privateGreetingImpl greetingImpl;
04  
05     publicGreetingProxy(GreetingImpl greetingImpl) {
06         this.greetingImpl = greetingImpl;
07     }
08  
09     @Override
10     publicvoidsayHello(String name) {
11         before();
12         greetingImpl.sayHello(name);
13         after();
14     }
15  
16     privatevoidbefore() {
17         System.out.println("Before");
18     }
19  
20     privatevoidafter() {
21         System.out.println("After");
22     }
23 }
就用這個 GreetingProxy 去代理 GreetingImpl,下面看看客戶端如何來調用:
1 publicclassClient {
2  
3     publicstaticvoidmain(String[] args) {
4         Greeting greetingProxy =newGreetingProxy(newGreetingImpl());
5         greetingProxy.sayHello("Jack");
6     }
7 }

這樣寫沒錯,可是有個問題,XxxProxy 這樣的類會愈來愈多,如何才能將這些代理類儘量減小呢?最好只有一個代理類。

這時咱們就須要使用 JDK 提供的動態代理了。 

3. JDK 動態代理

01 publicclassJDKDynamicProxyimplementsInvocationHandler {
02  
03     privateObject target;
04  
05     publicJDKDynamicProxy(Object target) {
06         this.target = target;
07     }
08  
09     @SuppressWarnings("unchecked")
10     public<T> T getProxy() {
11         return(T) Proxy.newProxyInstance(
12             target.getClass().getClassLoader(),
13             target.getClass().getInterfaces(),
14             this
15         );
16     }
17  
18     @Override
19     publicObject invoke(Object proxy, Method method, Object[] args)throwsThrowable {
20         before();
21         Object result = method.invoke(target, args);
22         after();
23         returnresult;
24     }
25  
26     privatevoidbefore() {
27         System.out.println("Before");
28     }
29  
30     privatevoidafter() {
31         System.out.println("After");
32     }
33 }
客戶端是這樣調用的:
1 publicclassClient {
2  
3     publicstaticvoidmain(String[] args) {
4         Greeting greeting =newJDKDynamicProxy(newGreetingImpl()).getProxy();
5         greeting.sayHello("Jack");
6     }
7 }

這樣全部的代理類都合併到動態代理類中了,但這樣作仍然存在一個問題:JDK 給咱們提供的動態代理只能代理接口,而不能代理沒有接口的類。有什麼方法能夠解決呢?

4. CGLib 動態代理

咱們使用開源的 CGLib 類庫能夠代理沒有接口的類,這樣就彌補了 JDK 的不足。CGLib 動態代理類是這樣玩的:

01 publicclassCGLibDynamicProxyimplementsMethodInterceptor {
02  
03     privatestaticCGLibDynamicProxy instance =newCGLibDynamicProxy();
04  
05     privateCGLibDynamicProxy() {
06     }
07  
08     publicstaticCGLibDynamicProxy getInstance() {
09         returninstance;
10     }
11  
12     @SuppressWarnings("unchecked")
13     public<T> T getProxy(Class<T> cls) {
14         return(T) Enhancer.create(cls,this);
15     }
16  
17     @Override
18     publicObject intercept(Object target, Method method, Object[] args, MethodProxy proxy)throwsThrowable {
19         before();
20         Object result = proxy.invokeSuper(target, args);
21         after();
22         returnresult;
23     }
24  
25     privatevoidbefore() {
26         System.out.println("Before");
27     }
28  
29     privatevoidafter() {
30         System.out.println("After");
31     }
32 }
以上代碼中了 Singleton 模式,那麼客戶端調用也更加輕鬆了:
1 publicclassClient {
2  
3     publicstaticvoidmain(String[] args) {
4         Greeting greeting = CGLibDynamicProxy.getInstance().getProxy(GreetingImpl.class);
5         greeting.sayHello("Jack");
6     }
7 }

到此爲止,咱們能作的都作了,問題彷佛所有都解決了。但事情總不會那麼完美,而咱們必定要追求完美!

老羅搞出了一個 AOP 框架,可否作到完美而優雅呢?請你們繼續往下看吧!

5. Spring AOP:前置加強、後置加強、環繞加強(編程式)

在 Spring AOP 的世界裏,與 AOP 相關的術語實在太多,每每也是咱們的「攔路虎」,不論是看那本書或是技術文檔,在開頭都要將這些術語逐個灌輸給讀者。我想這徹底是在嚇唬人了,其實沒那麼複雜的,你們放輕鬆一點。

咱們上面例子中提到的 before() 方法,在 Spring AOP 裏就叫 Before Advice(前置加強)。有些人將 Advice 直譯爲「通知」,我想這是不太合適的,由於它根本就沒有「通知」的含義,而是對原有代碼功能的一種「加強」。再說,CGLib 中也有一個 Enhancer 類,它就是一個加強類。

此外,像 after() 這樣的方法就叫 After Advice(後置加強),由於它放在後面來加強代碼的功能

若是能把 before() 與 after() 合併在一塊兒,那就叫 Around Advice(環繞加強),就像漢堡同樣,中間夾一根火腿。

這三個概念是否是輕鬆地理解了呢?若是是,那就繼續吧!

咱們下面要作的就是去實現這些所謂的「加強類」,讓他們橫切到代碼中,而不是將這些寫死在代碼中。

先來一個前置加強類吧:

1 publicclassGreetingBeforeAdviceimplementsMethodBeforeAdvice {
2  
3     @Override
4     publicvoidbefore(Method method, Object[] args, Object target)throwsThrowable {
5         System.out.println("Before");
6     }
7 }

注意:這個類實現了 org.springframework.aop.MethodBeforeAdvice 接口,咱們將須要加強的代碼放入其中。

再來一個後置加強類吧:

1 publicclassGreetingAfterAdviceimplementsAfterReturningAdvice {
2  
3     @Override
4     publicvoidafterReturning(Object result, Method method, Object[] args, Object target)throwsThrowable {
5         System.out.println("After");
6     }
7 }

相似地,這個類實現了 org.springframework.aop.AfterReturningAdvice 接口。

最後用一個客戶端來把它們集成起來,看看如何調用吧:

01 publicclassClient {
02  
03     publicstaticvoidmain(String[] args) {
04         ProxyFactory proxyFactory =newProxyFactory();    // 建立代理工廠
05         proxyFactory.setTarget(newGreetingImpl());        // 射入目標類對象
06         proxyFactory.addAdvice(newGreetingBeforeAdvice());// 添加前置加強
07         proxyFactory.addAdvice(newGreetingAfterAdvice()); // 添加後置加強
08  
09         Greeting greeting = (Greeting) proxyFactory.getProxy();// 從代理工廠中獲取代理
10         greeting.sayHello("Jack");                             // 調用代理的方法
11     }
12 }

請仔細閱讀以上代碼及其註釋,您會發現,其實 Spring AOP 仍是挺簡單的,對嗎?

固然,咱們徹底能夠只定義一個加強類,讓它同時實現 MethodBeforeAdvice 與 AfterReturningAdvice 這兩個接口,以下:

01 publicclassGreetingBeforeAndAfterAdviceimplementsMethodBeforeAdvice, AfterReturningAdvice {
02  
03     @Override
04     publicvoidbefore(Method method, Object[] args, Object target)throwsThrowable {
05         System.out.println("Before");
06     }
07  
08     @Override
09     publicvoidafterReturning(Object result, Method method, Object[] args, Object target)throwsThrowable {
10         System.out.println("After");
11     }
12 }
這樣咱們只須要使用一行代碼,同時就能夠添加前置與後置加強:
1 proxyFactory.addAdvice(newGreetingBeforeAndAfterAdvice());

剛纔有提到「環繞加強」,其實這個東西能夠把「前置加強」與「後置加強」的功能給合併起來,無需讓咱們同時實現以上兩個接口。

01 publicclassGreetingAroundAdviceimplementsMethodInterceptor {
02  
03     @Override
04     publicObject invoke(MethodInvocation invocation)throwsThrowable {
05         before();
06         Object result = invocation.proceed();
07         after();
08         returnresult;
09     }
10  
11     privatevoidbefore() {
12         System.out.println("Before");
13     }
14  
15     privatevoidafter() {
16         System.out.println("After");
17     }
18 }

環繞加強類須要實現 org.aopalliance.intercept.MethodInterceptor 接口。注意,這個接口不是 Spring 提供的,它是 AOP 聯盟(一個很牛逼的聯盟)寫的,Spring 只是借用了它。

在客戶端中一樣也須要將該加強類的對象添加到代理工廠中:

1 proxyFactory.addAdvice(newGreetingAroundAdvice());

好了,這就是 Spring AOP 的基本用法,但這只是「編程式」而已。Spring AOP 若是隻是這樣,那就太傻逼了,它曾經也是一度宣傳用 Spring 配置文件的方式來定義 Bean 對象,把代碼中的 new 操做所有解脫出來。

6.   Spring AOP:前置加強、後置加強、環繞加強(聲明式)

先看 Spring 配置文件是如何寫的吧:

01 <?xmlversion="1.0"encoding="UTF-8"?>
02 <beansxmlns="http://www.springframework.org/schema/beans"
03        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04        xmlns:context="http://www.springframework.org/schema/context"
05        xsi:schemaLocation="http://www.springframework.org/schema/beans
06        http://www.springframework.org/schema/beans/spring-beans.xsd
07        http://www.springframework.org/schema/context
08        http://www.springframework.org/schema/context/spring-context.xsd">
09  
10     <!-- 掃描指定包(將 @Component 註解的類自動定義爲 Spring Bean) -->
11     <context:component-scanbase-package="aop.demo"/>
12  
13     <!-- 配置一個代理 -->
14     <beanid="greetingProxy"class="org.springframework.aop.framework.ProxyFactoryBean">
15         <propertyname="interfaces"value="aop.Greeting"/><!-- 須要代理的接口 -->
16         <propertyname="target"ref="greetingImpl"/>      <!-- 接口實現類 -->
17         <propertyname="interceptorNames">                <!-- 攔截器名稱(也就是加強類名稱,Spring Bean 的 id) -->
18             <list>
19                 <value>greetingAroundAdvice</value>
20             </list>
21         </property>
22     </bean>
23  
24 </beans>

必定要閱讀以上代碼的註釋,其實使用 ProxyFactoryBean 就能夠取代前面的 ProxyFactory,其實它們倆就一回事兒。我認爲 interceptorNames 應該更名爲 adviceNames 或許會更容易讓人理解,不就是往這個屬性裏面添加加強類嗎?

此外,若是隻有一個加強類,可使用如下方法來簡化:

1 ...
2  
3     <beanid="greetingProxy"class="org.springframework.aop.framework.ProxyFactoryBean">
4         <propertyname="interfaces"value="aop.Greeting"/>
5         <propertyname="target"ref="greetingImpl"/>
6         <propertyname="interceptorNames"value="greetingAroundAdvice"/><!-- 注意這行配置 -->
7     </bean>
8  
9 ...

還須要注意的是,這裏使用了 Spring 2.5+ 的特性「Bean 掃描」,這樣咱們就無需在 Spring 配置文件裏不斷地定義 <bean id="xxx" class="xxx"/> 了,從而解脫了咱們的雙手。

看看這是有多麼的簡單:

1 @Component
2 publicclassGreetingImplimplementsGreeting {
3  
4     ...
5 }
1 @Component
2 publicclassGreetingAroundAdviceimplementsMethodInterceptor {
3  
4     ...
5 }
最後看看客戶端吧:
1 publicclassClient {
2  
3     publicstaticvoidmain(String[] args) {
4         ApplicationContext context =newClassPathXmlApplicationContext("aop/demo/spring.xml");// 獲取 Spring Context
5         Greeting greeting = (Greeting) context.getBean("greetingProxy");                       // 從 Context 中根據 id 獲取 Bean 對象(其實就是一個代理)
6         greeting.sayHello("Jack");                                                             // 調用代理的方法
7     }
8 }

代碼量確實少了,咱們將配置性的代碼放入配置文件,這樣也有助於後期維護。更重要的是,代碼只關注於業務邏輯,而將配置放入文件中。這是一條最佳實踐!

除了上面提到的那三類加強之外,其實還有兩類加強也須要了解一下,關鍵的時候您要能想獲得它們才行。 

7. Spring AOP:拋出加強

程序報錯,拋出異常了,通常的作法是打印到控制檯或日誌文件中,這樣不少地方都得去處理,有沒有一個一勞永逸的方法呢?那就是 Throws Advice(拋出加強),它確實很強,不信你就繼續往下看:

01 @Component
02 publicclassGreetingImplimplementsGreeting {
03  
04     @Override
05     publicvoidsayHello(String name) {
06         System.out.println("Hello! "+ name);
07  
08         thrownewRuntimeException("Error");// 故意拋出一個異常,看看異常信息可否被攔截到
09     }
10 }

下面是拋出加強類的代碼:

01 @Component
02 publicclassGreetingThrowAdviceimplementsThrowsAdvice {
03  
04     publicvoidafterThrowing(Method method, Object[] args, Object target, Exception e) {
05         System.out.println("---------- Throw Exception ----------");
06         System.out.println("Target Class: "+ target.getClass().getName());
07         System.out.println("Method Name: "+ method.getName());
08         System.out.println("Exception Message: "+ e.getMessage());
09         System.out.println("-------------------------------------");
10     }
11 }

拋出加強類須要實現 org.springframework.aop.ThrowsAdvice 接口,在接口方法中可獲取方法、參數、目標對象、異常對象等信息。咱們能夠把這些信息統一寫入到日誌中,固然也能夠持久化到數據庫中。

這個功能確實太棒了!但還有一個更厲害的加強。若是某個類實現了 A 接口,但沒有實現 B 接口,那麼該類能夠調用 B 接口的方法嗎?若是您沒有看到下面的內容,必定不敢相信原來這是可行的!

8. Spring AOP:引入加強

以上提到的都是對方法的加強,那可否對類進行加強呢?用 AOP 的行話來說,對方法的加強叫作 Weaving(織入),而對類的加強叫作 Introduction(引入)。而 Introduction Advice(引入加強)就是對類的功能加強,它也是 Spring AOP 提供的最後一種加強。建議您一開始千萬不要去看《Spring Reference》,不然您必定會後悔的。由於當您看了如下的代碼示例後,必定會完全明白什麼纔是引入加強。

定義了一個新接口 Apology(道歉):

1 publicinterfaceApology {
2  
3     voidsaySorry(String name);
4 }

但我不想在代碼中讓 GreetingImpl 直接去實現這個接口,我想在程序運行的時候動態地實現它。由於假如我實現了這個接口,那麼我就必定要改寫 GreetingImpl 這個類,關鍵是我不想改它,或許在真實場景中,這個類有1萬行代碼,我實在是不敢動了。因而,我須要藉助 Spring 的引入加強。這個有點意思了!

01 @Component
02 publicclassGreetingIntroAdviceextendsDelegatingIntroductionInterceptorimplementsApology {
03  
04    @Override
05    publicObject invoke(MethodInvocation invocation)throwsThrowable {
06        returnsuper.invoke(invocation);
07    }
08  
09     @Override
10     publicvoidsaySorry(String name) {
11         System.out.println("Sorry! "+ name);
12     }
13 }

以上定義了一個引入加強類,擴展 了 org.springframework.aop.support.DelegatingIntroductionInterceptor 類,同時也實現了新定義的 Apology 接口。在類中首先覆蓋了父類的 invoke() 方法,而後實現了 Apology 接口的方法。我就是想用這個加強類去豐富 GreetingImpl 類的功能,那麼這個 GreetingImpl 類無需直接實現 Apology 接口,就能夠在程序運行的時候調用 Apology 接口的方法了。這簡直是太神奇的!

看看是如何配置的吧:

01 <?xmlversion="1.0"encoding="UTF-8"?>
02 <beansxmlns="http://www.springframework.org/schema/beans"
03        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04        xmlns:context="http://www.springframework.org/schema/context"
05        xsi:schemaLocation="http://www.springframework.org/schema/beans
06        http://www.springframework.org/schema/beans/spring-beans.xsd
07        http://www.springframework.org/schema/context
08        http://www.springframework.org/schema/context/spring-context.xsd">
09  
10     <context:component-scanbase-package="aop.demo"/>
11  
12     <beanid="greetingProxy"class="org.springframework.aop.framework.ProxyFactoryBean">
13         <propertyname="interfaces"value="aop.demo.Apology"/>         <!-- 須要動態實現的接口 -->
14         <propertyname="target"ref="greetingImpl"/>                   <!-- 目標類 -->
15         <propertyname="interceptorNames"value="greetingIntroAdvice"/><!-- 引入加強 -->
16         <propertyname="proxyTargetClass"value="true"/>               <!-- 代理目標類(默認爲 false,代理接口) -->
17     </bean>
18  
19 </beans>

須要注意 proxyTargetClass 屬性,它代表是否代理目標類,默認爲 false,也就是代理接口了,此時 Spring 就用 JDK 動態代理。若是爲 true,那麼 Spring 就用 CGLib 動態代理。這簡直就是太方便了!Spring 封裝了這一切,讓程序員不在關心那麼多的細節。咱們要向老羅同志致敬,您是咱們心中永遠的 idol!

當您看完下面的客戶端代碼,必定會徹底明白以上的這一切:

01 publicclassClient {
02  
03     publicstaticvoidmain(String[] args) {
04         ApplicationContext context =newClassPathXmlApplicationContext("aop/demo/spring.xml");
05         GreetingImpl greetingImpl = (GreetingImpl) context.getBean("greetingProxy");// 注意:轉型爲目標類,而並不是它的 Greeting 接口
06         greetingImpl.sayHello("Jack");
07  
08         Apology apology = (Apology) greetingImpl;// 將目標類強制向上轉型爲 Apology 接口(這是引入加強給咱們帶來的特性,也就是「接口動態實現」功能)
09         apology.saySorry("Jack");
10     }
11 }

沒想到 saySorry() 方法原來是能夠被 greetingImpl 對象來直接調用的,只需將其強制轉換爲該接口便可。

咱們再次感謝 Spring AOP,感謝老羅給咱們提供了這麼強大的特性!

其實,Spring AOP 還有不少精彩的地方,下一篇將介紹更多更有價值的 AOP 技術,讓你們獲得更多的收穫。

未完,待續...

相關文章
相關標籤/搜索