在正常的業務流程中,往往存在著一些業務邏輯,例如安全審計、日志管理,它們存在於每一個業務中,然而卻和實際的業務邏輯沒有太強的關聯關系。
圖1
這些邏輯我們稱為橫切邏輯。如果把橫切的邏輯代碼寫在業務代碼中,散落在各個地方,則會變得非常難以維護,代碼也會顯得過於臃腫。
Spring AOP為處理這些問題提供了一種很好的方法。
1 通知(advice)。通知是指真正執行的目標邏輯。例如為系統記日志,那麼通知做的事情,就是記錄日志。
2 連接點(joinpoit)。連接點指何時執行advice的代碼。系統中如果想要為所有的start()方法執行之前,記錄一條"our system is starting"的日志,那麼,start()方法被調用的時機,稱為一個連接點。
3 切點(pointcut)。切點指何處執行advice的代碼。例如指定特定的一個路徑下的start()方法被調用時,執行advice的代碼,那麼,這個特定的路徑可以理解為切點。通常我們使用正則表達式定義匹配的類和方法來確定切點。
4 切面(aspect)。 切面是通知、切點和連接點的全部內容。它定義了何時在何處,完成什麼功能。
5 織入(weaving)。織入指實際代碼執行到切點時,完成對目標對象代理的過程。織入有如下三種方式:
a.編譯器織入。需要特殊編譯器支持,例如AspectJ的織入編譯器,在編譯期間將代碼織入。
b.類加載時。目標類在載入JVM時將切面引入。需要特殊的classLoader支持,改變目標類的字節碼,增加新的動作。
c.運行期。在運行期運用動態代理技術,代理目標類。Spring AOP就是使用這種方式。
Spring對AOP提供了多種支持,大體上分為2種思路。
1 使用動態代理,在運行期對目標類生成代理對象。代理類封裝了目標類,並攔截被通知的方法的調用,再將調用轉發給真正的目標類方法調用。在代理類中,可以執行切面邏輯。基於這種模式,僅限於對方法的攔截。如果想要實現更細粒度的攔截,例如特定的字段的修改,則需要使用第2種思路了。
2 使用AspectJ特有的AOP語言。這種方式的成本在於,需要學習AspectJ的語法。
大多數情況下,第一種思路已經可以滿足我們的需求。
我們可以使用execution指示器來指示切點。如圖2
圖2
execution指示器具體指定了哪個包下面哪個類執行什麼方法時,會被織入橫切邏輯。Spring AOP的demo網上有很多,但是原理其實都一樣。這裡貼出其中一種。
我們需要2個類,一個類(Biz)用作正常的業務邏輯,一個類用作橫切(AdviceTest)。同時寫一個類Main測試
1.Biz
1 public class Biz { 2 public void doBiz(){ 3 System.out.println("doing biz"); 4 } 5 } View Code2.AdviceTest
1 public class AdviceTest implements Advice, MethodInterceptor { 2 3 @Override 4 public Object invoke(MethodInvocation invocation) throws Throwable { 5 advice(); 6 invocation.proceed(); 7 return null; 8 } 9 10 public void advice(){ 11 System.out.println("this is a advice"); 12 } 13 14 } View Code3.Main
1 public class Main { 2 public static void main(String[] args) { 3 ApplicationContext applicationContext = new ClassPathXmlApplicationContext(new String[]{"config/spring/spring-aop.xml"}); 4 Biz biz = (Biz)applicationContext.getBean("biz"); 5 biz.doBiz(); 6 } 7 } View Code4.Spring配置
1 <?xml version="1.0" encoding="UTF-8" ?> 2 <beans 3 xmlns="http://www.springframework.org/schema/beans" 4 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 5 xmlns:aop="http://www.springframework.org/schema/aop" 6 xsi:schemaLocation=" 7 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 8 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd 9 "> 10 11 <bean id="adviceTest" class="aop.AdviceTest"/> 12 13 <bean id="biz" class="aop.Biz"/> 14 15 <aop:config> 16 <aop:pointcut id="bizPointCut" expression="execution(* aop.Biz.*(..))"/> 17 <aop:advisor pointcut-ref="bizPointCut" advice-ref="adviceTest"/> 18 </aop:config> 19 20 </beans> View Code執行Main函數代碼,輸出如下
this is a advice
doing biz
可見,實際的biz邏輯已經經過增強。
Spring提供了兩種方式來生成代理對象: JDKProxy和Cglib,具體使用哪種方式生成由AopProxyFactory根據AdvisedSupport對象的配置來決定。默認的策略是如果目標類是接口,則使用JDK動態代理技術,否則使用Cglib來生成代理。在我們的例子中,顯然使用的是Cglib。如圖3
圖3
為什麼我們配置了Biz的bean,得到的卻不是Biz的實例呢?這和Spring對xml文件的標簽解析策略有關。對於AOP相關的bean的解析,在AopNamespaceHandler類裡面有相關代碼,感興趣的同學可以去研究下。
我們繼續往下面單步調試,發現進入了CglibAopProxy類裡面。
1 public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { 2 Object oldProxy = null; 3 boolean setProxyContext = false; 4 Class<?> targetClass = null; 5 Object target = null; 6 try { 7 if (this.advised.exposeProxy) { 8 // Make invocation available if necessary. 9 oldProxy = AopContext.setCurrentProxy(proxy); 10 setProxyContext = true; 11 } 12 // May be null. Get as late as possible to minimize the time we 13 // "own" the target, in case it comes from a pool... 14 target = getTarget(); 15 if (target != null) { 16 targetClass = target.getClass(); 17 } 18 List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); 19 Object retVal; 20 // Check whether we only have one InvokerInterceptor: that is, 21 // no real advice, but just reflective invocation of the target. 22 if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) { 23 // We can skip creating a MethodInvocation: just invoke the target directly. 24 // Note that the final invoker must be an InvokerInterceptor, so we know 25 // it does nothing but a reflective operation on the target, and no hot 26 // swapping or fancy proxying. 27 Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); 28 retVal = methodProxy.invoke(target, argsToUse); 29 } 30 else { 31 // We need to create a method invocation... 32 retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed(); 33 } 34 retVal = processReturnType(proxy, target, method, retVal); 35 return retVal; 36 } 37 finally { 38 if (target != null) { 39 releaseTarget(target); 40 } 41 if (setProxyContext) { 42 // Restore old proxy. 43 AopContext.setCurrentProxy(oldProxy); 44 } 45 } 46 } View Code無恥的把代碼直接貼出來。我們重點關注下下面這一行
retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
在本文例子的場景下,會在此創建一個CglibMethodInvocation執行上下文,並執行proceed()方法。
圖4
最終執行AdviceTest中invoke()方法的調用。在AdviceTest的invoke方法中,我們可以自定義自己想要執行的增強邏輯invocation.proceed()來執行目標類的方法。
圖5
Spring AOP為許多與業務無關的邏輯的執行,提供了一種很好的解決思路。本文也只是拋磚引玉,在實際的Spring源碼中,還是比較復雜的,還需要細細研究才行。
參考文獻:
《Spring In Action》
《Spring源碼深度解析》
作者:mayday芋頭 出處:http://www.cnblogs.com/maypattis/ 本博客中未標明轉載的文章歸作者mayday芋頭和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。