AOP 是什么東西
首先來說 AOP 並不是 Spring 框架的核心技術之一,AOP 全稱 Aspect Orient Programming,即面向切面的編程。其要解決的問題就是在不改變源代碼的情況下,實現對邏輯功能的修改。常用的場景包括記錄日志、異常處理、性能監控、安全控制(例如攔截器)等,總結起來就是,凡是想對當前功能做變更,但是又不想修改源代碼的情況下,都可以考慮是否可以用 AOP 實現。
為什么要面向切面呢,我直接改源代碼不是很好嗎?當然沒有問題,如果情況允許。但是考慮到下面這些情況,我本來寫好了1000個方法,有一天,我想加入一些控制,我想在執行方法邏輯之前,檢查一些系統參數,參數檢查沒問題再執行邏輯,否則不執行。這種情況怎么辦呢,難道要修改這1000個方法嗎,那簡直就是災難。還有,有些線上邏輯執行緩慢,但我又不想重新部署環境,因為那樣會影響線上業務,這種情況下,也可以考慮 AOP 方式,Btrace 就是這樣一個線上性能排查的神器。
Spring AOP 的用法
面向切面編程,名字好像很炫酷,但是使用方式已經被 Spring 封裝的非常簡單,只需要簡單的配置即可實現。使用方式不是本文介紹的重點,下面僅演示最簡單最基礎的使用,實現對調用的方法進行耗時計算,並打印出來。
環境說明: JDK 1.8 ,Spring mvc 版本 4.3.2.RELEASE
1. 首先引用 Spring mvc 相關的 maven 包,太多了,就不列了,只列出 Spring-aop 相關的
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version> 4.3.2.RELEASE </version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.8.9</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.9</version> </dependency>
2. 在 Spring mvc 配置文件中增加關於 AOP 的配置,內容如下:
<?xml version="1.0" encoding="UTF-8"?> <beans default-lazy-init="true" xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"> <!-- 自動掃描與裝配bean --> <context:component-scan base-package="kite.lab.spring"></context:component-scan> <!-- 啟動 @AspectJ 支持 --> <aop:aspectj-autoproxy proxy-target-class="true" /> </bean>
3. 創建切面類,並在 kite.lab.spring.service 包下的方法設置切面,使用 @Around 注解監控,實現執行時間的計算並輸出,內容如下:
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import org.springframework.util.StopWatch; @Component @Aspect public class PerformanceMonitor { //配置切入點,該方法無方法體,主要為方便同類中其他方法使用此處配置的切入點 @Pointcut("execution(* kite.lab.spring.service..*(..))") public void aspect(){ } @Around("aspect()") public Object methodTime(ProceedingJoinPoint pjp) throws Throwable { StopWatch stopWatch = new StopWatch(); stopWatch.start(); // 開始 Object retVal = pjp.proceed(); stopWatch.stop(); // 結束 System.out.println(String.format("方法 %s 耗時 %s ms!",pjp.getSignature().toShortString(), stopWatch.getTotalTimeMillis())); return retVal; } }
4. 被切面監控的類定義如下:
package kite.lab.spring.service; public class Worker { public String dowork(){ System.out.println("生活向來不易,我正在工作!"); return ""; } }
5. 加載 Spring mvc 配置文件,並調用 Worker 類的方法
public static void main(String[] args) { String filePath = "spring-servlet.xml"; ApplicationContext ac = new FileSystemXmlApplicationContext(filePath); Worker worker = (Worker) ac.getBean("worker"); worker.dowork(); }
6. 顯示結果如下:
說完用法,接下來說一下實現原理,知其然也要知其所以然。
Spring AOP 原理
AOP 的實現原理就是動態的生成代理類,代理類的執行過程為:執行我們增加的代碼(例如方法日志記錄)—-> 回調原方法 ——> 增加的代碼邏輯。看圖比較好理解:
Spring AOP 動態代理可能采用 JDK 動態代理或 CGlib 動態生成代理類兩種方式中的一種, 決定用哪一種方式的判斷標准就是被切面的類是否有其實現的接口,如果有對應的接口,則采用 JDK 動態代理,否則采用 CGlib 字節碼生成機制動態代理方式。
代理模式是一種常用的設計模式,其目的就是為其他對象提供一個代理以控制對某個對象的訪問。代理類負責為委托類預處理消息,過濾消息並轉發消息,以及進行消息被委托類執行后的后續處理。代理類和委托類實現相同的接口,所以調用者調用代理類和調用委托類幾乎感覺不到差別。
是不是看完了定義,感覺正好可以解決切面編程方式要解決的問題。下圖是基本的靜態代理模式圖:
而動態代理的意思是運行時動態生成代理實現類,由於 JVM 的機制,需要直接操作字節碼,生成新的字節碼文件,也就是 .class
文件。
JDK 動態代理
JDK 動態代理模式采用 sun 的 ProxyGenerator 的字節碼框架。要說明的是,只有實現了接口的類才能使用 JDK 動態代理技術,實現起來也比較簡單。
1. 只要實現 InvocationHandler
接口,並覆寫 invoke
方法即可。具體實現代碼如下:
package kite.lab.spring.aop.jdkaop; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; /** * JdkProxy * * @author fengzheng */ public class JdkProxy implements InvocationHandler { private Object target; /** * 綁定委托對象並返回一個代理類 * * @param target * @return */ public Object bind(Object target) { this.target = target; //取得代理對象 return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } /** * 調用方法 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; System.out.println("事物開始"); //執行方法 result = method.invoke(target, args); System.out.println("事物結束"); return result; } }
Proxy.newProxyInstance 方法用於動態生成實際生成的代理類,三個參數依次為被代理類的類加載器、被代理類所實現的接口和當前代理攔截器。
覆寫的 invoke 中可以加入我們增加的業務邏輯,然后回調原方法。
2. 被代理的類仍然用的前面 spring aop 介紹的那個worker 類,只不過我們需要讓這個類實現自接口,接口定義如下:
package kite.lab.spring.service; /** * IWorker * **/ public interface IWorker { String dowork(); }
3. 實際調用如下:
public static void main(String[] args) { JdkProxy jdkProxy = new JdkProxy(); IWorker worker = (IWorker) jdkProxy.bind(new Worker()); worker.dowork(); }
原理說明: jdkProxy.bind 會生成一個實際的代理類,這個生成過程是利用的字節碼生成技術,生成的代理類實現了IWorker 接口,我們調用這個代理類的 dowork 方法的時候,實際在代理類中是調用了 JdkProxy (也就是我們實現的這個代理攔截器)的 invoke 方法,接着執行我們實現的 invoke 方法,也就執行了我們加入的邏輯,從而實現了切面編程的需求。
我們把動態生成的代理類字節碼文件反編譯一下,也就明白了。由於代碼較長,只摘出相關部分。
首先看到類的接口和繼承關系:
public final class $Proxy0 extends Proxy implements IWorker
代理類被命名為 $Proxy0 ,繼承了 Proxy ,並且實現了IWorker ,這是關鍵點。
找到 dowork
方法,代碼如下:
public final String dowork() throws { try { return (String)super.h.invoke(this, m3, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } }
super.h 就是我們實現的JdkProxy 這個類,可以看到調用了這個類的 invoke 方法,並且傳入了參數 m3 ,再來看 m3 是什么
m3 = Class.forName("kite.lab.spring.service.IWorker").getMethod("dowork", new Class0);
看到了吧,m3 就是 dowork 方法,是不是流程就明確了。
但是,並不是所有的被代理的類(要被切面的類)都實現了某個接口,沒有實現接口的情況下,JDK 動態代理就不行了,這時候就要用到 CGlib 字節碼框架了。
CGLIB 動態代理
CGlib庫使用了ASM這一個輕量但高性能的字節碼操作框架來轉化字節碼,它可以在運行時基於一個類動態生成它的子類。厲害了吧,不管有沒有接口,凡是類都可以被繼承,擁有這樣的特點,原則上來說,它可以對任何類進行代碼攔截,從而達到切面編程的目的。
CGlib 不需要我們非常了解字節碼文件(.class 文件)的格式,通過簡單的 API 即可實現字節碼操作。
基於這樣的特點,CGlib 被廣泛用於如 Spring AOP 等基於 代理模式的AOP框架中。
下面就基於 CGlib 實現一個簡單的動態代理模式。
1. 創建攔截類實現 MethodInterceptor
接口,並覆intercept
方法,在此方法中加入我們增加的邏輯,代碼如下:
public class MyAopWithCGlib implements MethodInterceptor { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("嘿,你在干嘛?"); methodProxy.invokeSuper(o, objects); System.out.println("是的,你說的沒錯。"); return null; }
2. 被代理的類依然是上面的 Worker 類,並且不需要接口。
3. 客戶端調用代理方法的代碼如下:
public static void main(String[] args) { System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "cglib"); MyAopWithCGlib aop = new MyAopWithCGlib(); Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(Worker.class); enhancer.setCallback(aop); Worker worker = (Worker) enhancer.create(); worker.dowork(); }
代碼第一行是要將動態生成的字節碼文件持久化到磁盤,方便反編譯觀察。
利用 CGlib 的 Enhancer 對象,設置它的繼承父類,設置回調類,即上面實現的 攔截類,然后用create 方法創造一個 Worker 類,實際這個類是 Worker 類的子類,然后調用dowork方法。執行結果如下:
可以看到我們織入的代碼起作用了。
4. 上面功能比較簡單,它會橫向切入被代理類的所有方法中,下來我們稍微做的復雜一點。控制一下,讓有些方法被織入代碼,有些不被織入代碼,模仿 Spring aop ,我們新增一個注解,用於注解哪些方法要被橫向切入。注解如下:
package kite.lab.spring.aop.AopWithCGlib; import java.lang.annotation.*; /** * CGLIB * * @author fengzheng */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface CGLIB { String value() default ""; }
5. 然后再 Worker 中增加一個方法,並應用上面的注解
package kite.lab.spring.service; import kite.lab.spring.aop.AopWithCGlib.CGLIB; /** * Worker * * @author fengzheng */ public class Worker { public String dowork(){ System.out.println("生活向來不易,我正在工作!"); return ""; } @CGLIB(value = "cglib") public void dowork2(){ System.out.println("生活如此艱難,我在奔命!"); } }
我們在 dowrok2 上應用了上面的注解
6. 在攔截方法中加入注解判斷邏輯,如果加了上面的注解,就加入織入的代碼邏輯,否則不加入,代碼如下:
@Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { Annotation[] annotations = method.getDeclaredAnnotations(); boolean isCglib = false; for(Annotation annotation: annotations){ if (annotation.annotationType().getName().equals("kite.lab.spring.aop.AopWithCGlib.CGLIB")){ isCglib = true; } } if(isCglib) { System.out.println("嘿,你在干嘛?"); methodProxy.invokeSuper(o, objects); System.out.println("是的,你說的沒錯。"); } return null; }
7. 調用方法如下:
public static void main(String[] args) { System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "cglib"); MyAopWithCGlib aop = new MyAopWithCGlib(); Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(Worker.class); enhancer.setCallback(aop); Worker worker = (Worker) enhancer.create(); worker.dowork(); worker.dowork2(); }
執行結果應該為 dowork 不執行被織入的邏輯,dowork2 執行被織入的代碼邏輯,執行結果如下:
另外說一下,CGlib 不支持 final 類, CGlib 的執行速度比較快,但是創建速度比較慢,所以如果兩種動態代理都適用的場景下,有大量動態代理類創建的場景下,用 JDK 動態代理模式,否則可以用 CGlib 。
標准的 Spring MVC 框架,一般都是一個服務接口類對應一個實現類,所以根據Spring AOP 的判斷邏輯,應該大部分情況下都是使用的 JDK 動態代理模式。當然也可以手動改成 CGlib 模式。
古時的風箏 【微信公眾號】gushidefengzheng