Java設計模式--代理模式

  • 概述

我們平常買火車票的時候是不是就只有三種方式:火車站售票廳,火車票代售處,網上購票,但是在很多年前的話就是前兩種方式。其實火車票代售處就是火車站售票廳的代理,並且火車票代售處可以提供額外的服務,比如電話預約,並且我們知道火車票代售處是不負責票的退換的,所以這就要說到代理模式的幾種模式了(請看概念)。

概念

代理模式:爲其他對象提供一種代理,以控制對這個對象的訪問。代理對象起到中介作用,可去掉功能服務或增加額外的服務。

常見代理模式 定義
遠程代理 爲不同地理的對象提供局域網代表對象
虛擬代理 根據需要將資源消耗很大的對象進行延遲,當真正需要的時候才進行創建
保護代理 權限控制
智能引用代理 根據需要可去掉功能服務或增加額外的服務

接下來我們就以智能引用代理來進行實現,智能引用代理模式主要分爲靜態代理和動態代理,接下來我們就分別用這兩種方式實現功能。

靜態代理代碼實現

靜態代理:代理和被代理對象在代理之前是確定的。他們都實現相同接口或者繼承相同的抽象類。
類圖
靜態代理有兩種實現方法,一種是繼承的方法,另一種是組合的方法,但是不管怎麼樣首先定義一個接口作爲基類。
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方法返回代理對象,但是具體步驟是什麼呢?

  1. 聲明一段源碼(動態產生代理)
  2. 編譯源碼(JDK Compiler API),產生新的代理類
  3. 將這個類加載到內存中,產生一個新的代理對象
  4. 返回代理對象

接下來我們就根據這個步驟來模仿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視圖我們就能看到我們自動生成的代理類。
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(面向切面編程)。
AOP

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章