AOP那點事兒:面向切面編程
又是一個周末,剛給寶寶喂完牛奶,終于讓她睡著了。所以現(xiàn)在我才能騰出手來,坐在電腦面前給大家寫這篇文章。
今天我要和大家分享的是 AOP(Aspect-Oriented Programming)這個東西,名字與 OOP 僅差一個字母,其實它是對 OOP 編程方式的一種補充,并非是取而代之。翻譯過來就是“面向方面編程”,可我更傾向于翻譯為“面向切面編程”。它聽起有些的神秘,為什么呢?當(dāng)你看完這篇文章的時候,就就知道,我們做的很重要的工作就是去寫這個“切面”。那么什么是“切面”呢?
沒錯!就是用一把刀來切一坨面。注意,相對于面而言,我們一定是橫著來切它,這簡稱為“橫切”??梢园岩欢未a想象成一坨面,同樣也可以用一把刀來橫切它,下面要做的就是如何去實現(xiàn)這把刀!
需要澄清的是,這個概念不是由 Rod Johnson(老羅)提出的。其實很早以前就有了,目前最知名最強大的 Java 開源項目就是 AspectJ 了,然而它的前身是 AspectWerkz(該項目已經(jīng)在 2005 年停止更新),這才是 AOP 的老祖宗。老羅(一個頭發(fā)禿得和我老爸有一拼的天才)寫了一個叫做 Spring 框架,從此一炮走紅,成為了 Spring 之父。他在自己的 IOC 的基礎(chǔ)之上,又實現(xiàn)了一套 AOP 的框架,后來仿佛發(fā)現(xiàn)自己越來越走進(jìn)深淵里,在不能自拔的時候,有人建議他還是集成 AspectJ 吧,他在萬般無奈之下才接受了該建議。于是,我們現(xiàn)在用得最多的想必就是 Spring + AspectJ 這種 AOP 框架了。
那么 AOP 到底是什么?如何去使用它?本文將逐步帶您進(jìn)入 AOP 的世界,讓您感受到前所未有的暢快!
不過在開始講解 AOP 之前,我想有必要回憶一下這段代碼:
1. 寫死代碼
先來一個接口:
- public interface Greeting {
- void sayHello(String name);
- }
還有一個實現(xiàn)類:
- 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() 方法體中了,這樣的代碼的味道非常不好。如果哪位仁兄大量寫了這樣的代碼,肯定要被你的架構(gòu)師罵個夠嗆。
比如:我們要統(tǒng)計每個方法的執(zhí)行時間,以對性能作出評估,那是不是要在每個方法的一頭一尾都做點手腳呢?
再比如:我們要寫一個 JDBC 程序,那是不是也要在方法的開頭去連接數(shù)據(jù)庫,方法的末尾去關(guān)閉數(shù)據(jù)庫連接呢?
這樣的代碼只會把程序員累死,把架構(gòu)師氣死!
一定要想辦法對上面的代碼進(jìn)行重構(gòu),首先給出三個解決方案:
2. 靜態(tài)代理
最簡單的解決方案就是使用靜態(tài)代理模式了,我們單獨為 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,下面看看客戶端如何來調(diào)用:
- public class Client {
- public static void main(String[] args) {
- Greeting greetingProxy = new GreetingProxy(new GreetingImpl());
- greetingProxy.sayHello("Jack");
- }
- }
這樣寫沒錯,但是有個問題,XxxProxy 這樣的類會越來越多,如何才能將這些代理類盡可能減少呢?最好只有一個代理類。
這是我們就需要使用 JDK 提供的動態(tài)代理了。
3. JDK 動態(tài)代理
- 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");
- }
- }
客戶端是這樣調(diào)用的:
- public class Client {
- public static void main(String[] args) {
- Greeting greeting = new JDKDynamicProxy(new GreetingImpl()).getProxy();
- greeting.sayHello("Jack");
- }
- }
這樣所有的代理類都合并到動態(tài)代理類中了,但這樣做仍然存在一個問題:JDK 給我們提供的動態(tài)代理只能代理接口,而不能代理沒有接口的類。有什么方法可以解決呢?
4. CGLib 動態(tài)代理
我們使用開源的 CGLib 類庫可以代理沒有接口的類,這樣就彌補了 JDK 的不足。CGLib 動態(tài)代理類是這樣玩的:
- 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 模式,那么客戶端調(diào)用也更加輕松了:
- public class Client {
- public static void main(String[] args) {
- Greeting greeting = CGLibDynamicProxy.getInstance().getProxy(GreetingImpl.class);
- greeting.sayHello("Jack");
- }
- }
到此為止,我們能做的都做了,問題似乎全部都解決了。但事情總不會那么完美,而我們一定要追求完美!
老羅搞出了一個 AOP 框架,能否做到完美而優(yōu)雅呢?請大家繼續(xù)往下看吧!
#p#
5. Spring AOP:前置增強、后置增強、環(huán)繞增強(編程式)
在 Spring AOP 的世界里,與 AOP 相關(guān)的術(shù)語實在太多,往往也是我們的“攔路虎”,不管是看那本書或是技術(shù)文檔,在開頭都要將這些術(shù)語逐個灌輸給讀者。我想這完全是在嚇唬人了,其實沒那么復(fù)雜的,大家放輕松一點。
我們上面例子中提到的 before() 方法,在 Spring AOP 里就叫 Before Advice(前置增強)。有些人將 Advice 直譯為“通知”,我想這是不太合適的,因為它根本就沒有“通知”的含義,而是對原有代碼功能的一種“增強”。再說,CGLib 中也有一個 Enhancer 類,它就是一個增強類。
此外,像 after() 這樣的方法就叫 After Advice(后置增強),因為它放在后面來增強代碼的功能。
如果能把 before() 與 after() 合并在一起,那就叫 Around Advice(環(huán)繞增強),就像漢堡一樣,中間夾一根火腿。
這三個概念是不是輕松地理解了呢?如果是,那就繼續(xù)吧!
我們下面要做的就是去實現(xiàn)這些所謂的“增強類”,讓他們橫切到代碼中,而不是將這些寫死在代碼中。
先來一個前置增強類吧:
- public class GreetingBeforeAdvice implements MethodBeforeAdvice {
- @Override
- public void before(Method method, Object[] args, Object target) throws Throwable {
- System.out.println("Before");
- }
- }
注意:這個類實現(xiàn)了 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");
- }
- }
類似地,這個類實現(xiàn)了 org.springframework.aop.AfterReturningAdvice 接口。
最后用一個客戶端來把它們集成起來,看看如何調(diào)用吧:
- public class Client {
- public static void main(String[] args) {
- ProxyFactory proxyFactory = new ProxyFactory(); // 創(chuàng)建代理工廠
- proxyFactory.setTarget(new GreetingImpl()); // 射入目標(biāo)類對象
- proxyFactory.addAdvice(new GreetingBeforeAdvice()); // 添加前置增強
- proxyFactory.addAdvice(new GreetingAfterAdvice()); // 添加后置增強
- Greeting greeting = (Greeting) proxyFactory.getProxy(); // 從代理工廠中獲取代理
- greeting.sayHello("Jack"); // 調(diào)用代理的方法
- }
- }
請仔細(xì)閱讀以上代碼及其注釋,您會發(fā)現(xiàn),其實 Spring AOP 還是挺簡單的,對嗎?
當(dāng)然,我們完全可以只定義一個增強類,讓它同時實現(xiàn) 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());
剛才有提到“環(huán)繞增強”,其實這個東西可以把“前置增強”與“后置增強”的功能給合并起來,無需讓我們同時實現(xiàn)以上兩個接口。
- 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");
- }
- }
環(huán)繞增強類需要實現(xiàn) org.aopalliance.intercept.MethodInterceptor 接口。注意,這個接口不是 Spring 提供的,它是 AOP 聯(lián)盟(一個很牛逼的聯(lián)盟)寫的,Spring 只是借用了它。
在客戶端中同樣也需要將該增強類的對象添加到代理工廠中:
- proxyFactory.addAdvice(new GreetingAroundAdvice());
好了,這就是 Spring AOP 的基本用法,但這只是“編程式”而已。Spring AOP 如果只是這樣,那就太傻逼了,它曾經(jīng)也是一度宣傳用 Spring 配置文件的方式來定義 Bean 對象,把代碼中的 new 操作全部解脫出來。
6. Spring AOP:前置增強、后置增強、環(huán)繞增強(聲明式)
先看 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"/> <!-- 接口實現(xiàn)類 -->
- <property name="interceptorNames"> <!-- 攔截器名稱(也就是增強類名稱,Spring Bean 的 id) -->
- <list>
- <value>greetingAroundAdvice</value>
- </list>
- </property>
- </bean>
- </beans>
一定要閱讀以上代碼的注釋,其實使用 ProxyFactoryBean 就可以取代前面的 ProxyFactory,其實它們倆就一回事兒。我認(rèn)為 interceptorNames 應(yīng)該改名為 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" class="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 中根據(jù) id 獲取 Bean 對象(其實就是一個代理)
- greeting.sayHello("Jack"); // 調(diào)用代理的方法
- }
- }
代碼量確實少了,我們將配置性的代碼放入配置文件,這樣也有助于后期維護。更重要的是,代碼只關(guān)注于業(yè)務(wù)邏輯,而將配置放入文件中。這是一條最佳實踐!
除了上面提到的那三類增強以外,其實還有兩類增強也需要了解一下,關(guān)鍵的時候您要能想得到它們才行。
#p#
7. Spring AOP:拋出增強
程序報錯,拋出異常了,一般的做法是打印到控制臺或日志文件中,這樣很多地方都得去處理,有沒有一個一勞永逸的方法呢?那就是 Throws Advice(拋出增強),它確實很強,不信你就繼續(xù)往下看:
- @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("-------------------------------------");
- }
- }
拋出增強類需要實現(xiàn) org.springframework.aop.ThrowsAdvice 接口,在接口方法中可獲取方法、參數(shù)、目標(biāo)對象、異常對象等信息。我們可以把這些信息統(tǒng)一寫入到日志中,當(dāng)然也可以持久化到數(shù)據(jù)庫中。
這個功能確實太棒了!但還有一個更厲害的增強。如果某個類實現(xiàn)了 A 接口,但沒有實現(xiàn) B 接口,那么該類可以調(diào)用 B 接口的方法嗎?如果您沒有看到下面的內(nèi)容,一定不敢相信原來這是可行的!
8. Spring AOP:引入增強
以上提到的都是對方法的增強,那能否對類進(jìn)行增強呢?用 AOP 的行話來講,對方法的增強叫做 Weaving(織入),而對類的增強叫做 Introduction(引入)。而 Introduction Advice(引入增強)就是對類的功能增強,它也是 Spring AOP 提供的最后一種增強。建議您一開始千萬不要去看《Spring Reference》,否則您一定會后悔的。因為當(dāng)您看了以下的代碼示例后,一定會徹底明白什么才是引入增強。
定義了一個新接口 Apology(道歉):
- public interface Apology {
- void saySorry(String name);
- }
但我不想在代碼中讓 GreetingImpl 直接去實現(xiàn)這個接口,我想在程序運行的時候動態(tài)地實現(xiàn)它。因為加入我實現(xiàn)了這個接口,那么我就一定要改寫 GreetingImpl 這個類,關(guān)鍵是我不想改它,或許在真實場景中,這個類有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 類,同時也實現(xiàn)了新定義的 Apology 接口。在類中首先覆蓋了父類的 invoke() 方法,然后實現(xiàn)了 Apology 接口的方法。我就是想用這個增強類去豐富 GreetingImpl 類的功能,那么這個 GreetingImpl 類無需直接實現(xiàn) Apology 接口,就可以在程序運行的時候調(diào)用 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"/> <!-- 需要動態(tài)實現(xiàn)的接口 -->
- <property name="target" ref="greetingImpl"/> <!-- 目標(biāo)類 -->
- <property name="interceptorNames" value="greetingIntroAdvice"/> <!-- 引入增強 -->
- <property name="proxyTargetClass" value="true"/> <!-- 代理目標(biāo)類(默認(rèn)為 false,代理接口) -->
- </bean>
- </beans>
需要注意 proxyTargetClass 屬性,它表明是否代理目標(biāo)類,默認(rèn)為 false,也就是代理接口了,此時 Spring 就用 JDK 動態(tài)代理。如果為 false,那么 Spring 就用 CGLib 動態(tài)代理。這簡直就是太方便了!Spring 封裝了這一切,讓程序員不在關(guān)心那么多的細(xì)節(jié)。我們要向老羅同志致敬,您是我們心中永遠(yuǎn)的 idol!
當(dāng)您看完下面的客戶端代碼,一定會完全明白以上的這一切:
- public class Client {
- public static void main(String[] args) {
- ApplicationContext context = new ClassPathXmlApplicationContext("aop/demo/spring.xml");
- GreetingImpl greetingImpl = (GreetingImpl) context.getBean("greetingProxy"); // 注意:轉(zhuǎn)型為目標(biāo)類,而并非它的 Greeting 接口
- greetingImpl.sayHello("Jack");
- Apology apology = (Apology) greetingImpl; // 將目標(biāo)類強制向上轉(zhuǎn)型為 Apology 接口(這是引入增強給我們帶來的特性,也就是“接口動態(tài)實現(xiàn)”功能)
- apology.saySorry("Jack");
- }
- }
沒想到 saySorry() 方法原來是可以被 greetingImpl 對象來直接調(diào)用的,只需將其強制轉(zhuǎn)換為該接口即可。
我們再次感謝 Spring AOP,感謝老羅給我們提供了這么強大的特性!
其實,Spring AOP 還有很多精彩的地方,下一篇將介紹更多更有價值的 AOP 技術(shù),讓大家得到更多的收獲。