我們平常買火車票的時候是不是就只有三種方式:火車站售票廳,火車票代售處,網上購票,但是在很多年前的話就是前兩種方式。其實火車票代售處就是火車站售票廳的代理,並且火車票代售處可以提供額外的服務,比如電話預約,並且我們知道火車票代售處是不負責票的退換的,所以這就要說到代理模式的幾種模式了(請看概念)。
概念
代理模式:爲其他對象提供一種代理,以控制對這個對象的訪問。代理對象起到中介作用,可去掉功能服務或增加額外的服務。
常見代理模式 | 定義 |
---|---|
遠程代理 | 爲不同地理的對象提供局域網代表對象 |
虛擬代理 | 根據需要將資源消耗很大的對象進行延遲,當真正需要的時候才進行創建 |
保護代理 | 權限控制 |
智能引用代理 | 根據需要可去掉功能服務或增加額外的服務 |
接下來我們就以智能引用代理來進行實現,智能引用代理模式主要分爲靜態代理和動態代理,接下來我們就分別用這兩種方式實現功能。
靜態代理代碼實現
靜態代理:代理和被代理對象在代理之前是確定的。他們都實現相同接口或者繼承相同的抽象類。
靜態代理有兩種實現方法,一種是繼承的方法,另一種是組合的方法,但是不管怎麼樣首先定義一個接口作爲基類。
Moveable.java
package com.xjh.proxy;
public interface Moveable {
void move();
}
之後定義一個類實現Moveable接口,這個類作爲被代理的類。
Car.java
package com.xjh.proxy;
import java.util.Random;
public class Car implements Moveable {
@Override
public void move() {
// 實現開車
try {
Thread.sleep(new Random().nextInt(1000));
System.out.println("汽車行駛中...");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
之後就是實現代理類,這裏就有兩種方法了:一種是繼承,另一種是組合。
CarTimeProxy1.java(繼承)
package com.xjh.proxy;
public class CarTimeProxy1 extends Car {
//實現了Car2對Car的代理,繼承方法
public void move() {
long starttime = System.currentTimeMillis();
System.out.println("汽車開始行駛...");
super.move();
long endtime = System.currentTimeMillis();
System.out.println("汽車結束行駛... 繼承方法實現汽車行駛時間:" + ( endtime - starttime) + "ms");
}
}
CarTimeProxy2.java(組合)
package com.xjh.proxy;
import java.util.Random;
public class CarTimeProxy2 implements Moveable {
//使用組合的方法實現代理
private Moveable m;
public CarLogProxy(Moveable m) {
super();
this.m = m;
}
@Override
public void move() {
long starttime = System.currentTimeMillis();
System.out.println("汽車開始行駛...");
m.move();
long endtime = System.currentTimeMillis();
System.out.println("汽車結束行駛... 組合實現方法汽車行駛時間:" + ( endtime - starttime) + "ms");
}
}
接下來我們就測試一下。
Client.java
package com.xjh.proxy;
public class Client {
public static void main(String[] args) {
CarTimeProxy1 car1 = new CarTimeProxy1();
car1.move();
Moveable car2 = new CarTimeProxy2(new Car());
car2.move();
}
}
當然我們要對比一下這兩種方法實現靜態代理有什麼區別,那個更加適合代理模式,其實我們可以想象一種情況,假如我們需要添加功能兩種方法都該這麼實現呢?
- 繼承:我們就要無限的新建一個類,繼承於之前的類,這樣的話如果功能增加過多,並且有很多重複組合,代理類會無限的膨脹下去,並且重複的代碼會很多,無法達到代碼複用的效果。(不推薦使用)
- 組合:我們只需要實現Moveable接口,然後不斷地嵌套就可以。
因爲繼承的方式實現很簡單,就是無限的重複CarTimeProxy1的編寫方式,我們就不來編寫相關代碼了。下面我們就來看看組合是怎麼處理這種方式的。
假如我們要加上打印日誌的功能,我們只需要再定義一個類實現Moveable接口。
CarLogProxy.java
package com.xjh.proxy;
import java.util.Random;
public class CarLogProxy implements Moveable {
//使用組合的方法實現代理
private Moveable m;
public CarLogProxy(Moveable m) {
super();
this.m = m;
}
@Override
public void move() {
long starttime = System.currentTimeMillis();
System.out.println("日誌開始...");
m.move();
long endtime = System.currentTimeMillis();
System.out.println("日誌結束...");
}
}
之後我們在main方法中new對象的時候傳入對象。
Client.java
package com.xjh.proxy;
public class Client {
public static void main(String[] args) {
Car car = new Car();
CarTimeProxy2 ctp = new CarTimeProxy2(car);
CarLogProxy clp = new CarLogProxy(ctp);
clp.move();
}
}
這樣我們就實現了功能的拓展,如果我們想要先移動再記錄日誌就沒有必要重新編寫一個類了,直接修改就好了。
Client.java
package com.xjh.proxy;
public class Client {
public static void main(String[] args) {
Car car = new Car();
CarLogProxy clp = new CarLogProxy(car);
CarTimeProxy2 ctp = new CarTimeProxy2(clp);
ctp.move();
}
}
這樣我們就解決了日誌和時間交換順序時不需要再新建類,但是每種功能在不同的類,比如汽車和火車,我們還是要新建代理類,假如我們要加入很多的功能,這樣我們的代理類還是很多,有什麼辦法解決嘛?答案是動態代理。
動態代理實現代碼
動態代理模式類圖:
我們可以通過下面的類圖發現,動態代理的實現就是在代理類和被代理類之間加入了一個InvocationHandler,而InvocationHandler接口需要我們實現invoke方法,他有三個參數,我們來分別理解一下三個參數以及返回對象對應的內容:
名稱 | 內容 |
---|---|
proxy | 生成的代理對象 |
method | 被代理對象的方法 |
args | 方法的參數 |
Object | 方法的返回值 |
接下來我們就來實現這個類,我們只需要在裏面調用method.invoke(target)方法,之後在前後實現這個功能的相關代碼。
TimeHandler.java
package com.xjh.jdkproxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class TimeHandler implements InvocationHandler {
private Object target;
public TimeHandler(Object target) {
super();
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long starttime = System.currentTimeMillis();
System.out.println("汽車開始行駛...");
method.invoke(target);
long endtime = System.currentTimeMillis();
System.out.println("汽車結束行駛... 繼承方法實現汽車行駛時間:" + ( endtime - starttime) + "ms");
return null;
}
}
之後我們編寫main函數,我們主要就是調用Proxy.newProxyInstance(loader, interfaces, h)方法自動生成代理類,裏面參數的含義如下:
名稱 | 含義 |
---|---|
loader | 類加載器 |
interfaces | 實現接口 |
h | InvocationHandler |
所以我們相應的就要new出這些參數,然後傳入Proxy.newProxyInstance方法中,生成一個代理類,然後調用Moveable的move方法完成該功能。
Client.java
package com.xjh.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import com.xjh.jdkproxy.TimeHandler;
public class Client {
public static void main(String[] args) {
Car car = new Car();
InvocationHandler h = new TimeHandler(car);
Class<?> cls = car.getClass();
Moveable m = (Moveable)Proxy.newProxyInstance(cls.getClassLoader(), cls.getInterfaces(), h);
m.move();
}
}
執行結果如下:
這樣我們就不需要編寫多個不同類的代理類,只需要在TimeHandler構造函數中傳入相應的類就可以了,系統的JDK會自動生成對應的Class。
所謂的Dynamic Proxy是這樣一種Class:它是在運行時生成的Class,該Class需要實現一組interface,並且使用動態代理類時,必須要實現InvocationHandler接口。
當然我們需要搞懂動態代理是如何實現的,主要的就是通過Proxy的newProxyInstance方法返回代理對象,但是具體步驟是什麼呢?
- 聲明一段源碼(動態產生代理)
- 編譯源碼(JDK Compiler API),產生新的代理類
- 將這個類加載到內存中,產生一個新的代理對象
- 返回代理對象
接下來我們就根據這個步驟來模仿JDK中動態代理的實現:
首先新建一個Proxy類,裏面新建一個newProxyInstance的靜態方法,然後根據我們之前的步驟來完成這個動態代理。
Proxy.java
package com.xjh.proxy;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
public class Proxy {
public static Object newProxyInstance(Class infce) throws Exception{
//聲明一段源碼(動態產生代理)
String methodStr = "";
for(Method m : infce.getMethods()) {
methodStr += " @Override\r\n" +
" public void " + m.getName() + "() {\r\n" +
" long starttime = System.currentTimeMillis();\r\n" +
" System.out.println(\"汽車開始行駛...\");\r\n" +
" m." + m.getName() + "();\r\n" +
" long endtime = System.currentTimeMillis();\r\n" +
" System.out.println(\"汽車結束行駛... 組合實現方法汽車行駛時間:\" + ( endtime - starttime) + \"ms\");\r\n" +
" }";
}
String str = "package com.xjh.proxy;\r\n" +
"import java.util.Random;\r\n" +
"public class $Proxy0 implements " + infce.getName() + " {\r\n" +
" //使用組合的方法實現代理\r\n" +
" private " + infce.getName() + " m;\r\n" +
" public $Proxy0(" + infce.getName() + " m) {\r\n" +
" super();\r\n" +
" this.m=m;\r\n" +
" }\r\n" +
methodStr + "\r\n" +
"}";
//產生代理類的Java文件
String filename = System.getProperty("user.dir")+"/bin/com/xjh/proxy/$Proxy0.java";
File file = new File(filename);
FileWriter writer;
try {
writer = new FileWriter(filename);
writer.write(str);
writer.flush();
writer.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//編譯源碼(JDK Compiler API),產生新的代理類
//拿到編譯器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//文件管理者
StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
//獲取文件
Iterable units = fileMgr.getJavaFileObjects(filename);
//編譯任務
CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);
t.call();
fileMgr.close();
//將這個類加載到內存中,產生一個新的代理對象
ClassLoader cl = ClassLoader.getSystemClassLoader();//得到類加載器
Class c = cl.loadClass("com.xjh.proxy.$Proxy0");
Constructor ctr = c.getConstructor(infce);
//返回代理對象
return ctr.newInstance(new Car());
}
}
其實在這裏面我們遇到了一個問題:找不到jdk的lib目錄下tools.jar文件,沒法編譯。因爲它編譯文件時,會找到JAVA_HOME的jre\lib\tools.jar,但是tools.jar並不在jre/lib中。所以我們需要在Project Explorer中點擊右鍵,然後依次選擇Properties -> Java Build Path -> Libraries,選中JRE System Library,然後點擊Edit -> Installed JREs,選中你的jre,然後點擊Edit -> Add External JARs,在本地jdk/lib目錄下選擇tools.jar添加,最後Finish -> Apply -> OK即可。
Client.java
package com.xjh.proxy;
public class Client {
public static void main(String[] args) {
try {
Moveable m = (Moveable) Proxy.newProxyInstance(Moveable.class);
m.move();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
之後我們在Window–>Show View–>Other–>Navigator–>Open,打開Navigator視圖我們就能看到我們自動生成的代理類。
Main函數的執行結果:
雖然我們首先了動態加載代理類,但是我們的源代碼都是固定好的,業務邏輯是寫死的,那怎麼才能真正的實現動態呢?
當然就是模仿着JDK中的樣式,同樣定義一個接口,裏面定義invoke方法。
InvocationHandler.java
package com.xjh.proxy;
import java.lang.reflect.Method;
public interface InvocationHandler {
public void invoke(Object o,Method m);
}
然後我們就和在JDK中一樣,new一個類實現這個接口。
TimeHandler.java
package com.xjh.proxy;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class TimeHandler implements InvocationHandler {
Object target;
public TimeHandler(Object target) {
super();
this.target = target;
}
@Override
public void invoke(Object o, Method m) {
try {
long starttime = System.currentTimeMillis();
System.out.println("汽車開始行駛...");
m.invoke(target);
long endtime = System.currentTimeMillis();
System.out.println("汽車結束行駛... 組合實現方法汽車行駛時間:" + ( endtime - starttime) + "ms");
} catch (Exception e) {
e.printStackTrace();
}
}
}
之後我們就要在Proxy.java中進行修改他的傳入參數,以及一些代碼的變動。我們就要根據傳入的InvocationHandler參數來動態的添加方法。
Proxy.java
package com.xjh.proxy;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
public class Proxy {
public static Object newProxyInstance(Class infce, InvocationHandler h) throws Exception{
//聲明一段源碼(動態產生代理)
String methodStr = "";
for(Method m:infce.getMethods()) {
methodStr += " @Override\r\n" +
"public void " + m.getName() + "(){\r\n" +
" try{\r\n" +
" Method md = " + infce.getName() + ".class.getMethod(\"" + m.getName() + "\");\r\n" +
" h.invoke(this, md);\r\n" +
" } catch (Exception e) {\r\n" +
" e.printStackTrace();\r\n" +
" }" +
"}";
}
String str = "package com.xjh.proxy;\r\n" +
"import java.lang.reflect.Method;\r\n" +
"import java.util.Random;\r\n" +
"import com.xjh.proxy.InvocationHandler;\r\n" +
"public class $Proxy0 implements " + infce.getName() + " {\r\n" +
" //使用組合的方法實現代理\r\n" +
" private InvocationHandler h;\r\n" +
" public $Proxy0(InvocationHandler h) {\r\n" +
" this.h = h;\r\n" +
" }\r\n" +
methodStr + "\r\n" +
"}";
//產生代理類的Java文件
String filename = System.getProperty("user.dir")+"/bin/com/xjh/proxy/$Proxy0.java";
File file = new File(filename);
FileWriter writer;
try {
writer = new FileWriter(filename);
writer.write(str);
writer.flush();
writer.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//編譯源碼(JDK Compiler API),產生新的代理類
//拿到編譯器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//文件管理者
StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
//獲取文件
Iterable units = fileMgr.getJavaFileObjects(filename);
//編譯任務
CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);
t.call();
fileMgr.close();
//將這個類加載到內存中,產生一個新的代理對象
ClassLoader cl = ClassLoader.getSystemClassLoader();//得到類加載器
Class c = cl.loadClass("com.xjh.proxy.$Proxy0");
Constructor ctr = c.getConstructor(InvocationHandler.class);
//返回代理對象
return ctr.newInstance(h);
}
}
然後我們在Main函數中new出Handler,傳入對象就可以獲得動態產生的代理類了。
Client.java
package com.xjh.proxy;
public class Client {
public static void main(String[] args) {
try {
Car car = new Car();
InvocationHandler h = new TimeHandler(car);
Moveable m = (Moveable) Proxy.newProxyInstance(Moveable.class, h);
m.move();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
總結
我們平常使用代理模式的主要願意是,我們經常會使用某些類庫的某些方法,但是對於源碼我們是不能進行修改的,我們就可以使用代理模式來在方法前後增加一些業務邏輯,已達到我們想要的效果。這就是AOP(面向切面編程)。