<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在學習代理之前,先回顧以下我們的常規編碼方式:所有 interface
型別的變數總是通過向上轉型並指向某個範例的。
1)首先,定義一個介面:
public interface SmsService { String send(String message); }
2)然後編寫其實現類:
public class SmsServicseImpl implements SmsService { public String send(String message) { System.out.println("send message:" + message); return message; } }
3)最後建立該實現類的範例,轉型為介面並呼叫:
SmsService s = new SmsServicseImpl(); s.send("Java");
上述這種方式就是我們通常編寫程式碼的方式。而代理模式和這種方式有很大的區別,且看下文。
簡單來說,代理模式就是 使用代理物件來代替對真實物件的存取,這樣就可以在不修改原目標物件的前提下,提供額外的功能操作,擴充套件目標物件的功能。
代理模式大致有三種角色:
通俗來說,代理模式的主要作用是擴充套件目標物件的功能,比如說在目標物件的某個方法執行前後你可以增加一些額外的操作,並且不用修改這個方法的原有程式碼。如果大家學過 Spring 的 AOP,一定能夠很好的理解這句話。
舉個例子:你找了小紅來幫你向小綠問話,小紅就看作是代理我的代理類 Proxy,而你是 Real Subject,因為小紅要傳達的話其實是你說的。那麼你和小紅都需要實現的介面(Subject)就是說話,由於你倆都能說話,在外界看來你倆就是一樣的(滑稽,大家理解就好,不用較真)
看到這裡,不知道大家能不能理解了為什麼委託類和代理類都需要實現相同的介面?
那是為了保持行為的一致性,在存取者看來兩者之間就沒有區別。這樣,通過代理類這個中間層,很好地隱藏和保護了委託類物件,能有效遮蔽外界對委託類物件的直接存取。同時,也可以在代理類上加上額外的操作,比如小紅在說話之前會跳一段舞,外界就會覺得你在說話前會跳一段舞,所以,這就實現了委託類的功能增強。
代理模式有靜態代理和動態代理兩種實現方式。
先來看靜態代理的實現步驟:
1)定義一個介面(Subject)
2)建立一個委託類(Real Subject)實現這個介面
3)建立一個代理類(Proxy)同樣實現這個介面
4)將委託類 Real Subject 注入進代理類 Proxy,在代理類的方法中呼叫 Real Subject 中的對應方法。這樣的話,我們就可以通過代理類遮蔽對目標物件的存取,並且可以在目標方法執行前後做一些自己想做的事情。
從實現和應用角度來說,靜態代理中,我們對目標物件的每個方法的增強都是手動完成的,非常不靈活(比如介面一旦新增加方法,目標物件和代理物件都要進行修改)且麻煩(需要對每個目標類都單獨寫一個代理類)。 實際應用場景非常非常少,日常開發幾乎看不到使用靜態代理的場景。
從 JVM 層面來說, 靜態代理在編譯時就將介面、委託類、代理類這些都變成了一個個實際的 .class
檔案。
1)定義傳送簡訊的介面
public interface SmsService { String send(String message); }
2)建立一個委託類(Real Subject)實現這個介面
public class SmsServiceImpl implements SmsService { public String send(String message) { System.out.println("send message:" + message); return message; } }
3)建立一個代理類(Proxy)同樣實現這個介面
4)將委託類 Real Subject 注入進代理類 Proxy,在代理類的方法中呼叫 Real Subject 中的對應方法。這樣的話,我們就可以通過代理類遮蔽對目標物件的存取,並且可以在目標方法執行前後做一些自己想做的事情。
public class SmsProxy implements SmsService { // 將委託類注入進代理類 private final SmsService smsService; public SmsProxy(SmsService smsService) { this.smsService = smsService; } @Override public String send(String message) { // 呼叫委託類方法之前,我們可以新增自己的操作 System.out.println("before method send()"); // 呼叫委託類方法 smsService.send(message); // 呼叫委託類方法之後,我們同樣可以新增自己的操作 System.out.println("after method send()"); return null; } }
那麼,如何使用這個被增強的 send
方法呢?
public class Main { public static void main(String[] args) { SmsService smsService = new SmsServiceImpl(); SmsProxy smsProxy = new SmsProxy(smsService); smsProxy.send("Java"); } }
執行上述程式碼之後,控制檯列印出:
before method send()
send message:java
after method send()
從輸出結果可以看出,我們已經增強了委託類 SmsServiceImpl
的 send()
方法。
當然,從上述程式碼我們也能看出來,靜態代理存在一定的弊端。假如說我們現在新增了一個委託類實現了 SmsService
介面,如果我們想要對這個委託類進行增強,就需要重新寫一個代理類,然後注入這個新的委託類,非常不靈活。也就是說靜態代理是一個委託了對應一個代理類,能不能將代理類做成一個通用的呢?為此,動態代理應用而生。
在講解動態之前,我們有必要詳細說一下 .class
位元組碼檔案這個東西。動態代理機制和 Java 位元組碼生成框架息息相關。
在上文反射中我們提到,一個 Class
類對應一個 .class
位元組碼檔案,也就說位元組碼檔案中儲存了一個類的全部資訊。位元組碼其實是二進位制檔案,內容是隻有 JVM 能夠識別的機器碼。
解析過程這樣的:JVM 讀取 .class
位元組碼檔案,取出二進位制資料,載入到記憶體中,解析位元組碼檔案內的資訊,生成對應的 Class
類物件:
顯然,上述這個過程是在編譯期就發生的。
那麼,由於JVM 是通過 .class
位元組碼檔案(也就是二進位制資訊)載入類的,如果我們在執行期遵循 Java 編譯系統組織 .class
位元組碼檔案的格式和結構,生成相應的二進位制資料,然後再把這個二進位制資料載入轉換成對應的類。這樣,我們不就完成了在執行時動態的建立一個類。這個思想其實也就是動態代理的思想。
在執行時期按照 JVM 規範對 .class
位元組碼檔案的組織規則,生成對應的二進位制資料。當前有很多開源框架可以完成這個功能,如
需要注意的是,CGLIB 是基於 ASM 的。 這裡簡單對比一下 ASM 和 Javassist:
總的來說 ASM 比 Javassist 快得多,並且提供了更好的效能,但是 Javassist 相對來說更容易使用,兩者各有千秋。
以 Javassist 為例,我們來看看這些框架在執行時生成 .class
位元組碼檔案的強大能力。
正常來說,我們建立一個類的程式碼是這樣的:
package com.samples; public class Programmer { public void code(){ System.out.println("I'm a Programmer,Just Coding....."); } }
下面通過 Javassist 建立和上面一模一樣的 Programmer
類的位元組碼:
import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.CtNewMethod; public class MyGenerator { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); // 建立 Programmer 類 CtClass cc= pool.makeClass("com.samples.Programmer"); // 定義方法 CtMethod method = CtNewMethod.make("public void code(){}", cc); // 插入方法程式碼 method.insertBefore("System.out.println("I'm a Programmer,Just Coding.....");"); cc.addMethod(method); // 儲存生成的位元組碼 cc.writeFile("d://temp"); } }
通過反編譯工具開啟 Programmer.class
可以看到以下程式碼:
OK,瞭解了 Java 位元組碼生成框架,可以開始學習動態代理(Dynamic Proxy)了。
回顧一下靜態代理,我們把靜態代理的執行過程抽象為下圖:
可以看見,代理類無非是在呼叫委託類方法的前後增加了一些操作。委託類的不同,也就導致代理類的不同。
那麼為了做一個通用性的代理類出來,我們把呼叫委託類方法的這個動作抽取出來,把它封裝成一個通用性的處理類,於是就有了動態代理中的 InvocationHandler
角色(處理類)。
於是,在代理類和委託類之間就多了一個處理類的角色,這個角色主要是對代理類呼叫委託類方法的這個動作進行統一的呼叫,也就是由 InvocationHandler
來統一處理代理類呼叫委託類方法這個操作。看下圖:
從 JVM 角度來說,動態代理是在執行時動態生成 .class
位元組碼檔案 ,並載入到 JVM 中的。這個我們在 Java 位元組碼生成框架中已經提到過。
雖然動態代理在我們日常開發中使用的相對較少,但是在框架中的幾乎是必用的一門技術。學會了動態代理之後,對於我們理解和學習各種框架的原理也非常有幫助,Spring AOP、RPC 等框架的實現都依賴了動態代理。
就 Java 來說,動態代理的實現方式有很多種,比如:
下面詳細講解這三種動態代理機制。
先來看下 JDK 動態代理機制的使用步驟:
1)定義一個介面(Subject)
2)建立一個委託類(Real Subject)實現這個介面
3)建立一個處理類並實現 InvocationHandler
介面,重寫其 invoke
方法(在 invoke
方法中利用反射機制呼叫委託類的方法,並自定義一些處理邏輯),並將委託類注入處理類
該方法有下面三個引數:
proxy:代理類物件(見下一步)
method:還記得我們在上篇文章反射中講到的 Method.invoke
嗎?就是這個,我們可以通過它來呼叫委託類的方法(反射)
args:傳給委託類方法的參數列
4)建立代理物件(Proxy):通過 Proxy.newProxyInstance()
建立委託類物件的代理物件
這個方法需要 3 個引數:
InvocationHandler
範例處理介面方法(也就是第 3 步我們建立的類的範例)也就是說:我們在通過 Proxy
類的 newProxyInstance()
建立的代理物件在呼叫方法的時候,實際會呼叫到實現了 InvocationHandler
介面的處理類的 invoke()
方法,可以在 invoke()
方法中自定義處理邏輯,比如在方法執行前後做什麼事情。
1)定義一個介面(Subject)
public interface SmsService { String send(String message); }
2)建立一個委託類(Real Subject)實現這個介面
public class SmsServiceImpl implements SmsService { public String send(String message) { System.out.println("send message:" + message); return message; } }
3)建立一個處理類並實現 InvocationHandler
介面,重寫其 invoke
方法(在 invoke
方法中利用反射機制呼叫委託類的方法,並自定義一些處理邏輯),並將委託類注入處理類
import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class DebugInvocationHandler implements InvocationHandler { // 將委託類注入處理類(這裡我們用 Object 代替,方便擴充套件) private final Object target; public DebugInvocationHandler(Object target) { this.target = target; } // 重寫 invoke 方法 @Override public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException { //呼叫方法之前,我們可以新增自己的操作 System.out.println("before method " + method.getName()); Object result = method.invoke(target, args); //呼叫方法之後,我們同樣可以新增自己的操作 System.out.println("after method " + method.getName()); return result; } }
4)定義一個建立代理物件(Proxy)的工廠類:通過 Proxy.newProxyInstance()
建立委託類物件的代理物件
public class JdkProxyFactory { public static Object getProxy(Object target) { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new DebugInvocationHandler(target) ); } }
5)實際使用
SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl()); smsService.send("Java");
執行上述程式碼之後,控制檯列印出:
before method send
send message:Java
after method send
JDK 動態代理有一個最致命的問題是它只能代理實現了某個介面的實現類,並且代理類也只能代理介面中實現的方法,要是實現類中有自己私有的方法,而介面中沒有的話,該方法不能進行代理呼叫。
為了解決這個問題,我們可以用 CGLIB 動態代理機制。
上文也提到過,CGLIB(Code Generation Library)是一個基於 ASM 的 Java 位元組碼生成框架,它允許我們在執行時對位元組碼進行修改和動態生成。原理就是通過位元組碼技術生成一個子類,並在子類中攔截父類別方法的呼叫,織入額外的業務邏輯。關鍵詞大家注意到沒有,攔截!CGLIB 引入一個新的角色就是方法攔截器 MethodInterceptor
。和 JDK 中的處理類 InvocationHandler
差不多,也是用來實現方法的統一呼叫的。看下圖:
另外由於 CGLIB 採用繼承的方式,所以被代理的類不能被 final
修飾。
很多知名的開源框架都使用到了 CGLIB, 例如 Spring 中的 AOP 模組中:如果目標物件實現了介面,則預設採用 JDK 動態代理,否則採用 CGLIB 動態代理。
來看 CGLIB 動態代理的使用步驟:
1)首先建立一個委託類(Real Subject)
2)建立一個方法攔截器實現介面 MethodInterceptor
,並重寫 intercept
方法。intercept
用於攔截並增強委託類的方法(和 JDK 動態代理 InvocationHandler
中的 invoke
方法類似)
該方法擁有四個引數:
Method.invoke
了,而是使用 MethodProxy.invokeSuper
方法)3)建立代理物件(Proxy):通過 Enhancer.create()
建立委託類物件的代理物件
也就是說:我們在通過 Enhancer
類的 create()
建立的代理物件在呼叫方法的時候,實際會呼叫到實現了 MethodInterceptor
介面的處理類的 intercept()
方法,可以在 intercept()
方法中自定義處理邏輯,比如在方法執行前後做什麼事情。
可以發現,CGLIB 動態代理機制和 JDK 動態代理機制的步驟差不多,CGLIB 動態代理的核心是方法攔截器 MethodInterceptor
和 Enhancer
,而 JDK 動態代理的核心是處理類 InvocationHandler
和 Proxy
。
不同於 JDK 動態代理不需要額外的依賴。CGLIB 是一個開源專案,如果你要使用它的話,需要手動新增相關依賴。
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
1)首先建立一個委託類(Real Subject)
public class AliSmsService { public String send(String message) { System.out.println("send message:" + message); return message; } }
2)建立一個方法攔截器實現介面 MethodInterceptor
,並重寫 intercept
方法
import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class DebugMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { // 呼叫方法之前,我們可以新增自己的操作 System.out.println("before method " + method.getName()); // 通過反射呼叫委託類的方法 Object object = methodProxy.invokeSuper(o, args); // 呼叫方法之後,我們同樣可以新增自己的操作 System.out.println("after method " + method.getName()); return object; } }
3)建立代理物件(Proxy):通過 Enhancer.create()
建立委託類物件的代理物件
import net.sf.cglib.proxy.Enhancer; public class CglibProxyFactory { public static Object getProxy(Class<?> clazz) { // 建立動態代理增強類 Enhancer enhancer = new Enhancer(); // 設定類載入器 enhancer.setClassLoader(clazz.getClassLoader()); // 設定委託類(設定父類別) enhancer.setSuperclass(clazz); // 設定方法攔截器 enhancer.setCallback(new DebugMethodInterceptor()); // 建立代理類 return enhancer.create(); } }
從 setSuperclass
我們就能看出,為什麼說 CGLIB 是基於繼承的。
4)實際使用
AliSmsService aliSmsService = (AliSmsService) CglibProxyFactory.getProxy(AliSmsService.class); aliSmsService.send("Java");
執行上述程式碼之後,控制檯列印出:
before method send
send message:Java
after method send
JDK 動態代理和 CGLIB 動態代理對比
1)JDK 動態代理是基於實現了介面的委託類,通過介面實現代理;而 CGLIB 動態代理是基於繼承了委託類的子類,通過子類實現代理。
2)JDK 動態代理只能代理實現了介面的類,且只能增強介面中現有的方法;而 CGLIB 可以代理未實現任何介面的類。
3)就二者的效率來說,大部分情況都是 JDK 動態代理的效率更高,隨著 JDK 版本的升級,這個優勢更加明顯。
常見的還有 Javassist 動態代理機制。和 CGLIB 一樣,作為一個 Java 位元組碼生成框架,Javassist 天生就擁有在執行時動態建立一個類的能力,實現動態代理自然不在話下。 Dubbo 就是預設使用 Javassit 來進行動態代理的。
1)設計模式中有一個設計原則是開閉原則,即對修改關閉,對擴充套件開放,我們在工作中有時會接手很多前人的程式碼,裡面程式碼邏輯讓人摸不著頭腦,就很難去下手修改程式碼,那麼這時我們就可以通過代理對類進行增強。
2)我們在使用 RPC 框架的時候,框架本身並不能提前知道各個業務方要呼叫哪些介面的哪些方法 。那麼這個時候,就可用通過動態代理的方式來建立一箇中間人給使用者端使用,也方便框架進行搭建邏輯,某種程度上也是使用者端程式碼和框架鬆耦合的一種表現。
3)Spring 的 AOP 機制同樣也是採用了動態代理,此處不做詳細討論。
1)靈活性 :動態代理更加靈活,不需要必須實現介面,可以直接代理實現類,並且可以不需要針對每個目標類都建立一個代理類。另外,靜態代理中,介面一旦新增加方法,目標物件和代理物件都要進行修改,這是非常麻煩的
2)JVM 層面 :靜態代理在編譯時就將介面、實現類、代理類這些都變成了一個個實際的 .class
位元組碼檔案。而動態代理是在執行時動態生成類位元組碼,並載入到 JVM 中的。
全部捋一遍下來還是收穫蠻多的,我感覺只要理解了位元組碼在編譯期生成還是在執行期生成,就差不多能夠把握住靜態代理和動態代理了。總結一下靜態代理和動態代理中的角色:
靜態代理:
Subject:公共介面
Real Subject:委託類
Proxy:代理類
JDK 動態代理:
Subject:公共介面
Real Subject:委託類
Proxy:代理類
InvocationHandler:處理類,統一呼叫方法
CGLIB 動態代理:
Subject:公共介面
Real Subject:委託類
Proxy:代理類
MethodInterceptor:方法攔截器,統一呼叫方法
以上就是淺談Java 代理機制的詳細內容,更多關於Java 代理機制的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45