微信公眾號:一個優秀的廢人。如有問題,請后台留言,反正我也不會聽。
最近在復習 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,動態地在內存中構建代理對象(需要我們傳入被代理類),並且默認實現所有目標方法。
源碼下載:https://github.com/turoDog/review_java.git
最后
如果看到這里,喜歡這篇文章的話,請轉發、點贊。微信搜索「一個優秀的廢人」,歡迎關注。
回復「1024」送你一套完整的 java、python、c++、go、前端、linux、算法、大數據、人工智能、小程序以及英語教程。
回復「電子書」送你 50+ 本 java 電子書。