當前位置:
首頁 > 知識 > AOP 那點事兒

AOP 那點事兒

(點擊

上方公眾號

,可快速關注)




來源:黃勇 ,


my.oschina.net/huangyong/blog/161338




又是一個周末,剛給寶寶喂完牛奶,終於讓她睡著了。所以現在我才能騰出手來,坐在電腦面前給大家寫這篇文章。




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



沒錯!就是用一把刀來切一坨面。注意,相對於面而言,我們一定是橫著來切它,這簡稱為「橫切」。可以把一段代碼想像成一坨面,同樣也可以用一把刀來橫切它,下面要做的就是如何去實現這把刀!




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




那麼 AOP 到底是什麼?如何去使用它?本文將逐步帶您進入 AOP 的世界,讓您感受到前所未有的暢快!




不過在開始講解 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() 方法體中了,這樣的代碼的味道非常不好。如果哪位仁兄大量寫了這樣的代碼,肯定要被你的架構師罵個夠嗆。




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




再比如:我們要寫一個 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 還有很多精彩的地方,下一篇將介紹更多更有價值的 AOP 技術,讓大家得到更多的收穫。




未完,待續…




源碼下載





http://www.oschina.net/code/snippet_223750_25978




【關於投稿】




如果大家有原創好文投稿,請直接給公號發送留言。




① 留言格式:


【投稿】+《 文章標題》+ 文章鏈接

② 示例:


【投稿】《不要自稱是程序員,我十多年的 IT 職場總結》:http://blog.jobbole.com/94148/

③ 最後請附上您的個人簡介哈~






看完本文有收穫?請轉發分享給更多人


關注「ImportNew」,提升Java技能


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 ImportNew 的精彩文章:

本機搭建三節點 k8s 集群

TAG:ImportNew |