微信公眾號:一個優秀的廢人。如有問題,請後臺留言,反正我也不會聽。
最近在複習 Java 相關,回顧了下代理模式。代理模式在 Java 領域很多地方都有應用,它分為靜態代理和動態代理,其中 Spring AOP 就是動態代理的典型例子。動態代理又分為介面代理和 cglib (子類代理),結合我的理解寫了幾個 demo 分享給你們,這是昨晚修仙到 3 點寫出來的文章,不點在看,我覺得說不過去了。
代理模式在我們日常中很常見,生活處處有代理:
- 看張學友的演唱會很難搶票,可以找黃牛排隊買
- 嫌出去吃飯麻煩,可以叫外賣
無論是黃牛、外賣騎手都得幫我們幹活。但是他們不能一手包辦(比如黃牛不能幫我吃飯),他們只能做我們不能或者不想做的事。
- 找黃牛可以幫我排隊買上張學友的演唱會門票
- 外賣騎手可以幫我把飯送到樓下
所以,你看。代理模式其實就是當前物件不願意做的事情,委託給別的物件做。
靜態代理
我還是以找黃牛幫我排隊買張學友的演唱會門票的例子,寫個 demo 說明。現在有一個 Human 介面,無論是我還是黃牛都實現了這個介面。
public interface Human {
void eat();
void sleep();
void lookConcert();
}
複製程式碼
例如,我這個類,我會吃飯和睡覺,如以下類:
public class Me implements Human{
@Override
public void eat() {
System.out.println("eat emat ....");
}
@Override
public void sleep() {
System.out.println("Go to bed at one o'clock in the morning");
}
@Override
public void lookConcert() {
System.out.println("Listen to Jacky Cheung's Concert");
}
}
複製程式碼
有黃牛類,例如:
public class Me implements Human{
@Override
public void eat() {
}
@Override
public void sleep() {
}
@Override
public void lookConcert() {
}
}
複製程式碼
現在我和黃牛都已經準備好了,怎麼把這二者關聯起來呢?我們要明確的是黃牛是要幫我買票的,買票必然就需要幫我排隊,於是有以下黃牛類:注意這裡我們不關心,黃牛的其他行為,我們只關心他能不能排隊買票。
public class HuangNiu implements Human{
private Me me;
public HuangNiu() {
me = new Me();
}
@Override
public void eat() {
}
@Override
public void sleep() {
}
@Override
public void lookConcert() {
// 新增排隊買票方法
this.lineUp();
me.lookConcert();
}
public void lineUp() {
System.out.println("line up");
}
}
複製程式碼
最終的 main 方法呼叫如下:
public class Client {
public static void main(String[] args) {
Human human = new HuangNiu();
human.lookConcert();
}
}
複製程式碼
結果如下:
由此可見,黃牛就只是做了我們不願意做的事(排隊買票),實際看演唱會的人還是我。客戶端也並不關心代理類代理了哪個類,因為程式碼控制了客戶端對委託類的訪問。客戶端程式碼表現為 Human human = new HuangNiu();
由於代理類實現了抽象角色的介面,導致代理類無法通用。比如,我的狗病了,想去看醫生,但是排隊掛號很麻煩,我也想有個黃牛幫我的排隊掛號看病,但是黃牛它不懂這隻狗的特性(黃牛跟狗不是同一型別,黃牛屬於 Human 但狗屬於 Animal 類)但排隊掛號和排隊買票相對於黃牛來說它兩就是一件事,這個方法是不變的,現場排隊。那我們能不能找一個代理說既可以幫人排隊買票也可以幫狗排隊掛號呢?
答案肯定是可以的,可以用動態代理。
基於介面的動態代理
如靜態代理的內容所描述的,靜態代理受限於介面的實現。動態代理就是通過使用反射,動態地獲取抽象介面的型別,從而獲取相關特性進行代理。因動態代理能夠為所有的委託方進行代理,因此給代理類起個通用點的名字 HuangNiuHandle。先看黃牛類可以變成什麼樣?
public class HuangNiuHandle implements InvocationHandler {
private Object proxyTarget;
public Object getProxyInstance(Object target) {
this.proxyTarget = target;
return Proxy.newProxyInstance(proxyTarget.getClass().getClassLoader(), proxyTarget.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object methodObject = null;
System.out.println("line up");
methodObject = method.invoke(proxyTarget, args);
System.out.println("go home and sleep");
return methodObject;
}
}
複製程式碼
這個時候的客戶端程式碼就變成這樣了
public class Client {
public static void main(String[] args) {
HuangNiuHandle huangNiuHandle = new HuangNiuHandle();
Human human = (Human) huangNiuHandle.getProxyInstance(new Me());
human.eat();
human.run();
human.lookConcert();
System.out.println("------------------");
Animal animal = (Animal) huangNiuHandle.getProxyInstance(new Dog());
animal.eat();
animal.run();
animal.seeADoctor();
}
}
複製程式碼
使用動態代理有三個要點,
-
必須實現 InvocationHandler 介面,表明該類是一個動態代理執行類。
-
InvocationHandler 介面內有一實現方法如下: public Object invoke(Object proxy, Method method, Object[] args) 。使用時需要重寫這個方法
-
獲取代理類,需要使用 Proxy.newProxyInstance(Clas loader, Class<?>[] interfaces, InvocationHandler h) 這個方法去獲取Proxy物件(Proxy 類型別的例項)。
注意到 Proxy.newProxyInstance 這個方法,它需要傳入 3 個引數。解析如下:
// 第一個引數,是類的載入器
// 第二個引數是委託類的介面型別,證代理類返回的是同一個實現介面下的型別,保持代理類與抽象角色行為的一致
// 第三個引數就是代理類本身,即告訴代理類,代理類遇到某個委託類的方法時該呼叫哪個類下的invoke方法
Proxy.newProxyInstance(Class loader, Class<?>[] interfaces, InvocationHandler h)
複製程式碼
再來看看 invoke 方法,使用者呼叫代理物件的什麼方法,實質上都是在呼叫處理器的 invoke 方法,通過該方法呼叫目標方法,它也有三個引數:
// 第一個引數為 Proxy 類型別例項,如匿名的 $proxy 例項
// 第二個引數為委託類的方法物件
// 第三個引數為委託類的方法引數
// 返回型別為委託類某個方法的執行結果
public Object invoke(Object proxy, Method method, Object[] args)
複製程式碼
呼叫該代理類之後的輸出結果:
由結果可知,黃牛不僅幫了(代理)我排隊買票,還幫了(代理)我的狗排隊掛號。所以,你看靜態代理需要自己寫代理類(代理類需要實現與目標物件相同的介面),還需要一一實現介面方法,但動態代理不需要。
注意,我們並不是所有的方法都需要黃牛這個代理去排隊。我們知道只有我看演唱會和我的狗去看醫生時,才需要黃牛,如果要實現我們想要的方法上面新增特定的代理,可以通過 invoke 方法裡面的方法反射獲取 method 物件方法名稱即可實現,所以動態代理類可以變成這樣:
public class HuangNiuHandle implements InvocationHandler {
private Object proxyTarget;
public Object getProxyInstance(Object target) {
this.proxyTarget = target;
return Proxy.newProxyInstance(proxyTarget.getClass().getClassLoader(), proxyTarget.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object methodObject = null;
if ("lookConcert".equals(method.getName()) ||
"seeADoctor".equals(method.getName())) {
System.out.println("line up");
// 呼叫目標方法
methodObject = method.invoke(proxyTarget, args);
} else {
// 不使用第一個proxy引數作為引數,否則會造成死迴圈
methodObject = method.invoke(proxyTarget, args);
}
return methodObject;
}
}
複製程式碼
結果如下:可以看到我們只在特定方法求助了黃牛
由此可見,動態代理一般應用在記錄日誌等橫向業務。
值得注意的是:
-
基於介面類的動態代理模式,必須具備抽象角色、委託類、代理三個基本角色。委託類和代理類必須由抽象角色衍生出來,否則無法使用該模式。
-
動態代理模式最後返回的是具有抽象角色(頂層介面)的物件。在委託類內被 private 或者 protected 關鍵修飾的方法將不會予以呼叫,即使允許呼叫。也無法在客戶端使用代理類轉換成子類介面,對方法進行呼叫。也就是說上述的動態代理返回的是委託類(Me)或 (Dog)的就介面物件 (Human)或 (Animal)。
-
在 invoke 方法內為什麼不使用第一個引數進行執行回撥。在客戶端使用getProxyInstance(new Child( ))時,JDK 會返回一個 proxy 的例項,例項內有InvokecationHandler 物件及動態繼承下來的目標 。客戶端呼叫了目標方法,有如下操作:首先 JDK 先查詢 proxy 例項內的 handler 物件 然後執行 handler 內的 invoke 方法。
根據 public Object invoke 這個方法第一個引數 proxy 就是對應著 proxy 例項。如果在 invoke 內使用 method.invoke(proxy,args) ,會出現這樣一條方法鏈,目標方法→invoke→目標方法→invoke...,最終導致堆疊溢位。
基於子類的動態代理
為了省事,我這裡並沒有繼承父類,但在實際開發中是需要繼承父類才比較方便擴充套件的。與基於介面實現類不同的是:
- CGLib (基於子類的動態代理)使用的是方法攔截器 MethodInterceptor ,需要匯入 cglib.jar 和 asm.jar 包
- 基於子類的動態代理,返回的是子類物件
- 方法攔截器對 protected 修飾的方法可以進行呼叫
程式碼如下:
public class Me {
public void eat() {
System.out.println("eat meat ....");
}
public void run() {
System.out.println("I run with two legs");
}
public void lookConcert() {
System.out.println("Listen to Jacky Cheung's Concert");
}
protected void sleep() {
System.out.println("Go to bed at one o'clock in the morning");
}
}
複製程式碼
Dog 類
public class Dog {
public void eat() {
System.out.println("eat Dog food ....");
}
public void run() {
System.out.println("Dog running with four legs");
}
public void seeADoctor() {
System.out.println("The dog go to the hospital");
}
}
複製程式碼
黃牛代理類,注意 invoke() 這裡多了一個引數 methodProxy ,它的作用是用於執行目標(委託類)的方法,至於為什麼用 methodProxy ,官方的解釋是速度快且在intercep t內呼叫委託類方法時不用儲存委託物件引用。
public class HuangNiuHandle implements MethodInterceptor {
private Object proxyTarget;
public Object getProxyInstance(Object target) {
this.proxyTarget = target;
return Enhancer.create(target.getClass(), target.getClass().getInterfaces(), this);
}
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object methodObject = null;
if ("lookConcert".equals(method.getName()) ||
"seeADoctor".equals(method.getName())) {
System.out.println("line up");
// 呼叫目標方法
methodObject = methodProxy.invokeSuper(proxy, args);
} else {
methodObject = method.invoke(proxyTarget, args);
}
return methodObject;
}
}
複製程式碼
client 類
public class Client {
public static void main(String[] args) {
HuangNiuHandle huangNiuHandle = new HuangNiuHandle();
Me me = (Me) huangNiuHandle.getProxyInstance(new Me());
me.eat();
me.run();
me.sleep();
me.lookConcert();
System.out.println("------------------");
Dog dog = (Dog) huangNiuHandle.getProxyInstance(new Dog());
dog.eat();
dog.run();
dog.seeADoctor();
}
}
複製程式碼
結果:
注意到 Me 類中被 protected 修飾的方法 sleep 仍然可以被客戶端呼叫。這在基於介面的動態代理中是不被允許的。
靜態代理與動態代理的區別
靜態代理需要自己寫代理類並一一實現目標方法,且代理類必須實現與目標物件相同的介面。
動態代理不需要自己實現代理類,它是利用 JDKAPI,動態地在記憶體中構建代理物件(需要我們傳入被代理類),並且預設實現所有目標方法。
原始碼下載:github.com/turoDog/rev…
後語
如果本文對你哪怕有一丁點幫助,請幫忙點好看,你的好看是我堅持寫作的動力。關注公眾號一個優秀的廢人回覆 1024 獲取資料:Python、C++、Java、Linux、Go、前端、演算法資料分享