AOP(Aspect-Oriented Programming)這個東西,名字與 OOP 僅差一個字母,其實它是對 OOP 編程方式的一種補充,並不是是取而代之。翻譯過來就是「面向方面編程」,可我更傾向於翻譯爲「面向切面編程」。它聽起有些的神祕,爲何呢?當你看完這篇文章的時候,就會知道,咱們作的很重要的工做就是去寫這個「切面」 。那麼什麼是「切面」呢?java
沒錯!就是用一把刀來切一坨面。注意,相對於面而言,咱們必定是橫着來切它,這簡稱爲「橫切」。能夠把一段代碼想象成一坨面,一樣也能夠用一把刀來橫切它,下面要作的就是如何去實現這把刀!程序員
須要澄清的是,這個概念不是由 Rod Johnson(老羅)提出的。其實很早之前就有了,目前最知名最強大的 Java 開源項目就是 AspectJ 了,然而它的前身是 AspectWerkz(該項目已經在 2005 年中止更新),這纔是 AOP 的老祖宗。老羅(一個頭發禿得和我老爸有一拼的天才)寫了一個叫作 Spring 框架,今後一炮走紅,成爲了 Spring 之父。他在本身的 IOC 的基礎之上,又實現了一套 AOP 的框架,後來彷彿發現本身愈來愈走進深淵裏,在不能自拔的時候,有人建議他仍是集成 AspectJ 吧,他在萬般無奈之下才接受了該建議。因而,咱們如今用得最多的想必就是 Spring + AspectJ 這種 AOP 框架了。正則表達式
那麼 AOP 究竟是什麼?如何去使用它?本文將逐步帶您進入 AOP 的世界,讓您感覺到史無前例的暢快!spring
不過在開始講解 AOP 以前,我想有必要回憶一下這段代碼:數據庫
1. 寫死代碼編程
先來一個接口:架構
public interface Greeting { void sayHello(String name); }
還有一個實現類:框架
public class GreetingImpl implements Greeting { @Override public void sayHello(String name) { before(); System.out.println("Hello! " + name); after(); } private void before() { System.out.println("Before"); } private void after() { System.out.println("After"); } }
before() 與 after() 方法寫死在 sayHello() 方法體中了,這樣的代碼的味道很是很差。若是哪位仁兄大量寫了這樣的代碼,確定要被你的架構師罵個夠嗆。ide
好比:咱們要統計每一個方法的執行時間,以對性能做出評估,那是否是要在每一個方法的一頭一尾都作點手腳呢?工具
再好比:咱們要寫一個 JDBC 程序,那是否是也要在方法的開頭去鏈接數據庫,方法的末尾去關閉數據庫鏈接呢?
這樣的代碼只會把程序員累死,把架構師氣死!
必定要想辦法對上面的代碼進行重構,首先給出三個解決方案:
2. 靜態代理
最簡單的解決方案就是使用靜態代理模式了,咱們單獨爲 GreetingImpl 這個類寫一個代理類:
public class GreetingProxy implements Greeting { private GreetingImpl greetingImpl; public GreetingProxy(GreetingImpl greetingImpl) { this.greetingImpl = greetingImpl; } @Override public void sayHello(String name) { before(); greetingImpl.sayHello(name); after(); } private void before() { System.out.println("Before"); } private void after() { System.out.println("After"); } }
就用這個 GreetingProxy 去代理 GreetingImpl,下面看看客戶端如何來調用:
public class Client { public static void main(String[] args) { Greeting greetingProxy = new GreetingProxy(new GreetingImpl()); greetingProxy.sayHello("Jack"); } }
這樣寫沒錯,可是有個問題,XxxProxy 這樣的類會愈來愈多,如何才能將這些代理類儘量減小呢?最好只有一個代理類。
這時咱們就須要使用 JDK 提供的動態代理了。
3. JDK 動態代理
public class JDKDynamicProxy implements InvocationHandler { private Object target; public JDKDynamicProxy(Object target) { this.target = target; } @SuppressWarnings("unchecked") public <T> T getProxy() { return (T) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), this ); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { before(); Object result = method.invoke(target, args); after(); return result; } private void before() { System.out.println("Before"); } private void after() { System.out.println("After"); } }
客戶端是這樣調用的:
public class Client { public static void main(String[] args) { Greeting greeting = new JDKDynamicProxy(new GreetingImpl()).getProxy(); greeting.sayHello("Jack"); } }
這樣全部的代理類都合併到動態代理類中了,但這樣作仍然存在一個問題:JDK 給咱們提供的動態代理只能代理接口,而不能代理沒有接口的類。有什麼方法能夠解決呢?
4. CGLib 動態代理
咱們使用開源的 CGLib 類庫能夠代理沒有接口的類,這樣就彌補了 JDK 的不足。CGLib 動態代理類是這樣玩的:
public class CGLibDynamicProxy implements MethodInterceptor { private static CGLibDynamicProxy instance = new CGLibDynamicProxy(); private CGLibDynamicProxy() { } public static CGLibDynamicProxy getInstance() { return instance; } @SuppressWarnings("unchecked") public <T> T getProxy(Class<T> cls) { return (T) Enhancer.create(cls, this); } @Override public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable { before(); Object result = proxy.invokeSuper(target, args); after(); return result; } private void before() { System.out.println("Before"); } private void after() { System.out.println("After"); } }
以上代碼中了 Singleton 模式,那麼客戶端調用也更加輕鬆了:
public class Client { public static void main(String[] args) { Greeting greeting = CGLibDynamicProxy.getInstance().getProxy(GreetingImpl.class); greeting.sayHello("Jack"); } }
到此爲止,咱們能作的都作了,問題彷佛所有都解決了。但事情總不會那麼完美,而咱們必定要追求完美!
老羅搞出了一個 AOP 框架,可否作到完美而優雅呢?請你們繼續往下看吧!
5. Spring AOP:前置加強、後置加強、環繞加強(編程式)
在 Spring AOP 的世界裏,與 AOP 相關的術語實在太多,每每也是咱們的「攔路虎」,無論是看那本書或是技術文檔,在開頭都要將這些術語逐個灌輸給讀者。我想這徹底是在嚇唬人了,其實沒那麼複雜的,你們放輕鬆一點。
咱們上面例子中提到的 before() 方法,在 Spring AOP 裏就叫 Before Advice(前置加強)。有些人將 Advice 直譯爲「通知」,我想這是不太合適的,由於它根本就沒有「通知」的含義,而是對原有代碼功能的一種「加強」。再說,CGLib 中也有一個 Enhancer 類,它就是一個加強類。
此外,像 after() 這樣的方法就叫 After Advice(後置加強),由於它放在後面來加強代碼的功能。
若是能把 before() 與 after() 合併在一塊兒,那就叫 Around Advice(環繞加強),就像漢堡同樣,中間夾一根火腿。
這三個概念是否是輕鬆地理解了呢?若是是,那就繼續吧!
咱們下面要作的就是去實現這些所謂的「加強類」,讓他們橫切到代碼中,而不是將這些寫死在代碼中。
先來一個前置加強類吧:
public class GreetingBeforeAdvice implements MethodBeforeAdvice { @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println("Before"); } }
注意:這個類實現了 org.springframework.aop.MethodBeforeAdvice 接口,咱們將須要加強的代碼放入其中。
再來一個後置加強類吧:
public class GreetingAfterAdvice implements AfterReturningAdvice { @Override public void afterReturning(Object result, Method method, Object[] args, Object target) throws Throwable { System.out.println("After"); } }
相似地,這個類實現了 org.springframework.aop.AfterReturningAdvice 接口。
最後用一個客戶端來把它們集成起來,看看如何調用吧:
public class Client { public static void main(String[] args) { ProxyFactory proxyFactory = new ProxyFactory(); // 建立代理工廠 proxyFactory.setTarget(new GreetingImpl()); // 射入目標類對象 proxyFactory.addAdvice(new GreetingBeforeAdvice()); // 添加前置加強 proxyFactory.addAdvice(new GreetingAfterAdvice()); // 添加後置加強 Greeting greeting = (Greeting) proxyFactory.getProxy(); // 從代理工廠中獲取代理 greeting.sayHello("Jack"); // 調用代理的方法 } }
請仔細閱讀以上代碼及其註釋,您會發現,其實 Spring AOP 仍是挺簡單的,對嗎?
固然,咱們徹底能夠只定義一個加強類,讓它同時實現 MethodBeforeAdvice 與 AfterReturningAdvice 這兩個接口,以下:
public class GreetingBeforeAndAfterAdvice implements MethodBeforeAdvice, AfterReturningAdvice { @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println("Before"); } @Override public void afterReturning(Object result, Method method, Object[] args, Object target) throws Throwable { System.out.println("After"); } }
這樣咱們只須要使用一行代碼,同時就能夠添加前置與後置加強:
proxyFactory.addAdvice(new GreetingBeforeAndAfterAdvice());
剛纔有提到「環繞加強」,其實這個東西能夠把「前置加強」與「後置加強」的功能給合併起來,無需讓咱們同時實現以上兩個接口。
public class GreetingAroundAdvice implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { before(); Object result = invocation.proceed(); after(); return result; } private void before() { System.out.println("Before"); } private void after() { System.out.println("After"); } }
環繞加強類須要實現 org.aopalliance.intercept.MethodInterceptor 接口。注意,這個接口不是 Spring 提供的,它是 AOP 聯盟(一個很牛逼的聯盟)寫的,Spring 只是借用了它。
在客戶端中一樣也須要將該加強類的對象添加到代理工廠中:
proxyFactory.addAdvice(new GreetingAroundAdvice());
好了,這就是 Spring AOP 的基本用法,但這只是「編程式」而已。Spring AOP 若是隻是這樣,那就太傻逼了,它曾經也是一度宣傳用 Spring 配置文件的方式來定義 Bean 對象,把代碼中的 new 操做所有解脫出來。
6. Spring AOP:前置加強、後置加強、環繞加強(聲明式)
先看 Spring 配置文件是如何寫的吧:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 掃描指定包(將 @Component 註解的類自動定義爲 Spring Bean) --> <context:component-scan base-package="aop.demo"/> <!-- 配置一個代理 --> <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="interfaces" value="aop.Greeting"/> <!-- 須要代理的接口 --> <property name="target" ref="greetingImpl"/> <!-- 接口實現類 --> <property name="interceptorNames"> <!-- 攔截器名稱(也就是加強類名稱,Spring Bean 的 id) --> <list> <value>greetingAroundAdvice</value> </list> </property> </bean> </beans>
必定要閱讀以上代碼的註釋,其實使用 ProxyFactoryBean 就能夠取代前面的 ProxyFactory,其實它們倆就一回事兒。我認爲 interceptorNames 應該更名爲 adviceNames 或許會更容易讓人理解,不就是往這個屬性裏面添加加強類嗎?
此外,若是隻有一個加強類,可使用如下方法來簡化:
... <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="interfaces" value="aop.Greeting"/> <property name="target" ref="greetingImpl"/> <property name="interceptorNames" value="greetingAroundAdvice"/> <!-- 注意這行配置 --> </bean> ...
還須要注意的是,這裏使用了 Spring 2.5+ 的特性「Bean 掃描」,這樣咱們就無需在 Spring 配置文件裏不斷地定義 <bean id=」xxx」/> 了,從而解脫了咱們的雙手。
看看這是有多麼的簡單:
@Component public class GreetingImpl implements Greeting { ... }
@Component public class GreetingAroundAdvice implements MethodInterceptor { ... }
最後看看客戶端吧:
public class Client { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("aop/demo/spring.xml"); // 獲取 Spring Context Greeting greeting = (Greeting) context.getBean("greetingProxy"); // 從 Context 中根據 id 獲取 Bean 對象(其實就是一個代理) greeting.sayHello("Jack"); // 調用代理的方法 } }
代碼量確實少了,咱們將配置性的代碼放入配置文件,這樣也有助於後期維護。更重要的是,代碼只關注於業務邏輯,而將配置放入文件中。這是一條最佳實踐!
除了上面提到的那三類加強之外,其實還有兩類加強也須要了解一下,關鍵的時候您要能想獲得它們才行。
7. Spring AOP:拋出加強
程序報錯,拋出異常了,通常的作法是打印到控制檯或日誌文件中,這樣不少地方都得去處理,有沒有一個一勞永逸的方法呢?那就是 Throws Advice(拋出加強),它確實很強,不信你就繼續往下看:
@Component public class GreetingImpl implements Greeting { @Override public void sayHello(String name) { System.out.println("Hello! " + name); throw new RuntimeException("Error"); // 故意拋出一個異常,看看異常信息可否被攔截到 } }
下面是拋出加強類的代碼:
@Component public class GreetingThrowAdvice implements ThrowsAdvice { public void afterThrowing(Method method, Object[] args, Object target, Exception e) { System.out.println("---------- Throw Exception ----------"); System.out.println("Target Class: " + target.getClass().getName()); System.out.println("Method Name: " + method.getName()); System.out.println("Exception Message: " + e.getMessage()); System.out.println("-------------------------------------"); } }
拋出加強類須要實現 org.springframework.aop.ThrowsAdvice 接口,在接口方法中可獲取方法、參數、目標對象、異常對象等信息。咱們能夠把這些信息統一寫入到日誌中,固然也能夠持久化到數據庫中。
這個功能確實太棒了!但還有一個更厲害的加強。若是某個類實現了 A 接口,但沒有實現 B 接口,那麼該類能夠調用 B 接口的方法嗎?若是您沒有看到下面的內容,必定不敢相信原來這是可行的!
8. Spring AOP:引入加強
以上提到的都是對方法的加強,那可否對類進行加強呢?用 AOP 的行話來說,對方法的加強叫作 Weaving(織入),而對類的加強叫作 Introduction(引入)。而 Introduction Advice(引入加強)就是對類的功能加強,它也是 Spring AOP 提供的最後一種加強。建議您一開始千萬不要去看《Spring Reference》,不然您必定會後悔的。由於當您看了如下的代碼示例後,必定會完全明白什麼纔是引入加強。
定義了一個新接口 Apology(道歉):
public interface Apology { void saySorry(String name); }
但我不想在代碼中讓 GreetingImpl 直接去實現這個接口,我想在程序運行的時候動態地實現它。由於假如我實現了這個接口,那麼我就必定要改寫 GreetingImpl 這個類,關鍵是我不想改它,或許在真實場景中,這個類有1萬行代碼,我實在是不敢動了。因而,我須要藉助 Spring 的引入加強。這個有點意思了!
@Component public class GreetingIntroAdvice extends DelegatingIntroductionInterceptor implements Apology { @Override public Object invoke(MethodInvocation invocation) throws Throwable { return super.invoke(invocation); } @Override public void saySorry(String name) { System.out.println("Sorry! " + name); } }
以上定義了一個引入加強類,擴展了 org.springframework.aop.support.DelegatingIntroductionInterceptor 類,同時也實現了新定義的 Apology 接口。在類中首先覆蓋了父類的 invoke() 方法,而後實現了 Apology 接口的方法。我就是想用這個加強類去豐富 GreetingImpl 類的功能,那麼這個 GreetingImpl 類無需直接實現 Apology 接口,就能夠在程序運行的時候調用 Apology 接口的方法了。這簡直是太神奇的!
看看是如何配置的吧:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="aop.demo"/> <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="interfaces" value="aop.demo.Apology"/> <!-- 須要動態實現的接口 --> <property name="target" ref="greetingImpl"/> <!-- 目標類 --> <property name="interceptorNames" value="greetingIntroAdvice"/> <!-- 引入加強 --> <property name="proxyTargetClass" value="true"/> <!-- 代理目標類(默認爲 false,代理接口) --> </bean> </beans>
須要注意 proxyTargetClass 屬性,它代表是否代理目標類,默認爲 false,也就是代理接口了,此時 Spring 就用 JDK 動態代理。若是爲 true,那麼 Spring 就用 CGLib 動態代理。這簡直就是太方便了!Spring 封裝了這一切,讓程序員不在關心那麼多的細節。咱們要向老羅同志致敬,您是咱們心中永遠的 idol!
當您看完下面的客戶端代碼,必定會徹底明白以上的這一切:
public class Client { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("aop/demo/spring.xml"); GreetingImpl greetingImpl = (GreetingImpl) context.getBean("greetingProxy"); // 注意:轉型爲目標類,而並不是它的 Greeting 接口 greetingImpl.sayHello("Jack"); Apology apology = (Apology) greetingImpl; // 將目標類強制向上轉型爲 Apology 接口(這是引入加強給咱們帶來的特性,也就是「接口動態實現」功能) apology.saySorry("Jack"); } }
沒想到 saySorry() 方法原來是能夠被 greetingImpl 對象來直接調用的,只需將其強制轉換爲該接口便可。
從寫死代碼,到使用代理;從編程式 Spring AOP 到聲明式 Spring AOP。一切都朝着簡單實用主義的方向在發展。沿着 Spring AOP 的方向,Rod Johnson(老羅)花了很多心思,都是爲了讓咱們使用 Spring 框架時不會感覺到麻煩,但事實卻並不是如此。那麼,後來老羅究竟對 Spring AOP 作了哪些改進呢?
9. Spring AOP:切面
以前談到的 AOP 框架其實能夠將它理解爲一個攔截器框架,但這個攔截器彷佛很是武斷。好比說,若是它攔截了一個類,那麼它就攔截了這個類中全部的方法。相似地,當咱們在使用動態代理的時候,其實也遇到了這個問題。須要在代碼中對所攔截的方法名加以判斷,才能過濾出咱們須要攔截的方法,想一想這種作法確實不太優雅。在大量的真實項目中,彷佛咱們只須要攔截特定的方法就好了,不必攔截全部的方法。因而,老羅同志藉助了 AOP 的一個很重要的工具,Advisor(切面),來解決這個問題。它也是 AOP 中的核心!是咱們關注的重點!
也就是說,咱們能夠經過切面,將加強類與攔截匹配條件組合在一塊兒,而後將這個切面配置到 ProxyFactory 中,從而生成代理。
這裏提到這個「攔截匹配條件」在 AOP 中就叫作 Pointcut(切點),其實說白了就是一個基於表達式的攔截條件罷了。
概括一下,Advisor(切面)封裝了 Advice(加強)與 Pointcut(切點 )。當您理解了這句話後,就往下看吧。
我在 GreetingImpl 類中故意增長了兩個方法,都以「good」開頭。下面要作的就是攔截這兩個新增的方法,而對 sayHello() 方法不做攔截。
@Component public class GreetingImpl implements Greeting { @Override public void sayHello(String name) { System.out.println("Hello! " + name); } public void goodMorning(String name) { System.out.println("Good Morning! " + name); } public void goodNight(String name) { System.out.println("Good Night! " + name); } }
在 Spring AOP 中,老羅已經給咱們提供了許多切面類了,這些切面類我我的感受最好用的就是基於正則表達式的切面類。看看您就明白了:
<?xml version="1.0" encoding="UTF-8"?> <beans ..."> <context:component-scan base-package="aop.demo"/> <!-- 配置一個切面 --> <bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="advice" ref="greetingAroundAdvice"/> <!-- 加強 --> <property name="pattern" value="aop.demo.GreetingImpl.good.*"/> <!-- 切點(正則表達式) --> </bean> <!-- 配置一個代理 --> <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target" ref="greetingImpl"/> <!-- 目標類 --> <property name="interceptorNames" value="greetingAdvisor"/> <!-- 切面 --> <property name="proxyTargetClass" value="true"/> <!-- 代理目標類 --> </bean> </beans>
注意以上代理對象的配置中的 interceptorNames,它再也不是一個加強,而是一個切面,由於已經將加強封裝到該切面中了。此外,切面還定義了一個切點(正則表達式),其目的是爲了只將知足切點匹配條件的方法進行攔截。
須要強調的是,這裏的切點表達式是基於正則表達式的。示例中的「aop.demo.GreetingImpl.good.*」表達式後面的「.*」表示匹配全部字符,翻譯過來就是「匹配 aop.demo.GreetingImpl 類中以 good 開頭的方法」。
除了 RegexpMethodPointcutAdvisor 之外,在 Spring AOP 中還提供了幾個切面類,好比:
總的來講,讓用戶去配置一個或少數幾個代理,彷佛還能夠接受,但隨着項目的擴大,代理配置就會愈來愈多,配置的重複勞動就多了,麻煩不說,還很容易出錯。可否讓 Spring 框架爲咱們自動生成代理呢?
10. Spring AOP:自動代理(掃描 Bean 名稱)
Spring AOP 提供了一個可根據 Bean 名稱來自動生成代理的工具,它就是 BeanNameAutoProxyCreator。是這樣配置的:
<?xml version="1.0" encoding="UTF-8"?> <beans ...> ... <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> <property name="beanNames" value="*Impl"/> <!-- 只爲後綴是「Impl」的 Bean 生成代理 --> <property name="interceptorNames" value="greetingAroundAdvice"/> <!-- 加強 --> <property name="optimize" value="true"/> <!-- 是否對代理生成策略進行優化 --> </bean> </beans>
以上使用 BeanNameAutoProxyCreator 只爲後綴爲「Impl」的 Bean 生成代理。須要注意的是,這個地方咱們不能定義代理接口,也就是 interfaces 屬性,由於咱們根本就不知道這些 Bean 到底實現了多少接口。此時不能代理接口,而只能代理類。因此這裏提供了一個新的配置項,它就是 optimize。若爲 true 時,則可對代理生成策略進行優化(默認是 false 的)。也就是說,若是該類有接口,就代理接口(使用 JDK 動態代理);若是沒有接口,就代理類(使用 CGLib 動態代理)。而並不是像以前使用的 proxyTargetClass 屬性那樣,強制代理類,而不考慮代理接口的方式。可見 Spring AOP 確實爲咱們提供了不少很好地服務!
既然 CGLib 能夠代理任何的類了,那爲何還要用 JDK 的動態代理呢?確定您會這樣問。
根據多年來實際項目經驗得知:CGLib 建立代理的速度比較慢,但建立代理後運行的速度卻很是快,而 JDK 動態代理正好相反。若是在運行的時候不斷地用 CGLib 去建立代理,系統的性能會大打折扣,因此建議通常在系統初始化的時候用 CGLib 去建立代理,並放入 Spring 的 ApplicationContext 中以備後用。
以上這個例子只能匹配目標類,而不能進一步匹配其中指定的方法,要匹配方法,就要考慮使用切面與切點了。Spring AOP 基於切面也提供了一個自動代理生成器:DefaultAdvisorAutoProxyCreator。
11. Spring AOP:自動代理(掃描切面配置)
爲了匹配目標類中的指定方法,咱們仍然須要在 Spring 中配置切面與切點:
<?xml version="1.0" encoding="UTF-8"?> <beans ...> ... <bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="pattern" value="aop.demo.GreetingImpl.good.*"/> <property name="advice" ref="greetingAroundAdvice"/> </bean> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"> <property name="optimize" value="true"/> </bean> </beans>
這裏無需再配置代理了,由於代理將會由 DefaultAdvisorAutoProxyCreator 自動生成。也就是說,這個類能夠掃描全部的切面類,併爲其自動生成代理。
看來無論怎樣簡化,老羅始終解決不了切面的配置,這件繁重的手工勞動。在 Spring 配置文件中,仍然會存在大量的切面配置。然而在有不少狀況下 Spring AOP 所提供的切面類真的不太夠用了,好比:想攔截指定註解的方法,咱們就必須擴展 DefaultPointcutAdvisor 類,自定義一個切面類,而後在 Spring 配置文件中進行切面配置。不作不知道,作了您就知道至關麻煩了。
老羅的解決方案彷佛已經掉進了切面類的深淵,這還真是所謂的「面向切面編程」了,最重要的是切面,最麻煩的也是切面。
必需要把切面配置給簡化掉,Spring 纔能有所突破!
神同樣的老羅總算認識到了這一點,接受了網友們的建議,集成了 AspectJ,同時也保留了以上提到的切面與代理配置方式(爲了兼容老的項目,更爲了維護本身的面子)。將 Spring 與 AspectJ 集成與直接使用 AspectJ 是不一樣的,咱們不須要定義 AspectJ 類(它是擴展了 Java 語法的一種新的語言,還須要特定的編譯器),只須要使用 AspectJ 切點表達式便可(它是比正則表達式更加友好的表現形式)。
12. Spring + AspectJ(基於註解:經過 AspectJ execution 表達式攔截方法)
下面以一個最簡單的例子,實現以前提到的環繞加強。先定義一個 Aspect 切面類:
@Aspect @Component public class GreetingAspect { @Around("execution(* aop.demo.GreetingImpl.*(..))") public Object around(ProceedingJoinPoint pjp) throws Throwable { before(); Object result = pjp.proceed(); after(); return result; } private void before() { System.out.println("Before"); } private void after() { System.out.println("After"); } }
注意:類上面標註的 @Aspect 註解,這代表該類是一個 Aspect(其實就是 Advisor)。該類無需實現任何的接口,只需定義一個方法(方法叫什麼名字都無所謂),只需在方法上標註 @Around 註解,在註解中使用了 AspectJ 切點表達式。方法的參數中包括一個 ProceedingJoinPoint 對象,它在 AOP 中稱爲 Joinpoint(鏈接點),能夠經過該對象獲取方法的任何信息,例如:方法名、參數等。
下面重點來分析一下這個切點表達式:
execution(* aop.demo.GreetingImpl.*(..))
是否是比正則表達式的可讀性更強呢?若是想匹配指定的方法,只需將第二個「*」改成指定的方法名稱便可。
如何配置呢?看看是有多簡單吧:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <context:component-scan base-package="aop.demo"/> <aop:aspectj-autoproxy proxy-target-class="true"/> </beans>
兩行配置就好了,不須要配置大量的代理,更不須要配置大量的切面,真是太棒了!須要注意的是 proxy-target-class=」true」 屬性,它的默認值是 false,默認只能代理接口(使用 JDK 動態代理),當爲 true 時,才能代理目標類(使用 CGLib 動態代理)。
Spring 與 AspectJ 結合的威力遠遠不止這些,咱們來點時尚的吧,攔截指定註解的方法怎麼樣?
13. Spring + AspectJ(基於註解:經過 AspectJ @annotation 表達式攔截方法)
爲了攔截指定的註解的方法,咱們首先須要來自定義一個註解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Tag { }
以上定義了一個 @Tag 註解,此註解可標註在方法上,在運行時生效。
只需將前面的 Aspect 類的切點表達式稍做改動:
@Aspect @Component public class GreetingAspect { @Around("@annotation(aop.demo.Tag)") public Object around(ProceedingJoinPoint pjp) throws Throwable { ... } ... }
此次使用了 @annotation() 表達式,只需在括號內定義須要攔截的註解名稱便可。
直接將 @Tag 註解定義在您想要攔截的方法上,就這麼簡單:
@Component public class GreetingImpl implements Greeting { @Tag @Override public void sayHello(String name) { System.out.println("Hello! " + name); } }
以上示例中只有一個方法,若是有多個方法,咱們只想攔截其中某些時,這種解決方案會更加有價值。
除了 @Around 註解外,其實還有幾個相關的註解,稍微概括一下吧:
此外還有一個 @AfterReturning(返回後加強),也可理解爲 Finally 加強,至關於 finally 語句,它是在方法結束後執行的,也就說說,它比 @After 還要晚一些。
最後一個 @DeclareParents 居然就是引入加強!爲何不叫作 @Introduction 呢?我也不知道爲何,但它乾的活就是引入加強。
14. Spring + AspectJ(引入加強)
爲了實現基於 AspectJ 的引入加強,咱們一樣須要定義一個 Aspect 類:
@Aspect @Component public class GreetingAspect { @DeclareParents(value = "aop.demo.GreetingImpl", defaultImpl = ApologyImpl.class) private Apology apology; }
只須要在 Aspect 類中定義一個須要引入加強的接口,它也就是運行時須要動態實現的接口。在這個接口上標註了 @DeclareParents 註解,該註解有兩個屬性:
咱們只須要對引入的接口提供一個默認實現類便可完成引入加強:
public class ApologyImpl implements Apology { @Override public void saySorry(String name) { System.out.println("Sorry! " + name); } }
以上這個實現會在運行時自動加強到 GreetingImpl 類中,也就是說,無需修改 GreetingImpl 類的代碼,讓它去實現 Apology 接口,咱們單獨爲該接口提供一個實現類(ApologyImpl),來作 GreetingImpl 想作的事情。
仍是用一個客戶端來嘗試一下吧:
public class Client { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("aop/demo/spring.xml"); Greeting greeting = (Greeting) context.getBean("greetingImpl"); greeting.sayHello("Jack"); Apology apology = (Apology) greeting; // 強制轉型爲 Apology 接口 apology.saySorry("Jack"); } }
從 Spring ApplicationContext 中獲取 greetingImpl 對象(實際上是個代理對象),可轉型爲本身靜態實現的接口 Greeting,也可轉型爲本身動態實現的接口 Apology,切換起來很是方便。
使用 AspectJ 的引入加強比原來的 Spring AOP 的引入加強更加方便了,並且還可面向接口編程(之前只能面向實現類),這也算一個很是巨大的突破。
這一切真的已經很是強大也很是靈活了!但仍然仍是有用戶不能嘗試這些特性,由於他們還在使用 JDK 1.4(根本就沒有註解這個東西),怎麼辦呢?沒想到 Spring AOP 爲那些遺留系統也考慮到了。
15. Spring + AspectJ(基於配置)
除了使用 @Aspect 註解來定義切面類之外,Spring AOP 也提供了基於配置的方式來定義切面類:
<?xml version="1.0" encoding="UTF-8"?> <beans ..."> <bean id="greetingImpl" class="aop.demo.GreetingImpl"/> <bean id="greetingAspect" class="aop.demo.GreetingAspect"/> <aop:config> <aop:aspect ref="greetingAspect"> <aop:around method="around" pointcut="execution(* aop.demo.GreetingImpl.*(..))"/> </aop:aspect> </aop:config> </beans>
使用 <aop:config> 元素來進行 AOP 配置,在其子元素中配置切面,包括加強類型、目標方法、切點等信息。
不管您是不能使用註解,仍是不肯意使用註解,Spring AOP 都能爲您提供全方位的服務。
好了,我所知道的比較實用的 AOP 技術都在這裏了,固然還有一些更爲高級的特性,因爲我的精力有限,這裏就再也不深刻了。
仍是依照慣例,給一張牛逼的高清無碼思惟導圖,總結一下以上各個知識點:
再來一張表格,總結一下各種加強類型所對應的解決方案:
加強類型 | 基於 AOP 接口 | 基於 @Aspect | 基於 <aop:config> |
Before Advice(前置加強) | MethodBeforeAdvice | @Before | <aop:before> |
AfterAdvice(後置加強) | AfterReturningAdvice | @After | <aop:after> |
AroundAdvice(環繞加強) | MethodInterceptor | @Around | <aop:around> |
ThrowsAdvice(拋出加強 | ThrowsAdvice | @AfterThrowing | <aop:after-throwing> |
IntroductionAdvice(引入加強) | DelegatingIntroductionInterceptor | @DeclareParents | <aop:declare-parents> |
最後給一張 UML 類圖描述一下 Spring AOP 的總體架構: