AOP = Aspect Oriental Programing 面向切面編程
文章里不講AOP術語,什么連接點、切點、切面什么的,這玩意太繞,記不住也罷。旨在以簡單、直白的方式理解AOP,理解Spring AOP, 應用 @AspectJ。
- 什么是AOP?
- Spring AOP 實現機制
- 使用Spring AOP,並通過xml配置(這個稍微看看就行了,你不一定用它)
- 使用@AspectJ (未完成)
1、什么是AOP?
方法1 | 方法2 | 方法3 |
A | A | A |
代碼x | 代碼y | 代碼z |
B | B | B |
從縱向看,方法1、2、3 都執行了相同的A、B代碼,這樣重復代碼是很無聊的。
一個典型的場景就是:開啟事務,更新表里數據,提交事務; 開啟事務,刪除表里數據,提交事務。
所以我們從橫向來,把重復代碼抽取出來,變為
A | A | A |
方法1(代碼x) | 方法2(代碼y) | 方法3(代碼z) |
B | B | B |
AOP希望將A、B 這些分散在各個業務邏輯中的相同代碼,通過橫向切割的方式抽取到一個獨立的模塊中,還業務邏輯類一個清新的世界。
當然,將這些重復性的橫切邏輯獨立出來很容易,但是如何將獨立的橫切邏輯 融合到 業務邏輯中 來完成和原來一樣的業務操作,這是事情的關鍵,也是AOP要解決的主要問題。
2.Spring AOP 實現機制
Spring AOP使用動態代理技術在運行期織入增強的代碼,使用了兩種代理機制,一種是基於JDK的動態代理,另一種是基於CGLib的動態代理。
織入、增強 是AOP的兩個術語,織入增強的代碼簡單點就是在你的代碼上插入另一段代碼。
JDK動態代理主要涉及到java.lang.reflect包中的兩個類:Proxy 和 InvocationHandler(接口)。
直接上代碼

package test; public interface CalcService { public void add(int x, int y); }

package test; public class CalcServiceImpl implements CalcService{ public void add(int x, int y) { System.out.println("結果為" + (x + y)); } }

package test; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class CalcHandler implements InvocationHandler { public Object target; public CalcHandler(Object target){ this.target = target; } /** * 實現接口的方法 * @param proxy 最終生成的代理實例 * @param method 被代理目標(也就是target)的某個具體方法 * @param args 某個具體方法的入參參數 * @return Object 方法返回的值*/ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("*******調用方法前執行代碼******"); Object obj = method.invoke(this.target, args); System.out.println("*******調用方法后執行代碼******"); return obj; } }

package test; import java.lang.reflect.Proxy; public class Test { public static void main(String[] args){ long start = System.nanoTime(); CalcService target = new CalcServiceImpl(); CalcHandler handler = new CalcHandler(target); CalcService calcProxy = (CalcService)Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler); System.out.println("創建時間:" + (System.nanoTime()-start)); start = System.nanoTime(); calcProxy.add(2, 3); System.out.println("執行時間:" + (System.nanoTime()-start)); } }
執行結果為
*******調用方法前執行代碼******
結果為2
*******調用方法后執行代碼******
但是JDK動態代理有一個限制,即它只能為接口創建代理實例*******************************。
看Proxy的方法 newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
interfaces 是需要代理實例實現的接口列表
那么對於一個沒有通過接口定義業務方法的類,怎么創建代理實例?
CGLib
CGLib采用非常底層的字節碼技術,可以在運行時為一個類創建子類,並在子類中采用方法攔截的技術攔截所有父類方法的調用。

package test; import java.lang.reflect.Method; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; public class CalcProxy implements MethodInterceptor { private Enhancer enhancer = new Enhancer(); public Object getProxy(Class clazz){ enhancer.setSuperclass(clazz); // 設置需要被代理的類 target enhancer.setCallback(this); return enhancer.create(); // 通過字節碼技術動態創建子類 } // 攔截父類所有方法的調用 public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("*******調用方法前執行代碼******"); Object result = proxy.invokeSuper(obj, args); // 通過代理類調用父類中的方法 System.out.println("*******調用方法后執行代碼******"); return result; } }

package com.ycp.framework.test.proxyPattern.sample2; public class Test2 { public static void main(String[] args) { CalcProxy proxy = new CalcProxy(); CalcServiceImpl calcImpl = (CalcServiceImpl)proxy.getProxy(CalcServiceImpl.class); calcImpl.add(2, 3); } }
之后對兩者做了一個效率對比
我在自己本機上通過System.nanoTime()對兩者做了記錄,結果如下
JDK動態代理 CGLiib
創建代理對象時間 720 1394 1 3473 7007 (時間單位為納秒)
代理對象執行方法時間 97 7322 15 2080
一個創建花費時間長,一個執行時間長。
3.使用 Spring AOP,並通過XML配置
在Spring中,定義了 AopProxy接口,並提供了兩個final類型的實現類
Cglib2AopProxy JdkDynamicAopProxy
以一個前置增強為例,也就是說在目標方法執行前執行的代碼

package test; import java.lang.reflect.Method; import org.springframework.aop.MethodBeforeAdvice; import org.springframework.aop.framework.ProxyFactory; public class CalcBeforeAdvice implements MethodBeforeAdvice { public void before(Method method, Object[] args, Object obj) throws Throwable { System.out.println("*******調用目標方法前執行代碼******"); } public static void main (String [] args){ CalcService target = new CalcServiceImpl(); CalcBeforeAdvice advice = new CalcBeforeAdvice(); // 1 spring 提供的代理工廠 ProxyFactory pf = new ProxyFactory(); // 2 設置代理目標 pf.setInterfaces(target.getClass().getInterfaces());// 指定對接口進行代理,將使用JdkDynamicAopProxy // 下面兩行操作,有任意一行,都將使用Cglib2AopProxy pf.setOptimize(true);// 啟用代理優化,將使用Cglib2AopProxy pf.setProxyTargetClass(true); // true表示對類進行代理,將使用Cglib2AopProxy pf.setTarget(target); // 3 為代理目標添加增強 pf.addAdvice(advice); // 4 生成代理實例 CalcService proxy = (CalcService) pf.getProxy(); System.out.println(proxy.getClass().getName()); proxy.add(2, 3); } }
以上通過ProxyFactory創建代理,下面我們通過Spring配置來聲明代理
<bean id="calcBeforAdvice" class="test.CalcBeforeAdvice"/> <bean id="calcTarget" class="test.CalcServiceImpl"/> <bean id="calcProxy" class="org.springframework.aop.framework.ProxyFactoryBean" p:proxyInterfaces="test.CalcService" //指定代理接口,如果有多個接口,可用,隔開 p:interceptorNames="calcBeforAdvice"//指定使用的增強,引用第一行,如果有多個可用,隔開 p:target-ref="calcTarget"//指定對那個Bean進行代理,引用第二行 p:proxyTargetClass="true" //指定是否對類進行代理 p:singleton="true"//指定返回的代理是否單實例,默認為true />
除了前置增強BeforeAdvice,還有后置增強AfterReturningAdvice、環繞增強MethodInterceptor、異常拋出增強
ThrowsAdvice、及引介增強IntroductionInterceptor,均為接口。
其中引介增強稍微強調一下,它會在目標類中增加一些新的方法和屬性。
到了這里,可能對AOP稍有些了解了,那我們簡單說一下AOP的幾個名詞
連接點Joinpoint:類初始化前、初始化后, 方法調用前、調用后,方法拋出異常后,這些特定的點,叫連接點。
切點Pointcut:想想數據庫查詢,切點就是通過其所設定的條件找到對應的連接點。
增強Advice:就是把代碼加到某個連接點上。
引介Introduction:一種特殊的增強,它為類增加一些屬性和方法,假設某個業務類沒有實現A接口,我們給它添加方法,讓其成為A的實現類。
織入Weaving:就是怎么將增強添加到連接點上。
三種織入方式:1、編譯期織入,要求使用特殊的JAVA編譯器
2.類裝載期織入,要求使用特殊的類裝載器
3.動態代理織入,在運行期為目標類添加增強
Spring采用動態代理,而AspectJ采用編譯期織入和類裝載期織入。
目標對象Target:也就是你自己的業務類,AOP就是對這個類做增強、引介。
代理Proxy: 目標對象被織入增強后產生的結果類。
切面:由切點和增強(引介)組成,Spring AOP就是負責實施切面的框架,它將切面所定義的橫切邏輯(也就是代碼)織入到切面所指定的連接點(也就是代碼往哪加)中。
看完了名詞,再看完之前的代碼,我們發現增強被織入到了目標類的所有方法中(XX的,都木有選擇的余地....)
現在我們要對某些類的某些方法織入增強,那這時候就涉及到切點概念了
以如下為例:我只想針對所有的以add開頭的方法做處理
靜態普通方法名匹配

package com.ycp.framework.test.proxyPattern.sample2; import java.lang.reflect.Method; import org.springframework.aop.ClassFilter; import org.springframework.aop.IntroductionInterceptor; import org.springframework.aop.ThrowsAdvice; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor; public class AddAdvisor extends StaticMethodMatcherPointcutAdvisor { //StaticMethodMatcherPointcutAdvisor 抽象類 // 實現父類 matches方法 public boolean matches(Method method, Class clazz) { //只匹配add方法 return 0 == "add".indexOf(method.getName()); } // //切點類匹配規則為 CalcServiceImpl的類或子類, // @Override // public ClassFilter getClassFilter(){ // return new ClassFilter(){ // public boolean matches(Class clazz){ // return CalcServiceImpl.class.isAssignableFrom(clazz); // } // }; // } public static void main (String [] args){ CalcService target = new CalcServiceImpl(); CalcBeforeAdvice advice = new CalcBeforeAdvice(); // 1 spring 提供的代理工廠 ProxyFactory pf = new ProxyFactory(); // 2 設置代理目標 pf.setInterfaces(target.getClass().getInterfaces());// 指定對接口進行代理,將使用JdkDynamicAopProxy // 下面兩行操作,有任意一行,都將使用Cglib2AopProxy pf.setOptimize(true);// 啟用代理優化,將使用Cglib2AopProxy pf.setProxyTargetClass(true); // true表示對類進行代理,將使用Cglib2AopProxy pf.setTarget(target); // 3 為代理目標添加增強 AddAdvisor advisor = new AddAdvisor(); advisor.setAdvice(advice); pf.addAdvisor(advisor); // 4 生成代理實例 CalcService proxy = (CalcService) pf.getProxy(); System.out.println(proxy.getClass().getName()); proxy.add(2, 3); } }
通過Spring配置來定義切面
<bean id="calcBeforAdvice" class="test.CalcBeforeAdvice"/> <bean id="calcTarget" class="test.CalcServiceImpl"/> <bean id="addAdvisor" class="test.AddAdvisor" p:advice-ref="calcBeforAdvice"//向切面注入一個前置增強 /> <bean id="parent" class="org.springframework.aop.framework.ProxyFactoryBean" p:interceptorNames="addAdvisor" p:proxyTargetClass="true" /> <bean id="calc" parent="parent" p:target-ref="calcTarget"/> //CalcServiceImpl的代理
上面的忒麻煩,我們通過靜態正則表達式來匹配
<bean id="calcBeforAdvice" class="test.CalcBeforeAdvice"/> <bean id="calcTarget" class="test.CalcServiceImpl"/> <bean id="addRegexpAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor" p:advice-ref="calcBeforAdvice"//向切面注入一個前置增強 > <property name="patterns"> <list> <value> .add*</value>//匹配模式串 </list> </property> </bean> <bean id="calc" class="org.springframework.aop.framework.ProxyFactoryBean" p:interceptorNames="addRegexpAdvisor" p:target-ref="calcTarget" p:proxyTargetClass="true" />
Spring提供了6種類型的切點,靜態方法切點、動態方法切點、注解切點、表達式切點、流程切點,我能力有限,沒有研究下去,僅以靜態切點 StaticMethodMatcherPointcut 做個例子就算完事,啥時項目用到了啥時再研究吧。
4.使用AspectJ
Spring AOP應用是比較麻煩的,要實現這個那個接口,寫這個那個XML描述,你頭疼不?
使用@AspectJ的注解可以非常容易的定義一個切面,不需要實現任何的接口