本文首發於 jaychen.cc
作者 jaychen
最近在學 Spring,研究了下 AOP 和代理模式,寫點心得和大家分享下。
AOP
先說下AOP,AOP 全稱 Aspect Oriented Programming,面向切面程式設計,和 OOP 一樣也是一種程式設計思想。AOP 出現的原因是為了解決 OOP 在處理 侵入性業務上的不足。
那麼,什麼是侵入性業務?類似日誌統計、效能分析等就屬於侵入性業務。本來原本的業務邏輯程式碼優雅大氣,正常執行,突然說需要在這段邏輯裡面加上效能分析,於是程式碼就變成了下面這個樣子
long begin = System.currentTimeMillis();
// 原本的業務
doSomething();
long end = System.currentTimeMillis();
long step = end - begin;
System.out.println("執行花費 :" + step);複製程式碼
從上面的程式碼看到,效能分析的業務程式碼和原本的業務程式碼混在了一起,好端端的程式碼就這麼被糟蹋了。所以,侵入性業務必須有一個更好的解決方案,這個解決方案就是 AOP。
那麼,AOP 是如何解決這類問題?
代理模式
通常,我們會使用代理模式來實現 AOP,這就意味著代理模式可以優雅的解決侵入性業務問題。所以下面來重點分析下代理模式。
這個是代理模式的類圖。很多人可能看不懂類圖,但是說實話有時候一圖勝千言,這裡稍微解釋下類圖的含義,尤其是類圖中存在的幾種連線符。
- 矩形代表一個類,矩形內部的資訊有:類名,屬性和方法。
- 虛線 + 三角空心箭頭為
is=a
的關係,表示繼承,所以上圖中TestSQL
和Performance
都實現IDatabase
介面。 - 實線 + 箭頭為關聯關係,一般在程式碼中以成員變數的形式體現,所以上圖中
Performance
類有一個TestSQL
的成員變數。
有了類圖,我們可以根據類圖直接寫出代理模式的程式碼了。這裡代理模式分為靜態代理和動態代理兩種,我們分別來看下。
靜態代理
假設一個場景,我們需要測試一條 sql query 執行所花費的時間。
如果按照普通的方式,程式碼邏輯應該如下
long begin = System.currentTimeMillis();
query();
long end = System.currentTimeMillis();
long step = end - begin;
System.out.println("執行花費 :" + step);複製程式碼
上面說過了,這種會導致查詢邏輯和效能測試邏輯混淆在一塊,那麼來看看使用代理模式是如何解決這個問題的。
代理模式,代理,意味著有一方代替另一方完成一件事。這裡,我們會編寫兩個類:TestSQL
為query 執行邏輯,Performance
為效能測試類。這裡 Performance
會代替 TestSQL
去執行 query 邏輯。
要想 Performance
能夠代替 TestSQL
執行 query 邏輯,那麼這兩個類應該是有血緣關係的,即這兩個必須實現同一個介面。
// 介面
public interface IDatabase {
void query();
}
public class TestSQL implements IDatabase {
@Override
public void query() {
System.out.println("執行 query。。。。");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 代理類
public class PerformanceMonitor implements IDatabase {
TestSQL sql;
public PerformanceMonitor(TestSQL sql) {
this.sql = sql;
}
@Override
public void query() {
long begin = System.currentTimeMillis();
// 業務邏輯。
sql.query();
long end = System.currentTimeMillis();
long step = end - begin;
System.out.println("執行花費 : " + step);
}
}
// 測試程式碼
public class Main {
public static void main(String[] strings) {
TestSQL sql = new TestSQL();
PerformanceMonitor performanceMonitor = new PerformanceMonitor(sql);
// 由 Performance 代替 testSQL 執行
performanceMonitor.query();
}
}複製程式碼
從上面的示例程式碼可以分析出來代理模式是如何運作的,這裡我們可以很明顯看出代理模式的優越性,TestSQL
的邏輯很純粹,沒有混入其他無關的業務程式碼。
動態代理
回顧靜態代理的程式碼,發現代理類 Performance
必須實現 IDatabase
介面。如果有很多業務需要用到代理來實現,那麼每個業務都需要定義一個代理類,這會導致類迅速膨脹,為了避免這點,Java 提供了動態代理。
為何稱之為動態代理,動態代理底層是使用反射實現的,是在程式執行期間動態的建立介面的實現。在靜態代理中,我們需要在編碼的時候編寫 Performance
類實現 IDatabase
介面。而使用動態代理,我們不必編寫 Performance
實現 IDatabase
介面,而是 JDK 在底層通過反射技術動態建立一個 IDatabase
介面的實現。
使用動態代理需要使用到 InvocationHandler
和 Proxy
這兩個類。
// 代理類,不再實現 IDatabase 介面,而是實現 InvocationHandler 介面
public class Performance implements InvocationHandler {
private TestSQL sql;
public Performance(TestSQL sql) {
this.sql = sql;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long begin = System.currentTimeMillis();
// method.invoke 實際上就是呼叫 sql.query()
Object object = method.invoke(sql, args);
long end = System.currentTimeMillis();
long step = end - begin;
System.out.println("執行花費 :" + step);
return object;
}
}
public class Main {
public static void main(String[] strings) {
TestSQL sql = new TestSQL();
Performance performance = new Performance(sql);
IDatabase proxy = (IDatabase) Proxy.newProxyInstance(
sql.getClass().getClassLoader(),
sql.getClass().getInterfaces(),
performance
);
proxy.query();
}
}複製程式碼
先來看看 newProxyInstance
函式,這個函式的作用就是用來動態建立一個代理物件的類,這個函式需要三個引數:
- 第一個引數為類載入器,如果不懂是什麼玩意,先套著模板寫,等我寫下一篇文章拯救你。
- 第二個引數為要代理的介面,在這個例子裡面就是
IDatabase
介面。 - 第三個引數為實現
InvocationHandler
介面的物件。
執行 newProxyInstance
之後,Java 會在底層自動生成一個代理類,其程式碼大概如下:
public final class $Proxy1 extends Proxy implements IDatabase{
private InvocationHandler h;
private $Proxy1(){}
public $Proxy1(InvocationHandler h){
this.h = h;
}
public void query(){
////建立method物件
Method method = Subject.class.getMethod("query");
//呼叫了invoke方法
h.invoke(this, method, new Object[]{});
}
}複製程式碼
你會發現,這個類很像在靜態代理中的 Performance
類,是的,動態代理其本質是 Java 自動為我們生成了一個 $Proxy1
代理類。在 mian
函式中 newProxyInstance
的返回值就是該類的一個例項。並且,$Proxy1
中的 h
屬性就是 newProxyInstance
的第三個引數。所以,當我們在 main
函式中執行 proxy.query()
,實際上是呼叫 $proxy1#query
方法,進而再呼叫 Performance#invoke
方法。而在 Performance#invoke
通過 Object object = method.invoke(sql, args);
呼叫了 TestSQL#query
方法。
回顧上面的流程,理解動態代理的核心在於理解 Java 自動生成的代理類。這裡還有一點要說明,JDK 的動態代理有一個不足:它只能為介面建立代理例項。這句話體現在程式碼上就是 newProxyInstance
的第二個引數是一個介面陣列。為什麼會存在這個不足?其實看 $Proxy1
代理類就知道了,這個由 JDK 生成的代理類需要繼承 Proxy
類,而 Java 只支援單繼承,所以就限制了 JDK 的動態代理只能為介面建立代理。