本文首發於 jaychen.cc
做者 jaychenjava
最近在學 Spring,研究了下 AOP 和代理模式,寫點心得和你們分享下。sql
先說下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。ide
那麼,AOP 是如何解決這類問題?函數
一般,咱們會使用代理模式來實現 AOP,這就意味着代理模式能夠優雅的解決侵入性業務問題。因此下面來重點分析下代理模式。性能
這個是代理模式的類圖。不少人可能看不懂類圖,可是說實話有時候一圖勝千言,這裏稍微解釋下類圖的含義,尤爲是類圖中存在的幾種連線符。測試
is=a
的關係,表示繼承,因此上圖中 TestSQL
和 Performance
都實現 IDatabase
接口。Performance
類有一個 TestSQL
的成員變量。有了類圖,咱們能夠根據類圖直接寫出代理模式的代碼了。這裏代理模式分爲靜態代理和動態代理兩種,咱們分別來看下。this
假設一個場景,咱們須要測試一條 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 的動態代理只能爲接口建立代理。