代理模式作爲框架中經常使用的設計模式,它對代碼的拓展性提升可以說是相當高,從aop的實現,到單元測試,再到Mybatis的Mapper接口實現類,都有代理模式的影子。
本篇主要講解代理模式的使用,動態代理的功能和JVM底層如何處理動態代理。
目錄
1.黑中介生成器(newProxyInstance方法)詳解
一、靜態代理的實現
代理是什麼意思呢,舉個例子,我是房東,我有一棟房子急租,但是我很忙,怎麼辦呢?當然是找黑……中介啦。
客戶如果需要租房子,那麼就去找黑中介就行了,這個中介也不一定只會做房產中介,也可以做些別的,反正就是不務正業的那種。
1、實現一個房東
房東的功能相當簡單,只需要租售房子(rentHouse())就行了。
public interface Rent {
void rentSth();
}
public class Landlord implements Rent{
//此處實現Rent接口,之後就可以對Rent接口進行拓展,比如租車,或者py交易也行
@Override
public void rentSth() {
rentHouse();
}
public void rentHouse(){
System.out.println("租售房子");
}
}
2.實現黑中介
黑中介只需要幫助房東租房子即可,但是爲了方便和客戶對接,因此,黑中介也需要繼承Rent接口
public class Intermediary implements Rent{
//因爲中介本身是沒有房子的,因此,需要有一個房東的存在,讓黑中介能幹活
private Rent rent;
//如果對靜態代理理解比較深的話,這個代理的對象可以有很多個,根據業務場景的需要,用代理的不同對象處理不同的事情。
public Intermediary(Rent rent) {
this.rent = rent;
}
@Override
public void rentSth() {
rent.rentSth();
}
public void getMoney(int money){
System.out.println("給房東"+money*0.1+"元");
System.out.println("獲利"+money*0.9+"元");
}
}
3.租房試一試
public class Tenant {
public static void main(String[] args) {
//找黑中介租房子
Intermediary intermediary = new Intermediary(new Landlord());
intermediary.rentSth();
intermediary.getMoney(3000);
}
}
運行結果如下:
4.靜態代理如何玩出花來?
雖然看了靜態代理之後,覺得實現起來好像沒啥用?但是實際上,靜態代理已經可以做很多事情了,比如,可以用靜態代理,讓代理的對象的方法按照你設定的順序進行執行(參考Spring AOP的功能),玩法很多。
但是靜態代理有一個致命的缺點,當然,這也是所有靜態代碼都有的缺點:不夠靈活,代理的對象必須要預先編譯好。
二、動態代理的實現
動態代理的在功能上基本上和靜態代理相差無幾,但是它是基於反射包下的產物,所謂反射,也就是程序在運行的過程中,使JVM加載其他class文件並且操作的一個過程。
基於反射的代理模式就解決了靜態代理不夠靈活的一個缺點,下面我們看下動態代理的使用方法。
1.黑中介生成器(newProxyInstance方法)詳解
這裏依舊拿上面的代碼爲例子,房東和Rent接口都是必須要的,而黑中介我們就不需要了,動態代理會替我們在程序運行的時候,生成一個黑中介。
Proxy.newProxyInstance();//Proxy(代理的意思)這個類提供的靜態方法可以產生一個黑中介
//詳細看一下這個方法需要的參數:
public static Object newProxyInstance(
ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
-
ClassLoader loader:該參數是被代理對象的類加載器,如果是我們寫的普通類,使用applicationClassLoader就可以了,也就是ClassLoader.getSystemClassLoader(),這個加載器也是默認加載器;有興趣的可以瞭解一下Java類加載機制(雙親委派模型)。
-
Class<?>[] interfaces:該參數是被代理對象的class對象,因爲可以代理多個類,因此用數組傳值。
-
InvocationHandler h:雖然反射生成的黑中介可以代理房東的所有行爲,但是程序員得告訴黑中介,應該怎麼做。InvocationHandler是黑中介的行爲處理器。(此處括號裏的內容可以忽略。實際上,動態代理生成的對象,本質上重寫了該對象的所有方法,使其所有方法中都只有一個功能,調用InvocationHandler對象的invoke方法。)
2.InvocationHandler接口詳解
該接口只有一個invoke方法,這個方法我們需要實現它,才能代理我們的對象。
因爲之後我們調用任何黑中介的方法,黑中介都會調用invoke方法。
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
- Object proxy:這個對象是黑中介自己,儘量不要去使用它,因爲它的所有方法都是調用invoke方法,調用它的任何方法都會進入死遞歸調用。
- Method method:因爲不論你調用黑中介的哪個方法,最後都會調用invoke方法,因此,需要依靠Method對象來識別到底調用了什麼方法;這個method對象也就是被調用的方法的實例對象。
- Object[] args:這個是被黑中介調用方法的參數;
- invoke方法的返回值就是黑中介被調用的方法的返回值。
3.使用動態代理
在解釋完黑中介生成器之後,現在可以開始自己動手試一試了。
public class Tenant {
public static void main(String[] args) {
Rent rent = (Rent) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Rent.class}, new InvocationHandler() {
//代理房東
Rent rent = new Landlord();
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
rent.rentSth();
System.out.println("我是黑中介,我被抓了");
return null;
}
});
rent.rentSth();
}
}
運行結果:
4.程序分析和整理
現在我們的黑中介已經如魚得水了,但是還是存在問題:
因爲調用黑中介的任何方法都會調用invoke方法,因此,如果想代理多個方法的話,就很麻煩了,比如現在調用黑中介的toString方法,他依舊會執行invoke方法,給客戶推銷房子。
所以,修改程序:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String menthodName = method.getName();
//根據方法的名字選擇做什麼事情。
if(menthodName.equals("rentSth")){
method.invoke(rent,null);
System.out.println("我是黑中介,我被抓了");
return null;
}
return null;
}
三、動態代理原理分析
從二中我們可以發現,整個動態代理對我們來說幾乎都是透明的,唯一的黑匣子便是newProxyInstance方法。
不過Proxy既然是位於reflet包下,那麼它的實現必定離不開反射技術。
事實上在最後,它會調用sun.misc.ProxyGenerator.generateProxyClass()方法來完成生成字節碼的動作,並且在運行時產生一個描述代理類的字節碼byte[]數組。
我們可以在main()方法中添加下面的代碼,將這個類文件寫入到本地。
byte[] bytes = ProxyGenerator.generateProxyClass("$Proxy0", new Class[]{Rent.class});
try(
FileOutputStream fos =new FileOutputStream(new File("E:\\$Proxy0.class"))
){
fos.write(bytes);
fos.flush();
}catch (Exception e){
e.printStackTrace();
}
再次運行main方法後,在E盤下會多出一個字節碼$Proxy0文件
通過反編譯可以得出下面的代碼(爲了方便觀看,我去掉了trycatch代碼塊):
public final class $Proxy extends Proxy
implements Rent
{
public $Proxy(InvocationHandler invocationhandler)
{
super(invocationhandler);
}
public final boolean equals(Object obj)
{
return ((Boolean)super.h.invoke(this, m1, new Object[] {
obj
})).booleanValue();
}
public final String toString()
{
return (String)super.h.invoke(this, m2, null);
}
public final void rentSth()
{
super.h.invoke(this, m3, null);
}
public final int hashCode()
{
return ((Integer)super.h.invoke(this, m0, null)).intValue();
}
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
static
{
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] {
Class.forName("java.lang.Object")
});
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m3 = Class.forName("proxy.Rent").getMethod("rentSth", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
}
}
不難發現實際上動態生成的對象在調用任何方法的時候,都會交給成員對象InvocationHandler的invoke方法去處理,動態代理也就不那麼神祕了。
jad工具使用方法:
在命令行下輸入:
jad -o -r -s java -d 生成Java文件路徑位置 字節碼文件位置/類名.class
例如:jad -o -r -s java -d src *.class