架構探險筆記4-使框架具備AOP特性(上)
對方法進行效能監控,在方法呼叫時統計出方法執行時間。
原始做法:在內個方法的開頭獲取系統時間,然後在方法的結尾獲取時間,最後把前後臺兩次分別獲取的系統時間做一個減法,即可獲取方法執行所消耗的總時間。
專案中大量的方法,如果對每個方法開頭結尾都加上這些程式碼,工作量會很大。現在不用修改現有程式碼,在另一個地方做效能監控,AOP(Aspect Oriented Programming,面向方面程式設計)就是我們尋找的解決方案。
在AOP中,我們需要定義一個Aspect(切面)類來編寫需要橫切業務邏輯的程式碼,也就是效能監控程式碼。此外,我們需要通過一個條件來匹配想要攔截的類,這個條件在AOP中稱為Pointcut(切點)。
案例思路,統計出執行每個Controller類的各個方法所消耗的時間。每個Controller類都有Controller註解,也就是說,我們只需要攔截所有帶有Controller註解的類就行了,切點很容易就能確定下來,剩下的就是做一個切面了。
代理技術
代理,或稱為Proxy,意思就是你不用去做,別人替你去處理。比如說:賺錢方面,我就是我老婆的Proxy;帶小孩方面,我老婆就是我的Proxy;家務事方面,沒有Proxy。
它在程式中開發起到了非常重要的作用,比如AOP,就是針對代理的一種應用。此外,在設計模式中,還有一個“代理模式”,在公司要上網,要在瀏覽器中設定一個Http代理。
Hello World例子
//介面 public interface Hello{ void say(String name); } //實現類 public class HelloImpl implements Hello{ @Override public void say(String name){ System.out.println("Hello!"+name); } }
如果要在println方法前面和後面分別需要處理一些邏輯,怎麼做呢?把這些邏輯寫死在say方法裡面嗎?這麼做肯定不夠優雅,“菜鳥”一般這樣幹,作為一名資深的程式設計師,我們堅決不能這麼做!
我們要用代理模式,寫一個HelloProxy類,讓它去呼叫HelloImpl的say方法,在呼叫的前後分別進行邏輯處理。
public class HelloProxy implements Hello{ private Hello hello; private HelloProxy(){ hello=new HelloImpl(); } private void say(String name){ before(); hello.say(name); after(); } private void before(){ System.out.println("Before"); } private void after(){ System.out.println("After"); } }
用HelloProxy類實現了Hello介面(和HelloImpl實現相同的介面),並且在構造方法中new出一個HelloImpl類的例項。這樣一來,我們就可以在HelloProxy的say方法裡面去呼叫HelloImpl的say方法了。更重要的是,我們還可以在呼叫的前後分別加上before和after兩個方法,在這兩個方法裡去實現那些前後邏輯。
main方法測試
public static void main(String[] args){ Hello helloProxy = new HelloProxy(); helloProxy.say("Jack"); } //列印結果 Before Hello! Jack After
JDK動態代理
於是瘋狂使用代理模式,專案中到處都是XXXProxy的身影,直到有一天,架構師看到了我的程式碼,他驚呆了,對我說“你怎麼這麼喜歡靜態代理呢?你就不會用動態代理嗎?全部重構!”
研究了一下,原來一直用的是靜態代理(上面的例子),到處都是XXXProxy類。一定要將這些垃圾Proxy都重構為“動態代理”。
/** * 動態代理 */ public class DynamicProxy implements InvocationHandler { private Object target; public DynamicProxy(Object target) { this.target = target; } /* * 用時代理 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { before(); Object result method.invoke(target, args); after();
return result;
} } //執行 public static void main(String[] args) { Hello hello = new HelloImpl();//用時代理 DynamicProxy dynamicProxy = new DynamicProxy(hello);
Hello helloProxy = (Hello)Proxy.newProxyInstance(
hello.getClass().getClassLoader(),
hello.getClass().getInterfaces(),
dynamicProxy
); //執行run方法 helloProxy.say("Jack"); }
在這個例子中,DynamicProxy定義了一個Object型別的Object變數,它就是被代理的目標物件,通過建構函式來初始化(“注入”,構造方法初始化叫“正著射”,所以反射初始化叫“反著射”,簡稱“反射”)。
通過DynamicProxy類去包裝Car例項,然後再呼叫JDK給我們的提供的Proxy類的工廠方法newProxyInstance去動態的建立一個Hello介面的代理類,最後呼叫這個代理類的run方法。
Proxy.newProxyInstance方法的引數
引數1:ClassLoader
引數2:該實現類的所有介面
引數3:動態代理物件
呼叫完了用強制型別轉換下
這一塊想辦法封裝一下,避免再次出現到處都是Proxy.newProxyInstance方法的情況。於是將這個DynamicProxy重構一下:
public class DynamicProxy implements InvocationHandler{
private Object target; public DynamicProxy(Object target) { this.target = target; }
@SupressWarnings("unchecked") public <T> T getProxyInstance() { 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; } //請求前的操作 public void before(){ //預處理 System.out.println(target.getClass()+"被動態代理了,在它執行之前要執行動態代理加入的預處理方法"); } //請求後的操作 public void after(){ //善後處理 System.out.println(target.getClass()+"被動態代理了,在它執行之後要執行動態代理加入的善後方法"); } } public class DynamicProxyDemo { public static void main(String[] args) {
DynamicProxy dynamicProxy = new DynamicProxy(new HelloImpl());
Hello helloProxy = dynamicProxy.getProxy();
helloProxy.say("Jack"); } }
在DynamicProxy裡添加了一個getProxy方法,無需傳入任何引數,將剛才所說的那塊程式碼放在這個方法中,並且該方法返回一個泛型型別,就不會強制轉換型別了。方法頭上@SupressWarnings(“unchecked”)註解表示忽略編譯時的警告(因為Proxy.newProxyInstance方法返回的是一個Object,這裡強制轉換為T了,這是向下轉型,IDE中就會有警告,編譯時也會出現提示)。
呼叫時就簡單了,用2行代理去掉了前面的7行程式碼(省了5行)。
CGlib動態代理
用了DynamicProxy以後,好處是介面變了,這個動態代理類不用動。而靜態代理就不一樣了,介面變了,實現類還要動,代理類也要動。但是動態代理並不是萬能的,它也有搞不定的時候,比如要代理一個沒有任何介面的類,它就沒有用武之地了。
CGlib是一個能代理沒有介面的類,雖然看起來不起眼,但Spring、Hibernate這樣高階的開源框架都用到了它,它是一個在執行期間動態生成位元組碼的工具,也就是動態生成代理類了。
public class CGLibProxy implements MethodInterceptor{ public <T> T getProxy(Class<T> cls){ return (T) Enhancer.create(cls,this); } public Object intercept(Object obj,Method method,Object[] args,MethodProxy proxy) throws Throwable{ before(); Object result = proxy.invokeSuper(obj,args); after(); return result; } ...... }
需要實現CGLib給我們提供的MethodInterceptor實現類,並填充intercept方法。方法中最後一個MethodProxy型別的引數proxy值得注意。CGLib給我們提供的是方法級別的代理,也可以理解為堆方法攔截(也就是“方法攔截器”)。這個功能對於我們程式設計師來說,如同雪中送炭。我們直接呼叫proxy的invokeSuper方法,將被代理的物件obj以及方法引數args傳入其中即可。
與DynamicProxy類似,在CGLibProxy中也添加了一個泛型的getProxy方法,便於我們可以快速地獲取自動生成的代理物件。
public static void main(){ CGLibProxy cgLibProxy = new CGLibProxy(); Hello helloProxy = cgLibProxy.getProxy(HelloImpl.class); helloProxy.say(Jack); }
仍然通過2行程式碼就可以返回代理物件,與JDK動態代理不同的是,這裡不需要任何的介面資訊,對誰都可以生成動態代理物件。
用2行程式碼返回代理物件還是有些多餘的,不想總是去new這個CGLibProxy物件,最好new一次,以後隨時拿隨時用,於是想到了“單例模式”:
public class CGLibProxy implements MethodInterceptor{ private static CGLibProxy instance = new CGLibProxy(); private CGLibProxy(){ } private static CGLibProxy getInstance(){ return instance; }
...
getProxy...
intercept... }
加上以上幾行程式碼問題就解決了。需要說明的是,這裡有一個private的構造方法,就是為了限制外界不能再去new它了,換句話說,這個類被閹割了。
public static void main(String[] args){ Hello helloProxy = CGLibProxy.getInstance().getProxy(HelloImpl.class); helloProxy.say("Jack"); }
這裡只需要一行程式碼就可以獲取代理物件了.
AOP技術
什麼是AOP
AOP(Aspect-Oriented Programming),名字與OOP僅僅差一個字母,其實它是對OOP程式設計方式的一種補充,並非是取而代之。翻譯過來就是“面向切面程式設計”或“面向方面程式設計”。最重要的工作就是寫這個“切面”,那麼什麼事“切面”呢?
切面是AOP中的一個術語,表示從業務邏輯中分離出來的橫切邏輯,比如效能監控、日誌記錄、許可權控制等,這些功能都可以從業務邏輯程式碼中抽離出去。也就是說,通過AOP可以解決程式碼耦合問題,讓職責更加單一。
需要澄清的是,其實很早以前就出現了AOP這個概念。最知名強大的Java開源專案就是AspectJ了。它的前身是AspectWerkz(AOP真正的的老祖宗)。Rod Johnson寫了一個Spring框架,稱為Spring之父。他在Spring的IOC框架基礎上又實現了一套AOP框架,後來掉進了深淵,在無法自拔的時候被迫使用了AspectJ。所以我們現在用的最多的就是Spring+AspectJ這種AOP框架了。
寫死程式碼
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程式,那是不是也要在方法的開頭去連線資料庫,方法的末尾去關閉資料庫連線呢?
這樣寫程式碼只會把程式設計師累死,把架構師氣死!
一定要想辦法對上面的程式碼進行重構,首先給出三個解決方案:
靜態代理
JDK動態代理
CGLib動態代理
靜態代理
最簡單的解決方案就是使用靜態代理模式了,我們單獨為GreetingImpl這個類寫一個代理類:
public class GreetingProxy implements Greeting { private GreetingImpl greetingImpl; public GreetingProxy(GreetingImpl greetingImpl) { this.greetingImpl = greetingImpl; } @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"); } }
就用這個GreetingProxy去代理GreetingImpl,看看客戶端如何來呼叫:
public class Client { public static void main(String[] args) { Greeting greetingProxy = new GreetingProxy(new GreetingImpl()); greetingProxy.sayHello("Jack"); } }
這個寫的沒錯,但是有個問題。XxxProxy這樣的類會越來越多(這裡建構函式引數為GreetingImpl,所以換一個子類就要再次寫一個代理類,如果建構函式引數改為介面Greet,這樣再使用Greet的子類時可以使用這個類,當換一個介面時又要去寫一個代理類),如何才能將這些代理類儘可能減少呢?最好只有一個代理類。
這時我們需要使用JDK的動態代理了。
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"); } }
這樣所有的代理類都合併到動態代理類中了,但這樣做仍然存在一個問題:JDK給我們提供的動態代理只能代理介面,而不能代理沒有介面的類。
public class Client { public static void main(String[] args) { //靜態代理 /*Greeting greetingProxy = new GreetingProxy(new GreetingImpl()); greetingProxy.sayHello("Jack");*/ //JDK動態代理 Greeting greeting = new JDKDynamicProxy(new GreetingImpl()) .getProxy(); greeting.sayHello("Jack"); } }
CGLib動態代理
我們使用開源的CGLib類庫可以代理沒有介面的類,這樣就彌補了JDK的不足。CGLib動態代理類是這樣的:
public class CGLibDynamicProxy implements MethodInterceptor{ //單例模式 private static CGLibDynamicProxy instance = new CGLibDynamicProxy(); //私有化建構函式,防止new 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 methodProxy) throws Throwable { before(); Object result = methodProxy.invokeSuper(target,args); after(); return result; } private void before(){ System.out.println("Before"); } private void after(){ System.out.println("After"); } }
注意這裡的座標
<!--不能超過3.0版本,這裡用2.2--> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.2</version> </dependency> <!--CGLib依賴此包--> <dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm</artifactId> <version>6.2</version> </dependency>
到此為止,能做的都做了,問題似乎全部都解決了。但事情總不會那麼完美,而我們一定要追求完美。
Spring AOP
Rod Johnson搞出了一個AOP框架,Spring AOP:前置增強、後置增強、環繞增強(程式設計式)
上面例子中提到的before方法,在Spring AOP裡就叫Before Advice(前置增強)。有些人將Advice直譯為“通知”,這裡是不太合適的,因為它沒有“通知”的含義,而是對原有程式碼功能的一種“增強”。再者,CGLib中也有一個Enhancer類,它就是一個增強類。
此外,像after這樣的方法就叫After Advice(後置增強),因為它放在後面來增強程式碼的功能。
如果能把before與after結合在一起,那就叫Around Advice(環繞增強),就像漢堡一樣。
前置增強類程式碼(這個類實現了org.spring.framework.aop.MethodBeforeAdvice):
import org.springframework.aop.MethodBeforeAdvice; public class GreetingBeforeAdvice implements MethodBeforeAdvice { @Override public void before(Method method, Object[] objects, Object o) throws Throwable { System.out.println("Before"); } }
後置增強類:
import org.springframework.aop.AfterReturningAdvice; public class GreetingAfterAdvice implements AfterReturningAdvice { @Override public void afterReturning(Object o, Method method, Object[] objects, Object o1) 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"); } }
當然我們完全可以用一個增強類,讓它同時實現MethodBeforeAdvice和AfterReturningAdvice這兩個介面,程式碼:
public class GreetingBeforeAndAfterAdvice implements MethodBeforeAdvice,AfterReturningAdvice { @Override public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { System.out.println("before"); } @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println("after"); } }
這樣我們只需要使用一行程式碼,就可以同時新增前置與後置增強
proxyFactory.addAdvice(new GreetingBeforeAndAfterAdvice());
剛才有提到過“環繞增強”,其實它可以把“前置增強”與“後置增強”的功能合併起來,無須讓我們同時實現兩個介面。
import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import java.lang.reflect.Method; public class GreetingAroundAdvice implements MethodInterceptor { @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { before(); Object result = methodInvocation.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只是借用了它,在客戶端彙總同樣也需要將該增強類的物件新增到代理工廠中。
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()); //新增後置增強 //proxyFactory.addAdvice(new GreetingBeforeAndAfterAdvice()); //實現兩個介面 proxyFactory.addAdvice(new GreetingAroundAdvice()); //實現一個Around環繞式介面 Greeting greeting = (Greeting) proxyFactory.getProxy(); greeting.sayHello("Jack"); } }
以上就是SpringAOP的基本用法,單這只是“程式設計式”而已。Spring AOP如果只是這樣,那就太弱了,它曾經也一度宣傳用Spring配置檔案的方式來定義Bean物件,把程式碼中的new操作全部解脫出來。
SpringAOP:前置增強、後置增強、環繞增強(宣告式)
Spring配置檔案篇日誌
<!--掃描指定包(將帶有Component註解的類自動定義為SpringBean)--> <context:component-scan base-package="com.smart4j.framework"/> <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="interfaces" value="com.smart4j.framework.Greeting"/> <!--需要代理的介面--> <property name="target" ref="greetingImpl"/> <!--實現介面類--> <property name="interceptorNames"> <!--攔截器名稱(也就是增強類名稱,SpringBean的Id)--> <list> <value>greetingAroundAdvice</value> </list> </property> </bean>
使用ProxyFactoryFactoryBean就可以取代前面的ProxyFactory,其實他們是一回事。interceptorNames改名為adviceNames或許更容易讓人理解。就是網這個屬性裡新增增強類。
此外,如果只有一個增強類,可以使用下面這個方法來簡化
<bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="interfaces" value="com.smart4j.framework.Greeting"/> <!--需要代理的介面 name也可以為proxyInterfaces--> <property name="target" ref="greetingImpl"/> <!--實現介面類 name也可以為targetName--> <property name="interceptorNames" value="greetingAroundAdvice"> <!--攔截器名稱(也就是增強類名稱,SpringBean的Id)--> </property> </bean>
需要注意的是,這裡使用了Spring2.5+的"Bean掃描"特性,這樣我們就無需再Spring配置問加你了不斷的定義<bean id="xxx" class="xxx"/>了,從而解脫了我們的雙手。
去掉原本的
<bean id="greetingImpl" class="com.smart4j.framework.GreetingImpl"></bean> <bean id="greetingAroundAdvice" class="com.smart4j.framework.aop.GreetingAroundAdvice"></bean>
改為使用@Compoent
@Component public class GreetingImpl implements Greeting{ 。。。 } @Component public class GreetingAroundAdvice implements MethodInterceptor { 。。。 }
程式碼量確實少了,我們將配置性的程式碼放入配置檔案,這樣也有助於後期維護。更重要的是,程式碼值關注於業務邏輯,而將配置放入檔案中,這是一條最佳實踐!
除了上面提到的那三個增強意外,其實還有兩個增強也需要了解一下,關鍵的時候要能想到它們才行。
Spring AOP:丟擲增強
程式報錯,丟擲異常了,一般的做法是列印控制檯到日誌檔案中,這樣很多地方都得去處理,有沒有一個一勞永逸的方法呢?那就是Throws Advice(丟擲增強)
@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("---------------------------------------------"); } }
配置spring.xml
<bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces" value="com.smart4j.framework.Greeting"/> <!--需要代理的介面--> <property name="targetName" value="greetingImpl"/> <!--實現介面類--> <property name="interceptorNames"> <!--攔截器名稱(也就是增強類名稱,SpringBean的Id)--> <list> <!-- <value>greetingAroundAdvice</value>--> <value>greetingThrowAdvice</value> </list> </property> </bean>
結果
丟擲增強需要實現org.springframework.aop.ThrowsAdvice介面,在介面方法中可獲取方法、引數、目標物件、異常物件等資訊。我們可以把這些資訊統一寫入到日誌中,當然也可以持久化到資料庫中。
Spring AOP:引入增強
以上提到的都是對方法的增強,那能否對類進行增強呢?用AOP的行話來講,對方法的增強叫Weaving(織入),而對類的增強叫Introduction(引入),Introduction Advice(引入增強)就是對類的功能增強,它也是Spring AOP提供的最後一種增強。
定義介面
public interface Apology { void saySorry(String name); }
但是我們不想在程式碼中讓GreetingImpl直接去實現這個介面,而想在程式執行的時候動態地實現它。因為加入實現了這個介面,那麼久一定要改寫GreetingImpl這個類,關鍵是我們不想改它,或許在真實場景中,這個類有一萬行程式碼。於是,我們需要藉助Spring的引入增強。
@Component public class GreetingIntroAdvice extends DelegatingIntroductionInterceptor implements Apology{ @Override public Object invoke(MethodInvocation mi) throws Throwable { return super.invoke(mi); } @Override public void saySorry(String name) { System.out.println("Sorry "+name); } }
以上一個引入增強類,擴充套件了org.springframework.aop.support.DelegatingIntroductionInterceptor類,同時也實現了新定義的Apology介面。在類中首先覆蓋了父類的invoke()方法,然後實現了Apology介面的方法。我們相擁這個增強類去豐富GreetingImpl類的功能,那麼這個GreetingImpl類無須直接實現Apology介面,就可以直接在程式執行的時候呼叫Apology介面的方法了。
配置Spring.xml
<!--引入增強--> <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces" value="com.smart4j.framework.aop.Apology"/> <!--需要動態實現的介面--> <property name="targetName" value="greetingImpl"/> <!--目標類--> <property name="interceptorNames" value="greetingIntroAdvice"/> <!--攔截器名稱(也就是增強類名稱,SpringBean的Id)--> <property name="proxyTargetClass" value="true"/> <!--代理目標類,(預設為false,代理介面)--> </bean>
需要注意proxyTargetClass屬性,它表明是否代理目標類,預設為false,也就是代理介面,此時Spring就用JDK動態代理;如果為TRUE,那麼Spring就用CGLib動態代理。
呼叫
ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml"); //獲取Spring Context Greeting greeting = (Greeting) context.getBean("greetingProxy"); //從Context中根據id獲取Bean物件(其實也就是一個代理) greeting.sayHello("jack"); //呼叫代理方法 Apology apology = (Apology) greeting; //將目標類增強向上轉型為Apology介面(這是引入增強給我們帶來的特性,也是"介面動態實現"功能) apology.saySorry("jack");
sarySorry方法原來是可以被greetingImpl物件來直接呼叫的,只需將其強制轉換為該介面即可。
SpringAOP:切面
之前談到的AOP框架其實可以將它理解為一個攔截器框架,但這個攔截器似乎非常武斷。比如說,如果它攔截了一個類,那麼它就攔截這個類中所有的方法。類似的,當我們在使用動態代理的時候,其實也遇到了這個問題。需要在程式碼中對所攔截的方法名加以判斷,才能過濾出我們需要攔截的方法,這種做法確實不太優雅。在大量的真實專案中,似乎我們只需要攔截特定的方法就行了,沒必要攔截所有的方法。於是,Spring藉助很重要的工具---Advisor(切面),來解決這個問題。它也是AOP中的核心,是我們關注的重點。
也就是說,我們可以通過切面,將增強類與攔截匹配條件組合在一起,然後將這個切面配置到ProxyFactory中,從而生成代理。
這裡提到這個“攔截匹配條件”在AOP中就叫作Pointcut(切點),其實說白了就是一個基於表示式的攔截條件。Advisor(切面)封裝了Advice(增強)與Pointcut(切點)。
@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中,最好用的是基於正則表示式的切面類。
配置
<bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="advice" ref="greetingAroundAdvice"/> <!--增強--> <property name="pattern" value="com.smart4j.framework.GreetingImpl.good.*"/> <!--切點(正則表示式)--> </bean> <!--配置一個代理類--> <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="targetName" value="greetingImpl"/> <!--目標類--> <property name="interceptorNames" value="greetingAdvisor"/> <!--切面(替換之前的攔截器名稱)--> <property name="proxyTargetClass" value="true"/> <!--代理目標類,(預設為false,代理介面)--> </bean>
呼叫
ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml"); //獲取Spring Context GreetingImpl greeting = (GreetingImpl) context.getBean("greetingProxy"); //從Context中根據id獲取Bean物件(其實也就是一個代理) greeting.sayHello("jack"); //呼叫未被代理方法 greeting.goodMorning("Jhon"); greeting.goodNight("Sawer");
結果
注意以上代理物件中的配置的interceptorNames,它不再是一個增強,而是一個切面,因為已經將增強封裝到該切面中了。此外,切面還定義了一個切點(正則表示式),其目的是為了只對滿足切點匹配條件的方法進行攔截。
這裡的切點表示式是基於正則表示式的。其中.*代表匹配所有字元,翻譯過來就是匹配GreetingImpl類中以good開頭的方法。
除了RegexpMethodPointcutAdvisor以外,在Spring AOP中還提供了幾個切面類,比如:
- DefaultPointcutAdvisor - 預設切面(可擴充套件它來自定義切面)
- NameMatchMethodPointcutAdvisor - 根據方法名稱進行匹配的切面
- StaticMethodMatcherPointcutAdvisor - 用於匹配靜態方法的切面
總的來說,讓使用者去配置一個或少數幾個代理,似乎還可以接受,但隨著專案的擴大,代理配置就會越來越多,配置的重複勞動就多了,麻煩不說,還很容易出錯。。能否讓Spring框架為我們自動生成代理呢?
Spring AOP:自動代理(掃描Bean名稱)
Spring AOP提供了一個可以根據Bean名稱來自動生成代理的工具,它就是BeanNameAutoProxyCreator。配置如下:
<!--自動代理--> <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>
呼叫
ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml"); //獲取Spring Context GreetingImpl greeting = (GreetingImpl) context.getBean("greetingImpl"); //從Context中根據id獲取Bean物件(自動掃描的id為首字母小寫的類名) greeting.sayHello("jack");
以上使用BeanNameAutoProxyCreator只為字尾為"Impl"的Bean生成代理。需要注意的是,這個地方我們不能定義代理介面,也及時interfaces屬性,因為我們根本就不知道這些Bean到底實現了多少介面。此時不能代理介面,而只能代理類。所以這裡提供了一個新的配置項,它就是optimize。若為true時,則可對代理生成策略進行優化(預設是false)。也就是說,如果該類有介面,就代理介面(JDK動態代理);如果沒有介面,就代理類(使用CGLib動態代理)。並非像之前使用的proxyTargetClass屬性那樣,強制代理類,而不考慮代理介面的方式。
既然CGLib可以代理任何類,那為什麼還要用JDK的動態代理呢?
根據實際專案經驗得知,CGLib建立代理的速度比較慢,但建立代理後執行的速度卻非常快,而JDK動態代理正好相反。如果在執行的時候不斷地用CGLib去建立代理,系統的效能會大打折扣,所以建議一般在系統初始化的時候用CGLib去建立代理,並放入Spring的ApplicationContext中以備後用。
這個例子只能匹配目標類,而不能進一步匹配其中指定的方法,要匹配方法,就要考慮使用切面與切點了。Spring AOP基於切面也提供了一個自動代理生成器:DefaultAdvisorAutoProxyCreator。
Spring AOP:自動代理(掃描切面配置)
為了匹配目標類中的指定方法,我們讓然需要在Spring中配置切面與切點:
<!--自動代理 - 掃描切面配置--> <bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="pattern" value="com.smart4j.framework.GreetingImpl.good.*"/> <property name="advice" ref="greetingAroundAdvice"/> </bean> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"> <property name="optimize" value="true" /> </bean>
這裡無須再配置代理,因為代理將由DefaultAdvisorAutoProxyCreator自動生成。也就是說,這個類可以掃描所有的切面類,併為其自動生成代理。
看來不論怎麼簡化,Rod始終解決不了切面的配置這件繁重的手工勞動。在Spring配置檔案中,仍然會存在大量的切面配置。然而在很多情況下,Spring AOP所提供的切面類真的不太夠用了,比如像攔截指定註解的方法,我們就必須擴充套件DefaultPointcutAdvisor類,自定義一個切面類,然後在Spring配置檔案中進行切面配置。Rod的解決方案似乎已經掉進了切面類的深淵,最重要的是切面,最麻煩的也是切面。所以要把切面配置給簡化掉。
Spring+AspectJ
神一樣的rod總算認識到了這一點,接受了網友們的建議,集成了AspectJ,同時也保留了以上提到的切面與代理配置方式(為了相容老專案,更為了維護自己的面子)。將Spring與AspectJ整合與直接使用AspectJ是不同的,我們不需要定義AspectJ類(它擴充套件了Java語法的一種新的語言,還需要特定的編譯器),只需要使用AspectJ切點表示式即可(它是比正則表示式更加友好的表現形式)。
1.Spring+AspectJ(基於註解:通過AspectJ execution表示式攔截方法)
定義一個Aspect切面類
@Aspect /*切面*/ @Component public class GreetingAspect { @Around("execution(* com.smart4j.framework.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(* com.smart4j.framework.GreetingImpl.*(..))
- execution表示攔截方法,括號中可定義需要匹配的規則。
- 第一個"*"表示方法的返回值是任意的;
- 第二個"*"表示匹配該類中的所有方法;
- (..)表示方法的引數是任意的。
是不是比正則表示式可讀性更強呢?如果想匹配指定的方法,只需將第二個“*”改為指定的方法名即可。
配置如下
<!--掃描指定包(將帶有Component註解的類自動定義為SpringBean)--> <context:component-scan base-package="com.smart4j.framework"/> <aop:aspectj-autoproxy proxy-target-class="true"/>
兩行配置就行了,不需要配置大量的代理,更不需要配置大量的切面!proxy-target-class屬性,它的值預設是false,預設只能代理介面(使用JDK動態代理),當為true時,才能代理目標類(使用CGLib動態代理)。
Spring與AspectJ結合功能遠遠不止這些,我們還可以攔截指定註解的方法。
2.Spring+AspectJ(基於註解:通過AspectJ @annotation表示式攔截方法)
註解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Tag { }
以上定義一個Tag註解,此註解可標註在方法上,在執行時生效。
@Aspect /*切面*/ @Component public class GreetingAspect { @Around("@annotation(com.smart4j.framework.aspectj.Tag)") /*切點 - 有Tag標記的Method*/ 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"); } }
直接將Tag註解定義在想要攔截的方法上
@Component public class GreetingImpl implements Greeting { @Tag /*AspectJ 註解*/ @Override public void sayHello(String name) { System.out.println("Hello! "+name); } }
在以上例項中只有一個方法,如果有多個方法,我們只想攔截其中的某一些時,這種解決方案會更加有價值。
除了Around註解外,其實還有幾個相關的註解,稍微歸納一下:
- Before - 前置增強
- After - 後置增強
- Around - 環繞增強
- AfterThrowing - 丟擲增強
- DeclareParents - 引入增強
此外還有一個AfterReturning(返回後增強),也可理解為Finally增強,相當於finally語句,它是在方法結束後執行的,也就是說,它比After晚一些。
3.Spring+AspectJ(引入增強)
為了實現基於AspectJ的引入增強,我們同樣需要定義一個Aspect類:
@Aspect /*切面*/ @Component public class GreetingAspect { /*引入增強*/ @DeclareParents(value = "com.smart4j.framework.GreetingImpl",defaultImpl = ApologyImpl.class) private Apology apology; }
在Aspect類中定義一個需要引入增強的介面,它也就是執行時需要動態實現的介面。在這個介面上標註了DeclareParents註解,該註解有兩個屬性:
- Value - 目標類;
- defaultImpl - 引入介面的預設實現類。
我們只需要對引入的介面提供一個預設實現類即可完成增強:
public class ApologyImpl implements Apology { @Override public void saySorry(String name) { System.out.println("Sorry! " + name); } }
執行
ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml"); //獲取Spring Context GreetingImpl greeting = (GreetingImpl) context.getBean("greetingImpl"); //從Context中根據id獲取Bean物件(自動掃描的id為首字母小寫的類名) greeting.sayHello("jack"); Apology apology = (Apology) greeting; //將目標類增強向上轉型為Apology介面(這是引入增強給我們帶來的特性,也是"介面動態實現"功能) apology.saySorry("jack");
從SpringApplicationContext中獲取greetingImpl物件(其實是個代理物件),可轉型為自己靜態實現的介面Greeting,也可轉型為自己動態實現的介面Apology,切換起來非常方便。
使用AspectJ的引入增強比原來的SpringAOP的引入增強更加方便了,而且還可面向介面程式設計(以前只能面向實現類)。
這一切已經非常強大並且非常靈活了,但仍然還是由使用者不能嘗試這些特性,因為他們還在使用JDK1.4(根本就沒有註解這個東西),怎麼辦呢?SpringAOP為那些遺留系統也考慮到了。
3.Spring+AspectJ(基於配置)
除了使用Aspect註解來定義切面之外,SpringAOP也提供了基於配置的方式來定義切面類:
<!--AspectJ - 基於配置--> <bean id="greetingImpl" class="com.smart4j.framework.GreetingImpl"/> <bean id="greetingAspect" class="com.smart4j.framework.aspectj.GreetingAspect"/> <aop:config> <aop:aspect ref="greetingAspect"> <aop:around method="around" pointcut="execution(* com.smart4j.framework.GreetingImpl.*(..))"/> </aop:aspect> </aop:config>
使用<aop:config>元素來進行AOP配置,在其子元素中配置切面,包括增強型別、目標方法、切點等資訊。
無論使用者是不能使用註解,還是不願意使用註解,SpringAOP都能提供全方位的服務。
AOP思維導圖
各類增強型別所對應的解決方案
SpringAOP整體架構UML類圖