设计模式之代理模式

一、静态代理

背景:假如有一个接口叫做Movable,里面有一方法叫做move,有一个Tank类实现了此接口并实现了move方法,那么要统计Tank类中move方法的开始执行时间和结束执行时间,该怎么办?

Movable接口:

public interface Movable {

	public void move();
}

Tank类:

public class Tank implements Movable {

	@Override
	public void move() {
		System.out.println("Tank Moving...");
		try {
			Thread.sleep(new Random().nextInt(10000));
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

}

如果是在能修改源码的前提下,那么最简单最直观的做法就是在move方法中前后分别加上起始时间和结束时间,如下:

public class Tank implements Movable {

	@Override
	public void move() {
		
		System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));//开始执行时间
		
		System.out.println("Tank Moving...");
		try {
			Thread.sleep(new Random().nextInt(10000));
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));//结束执行时间
	}

}

但是,如果是不能够修改Tank类中的源代码呢?那么此时就可以采用静态代理的方式来实现时间的统计。

1、采用继承的方式来实现静态代理

public class Tank02 extends Tank {

	@Override
	public void move() {
		System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));//开始执行时间
		
		super.move();
		
		System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));//结束执行时间
	}

}

但是此种方式不灵活,首先采用继承,在Java中则就有了单继承的限制,再者,如果现在不需要做时间统计了,需要对move方法的执行前后做日志,那么又得新建一个Tank03类继承Tank类;如果不仅仅需要统计move方法的前后执行时间,还需要做日志,那么又得新建一个Tank04类继承Tank,加上对时间和日志的处理;如果需要先做日志处理再做时间统计,那么又得新建一个Tank05类继承Tank类,总之,采用继承的方式来实现静态代理,那么类有可能会出现无限制的增加。

2、采用聚合的方式来实现静态代理

public class TankTimeProxy implements Movable {
	
	private Movable m;
	
	public TankTimeProxy(Movable m) {
		this.m = m;
	}

	@Override
	public void move() {
		System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));//开始执行时间
		
		this.m.move();
		
		System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));//结束执行时间

	}

}

此种方式就灵活很多,如果需要对move方法的执行前后做日志,那么可以建立一个TimeLogProxy类,如下:

public class TankLogProxy implements Movable {
	
	private Movable m;
	
	public TankLogProxy(Movable m) {
		this.m = m;
	}

	@Override
	public void move() {
		System.out.println("方法开始,记录日志");
		
		this.m.move();
		
		System.out.println("方法结束,记录日志");
	}

}

如果需要先记录时间再记录日志,那么TankTimeProxy类和TankLogProxy类还可以聚合,如下:

public class Client {

	public static void main(String[] args) {
		Tank t = new Tank();
		TankLogProxy tlp = new TankLogProxy(t);
		TankTimeProxy ttp = new TankTimeProxy(tlp);
		
		Movable m = ttp;
		
		m.move();
	}

}

再者,如果要先记录日志再记录时间,那么可以调换聚合的顺序即可实现,如下:

public class Client {

	public static void main(String[] args) {
		Tank t = new Tank();
		TankTimeProxy ttp = new TankTimeProxy(t);
		TankLogProxy tlp = new TankLogProxy(ttp);
		Movable m = tlp;
		
		m.move();
	}

}

输出如下:

设计模式之代理模式_第1张图片

因为它们都实现了Movable接口,可以随意调换聚合的顺序,所以,用聚合的方式来实现静态代理比采用继承的方式来实现代理要好很多。

二、动态代理

采用继承和聚合的方式都能实现代理,继承的方式不需要被代理对象实现某个接口,而聚合的方式是被代理对象要实现某个 接口;在Spring中当被代理对象不实现接口也可以做成动态代理,就是用的继承的方式,但是这是不推荐的。所以这里的动态代理采用聚合的方式,假设被代理对象都实现了某个接口。

如上面的TankTimeProxy类,这是一个实现了统计时间的静态代理类,假设我们根本看不到此类,只用调用某个类的某个方法就能返回此具体的代理类,也就说让TankTimeProxy类动态生成,采用测试驱动编程的方式,那么我们的调用方式如下:

public class Client {

	public static void main(String[] args) throws Exception {
		Movable m = (Movable)Proxy.newProxyInstance();
		m.move();
	}

}

所以这里的(Movable)Proxy.newProxyInstance()就能返回一个TankTimeProxy对象,那么Proxy类中的静态方法newProxyInstance是如何动态产生TankTimeProxy对象的呢?我们可不可以将原来的TankTImeProxy类的源代码当作一个字符串保存在newProxyInstance方法中,然后将此字符串写入一个文件中,然后在动态编译此文件,最后在生成对象的方式呢?答案是可以的,关键是如何实现动态编译呢?实现动态编译的方式很多,比如JDK6提供的Compiler API,还有使用开源的东西,比如CGLib和ASM,其中CGLib用到了ASM,它们甚至不用源代码来编译,可以直接生成二进制文件,而Spring也用到了CGLib和ASM,当然,如果被代理的对象实现了某个接口的方式用的就是JDK本身的代理机制。

我们这里还是使用JDK6提供的API方式来实现动态编译并生成TankTimeProxy对象,如下:

public class Proxy {

	public static Object newProxyInstance() throws Exception{
		String rn = "\r\n";
		//src字符串的内容其实就是原来的TankTimeProxy类的字符串表示,会将此字符串内容写入文件之中,为了写入后的格式好看,加入了一些额外的格式控制
		String src = 
			"package com.jgao.dp.proxy;" + rn + 
			"" + rn +
			"import java.text.SimpleDateFormat;" + rn + 
			"import java.util.Date;" + rn + 
			"" + rn +
			"public class TankTimeProxy implements Movable {" + rn + 
			"" + rn +
			"    private Movable m;" + rn + 
			"" + rn +
			"    public TankTimeProxy(Movable m) {" + rn + 
			"	    this.m = m;" + rn + 
			"    }" + rn + 
			"" + rn +
			"    @Override" + rn + 
			"    public void move() {" + rn + 
			"	    System.out.println(new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\").format(new Date()));//开始执行时间" + rn + 
			"" + rn +	
			"	    this.m.move();" + rn + 
			"" + rn +	
			"	    System.out.println(new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\").format(new Date()));//结束执行时间" + rn + 
			"" + rn +
			"    }" + rn + 
			"" + rn +
			"}";
		
		//首先将作为TankTimeProxy类的字符串写入一个文件中
		String path = System.getProperty("user.dir") + "/com/jgao/dp/proxy/TankTimeProxy.java";
		File f = new File(path);
		if(!f.getParentFile().exists()) {
			f.getParentFile().mkdirs();
		}
		Writer writer = new FileWriter(f);
		writer.write(src);
		writer.flush();
		writer.close();
		
		//动态编译该文件
		JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
		StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
		Iterable units = fileMgr.getJavaFileObjects(f);
		CompilationTask task = compiler.getTask(null, fileMgr, null, null, null, units);
		task.call();
		fileMgr.close();
		
		//load到内存并生成对象,注意:上面动态编译后的二进制文件并没有在classpath中,所以这里要用到URLClassLoader
		URL[] url = new URL[]{new URL("file:/" + System.getProperty("user.dir") + "/")};
		URLClassLoader loader = new URLClassLoader(url);
		Class<?> clazz = loader.loadClass("com.jgao.dp.proxy.TankTimeProxy");
		Constructor constructor = clazz.getConstructor(Movable.class);
		Object obj = constructor.newInstance(new Tank());
 		
		return obj;
	}

}

如上的代码就能使得TankTimeProxy类动态编译且生成该类对象,且关键是我们根本看不到此类的存在,用户只管调用newProxyInstance方法就能得到这个TankTimeProxy代理类。

但是注意看上面的src字符串,其中的implements后面的接口名称可是Movable,且里面也只是对此接口的move方法做了代理,那么能不能对任意接口的任意方法做同样的处理呢?当然可以,只用在newProxyInstance方法中传入实现的接口名称即可,再通过反射取得接口里面的方法就能实现,如下(这里暂且仅对实现了一个接口的情况做处理):

public class Proxy {

	public static Object newProxyInstance(Class<?> inter) throws Exception{
		String rn = "\r\n";//用于在生成的代码中进换行
		String methodsStr = null;//接口中定义的方法(0个或多个)的完整字符串表示
		
		for(Method m : inter.getMethods()) {
			String returnType = m.getReturnType().getName();//方法返回值
			String methodName = m.getName();//方法名称
			String parameterStr = "";//表示方法参数的字符串
			String actualParameters = "";//要传给被代理对象的实际参数的字符串表示
			Class<?>[] parameterTypes = m.getParameterTypes();
			for(int i = 0; i < parameterTypes.length; i++) {
				String tempActualParameters = "arg" + i;
				parameterStr += parameterTypes[i].getName() + " " + tempActualParameters + ", ";
				actualParameters += tempActualParameters + ", ";
			}
			if(parameterStr != null && !"".equals(parameterStr)) {//截掉最后的", "字符串
				parameterStr = parameterStr.substring(0, parameterStr.length() - 2);
				actualParameters = actualParameters.substring(0, actualParameters.length() - 2);
			}
			String exceptionStr = ""; //方法异常的字符串表示
			Class<?>[] exceptionTypes = m.getExceptionTypes();
			if(exceptionTypes != null && exceptionTypes.length > 0) {
				exceptionStr += " throws ";
				for(int i = 0; i < exceptionTypes.length; i++) {
					exceptionStr += exceptionTypes[i].getName() + ", ";
				}
			}
			if(exceptionStr != null && !"".equals(exceptionStr)) {//截掉最后的", "字符串
				exceptionStr = exceptionStr.substring(0, exceptionStr.length() - 2);
			}
			
			String saveReturnValueStr = "";//用于保存方法返回值的字符串,类似"String result = "这种
			String returnStr = "";//执行方法返回的那个语句,类似"return result;"这种
			if(returnType != null && !"".equals(returnType) && !"void".equals(returnType)) {
				saveReturnValueStr += returnType + " result = ";
				returnStr += "return result;";
			}
			
			methodsStr = 
			"    @Override" + rn + 
			"    public " + returnType + " " + methodName + "(" + parameterStr + ")" + exceptionStr + " {" + rn + 
			"	    System.out.println(new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\").format(new Date()));//开始执行时间" + rn + 
			"" + rn +	
			"	    " + saveReturnValueStr + "this.target." + methodName + "(" + actualParameters + ");" + rn + 
			"" + rn +	
			"	    System.out.println(new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\").format(new Date()));//结束执行时间" + rn + 
			"" + rn +
			"        " + returnStr + rn +
			"    }" + rn + 
			"";
		}
		
		String src = //整个要生成的代理类的字符串表示
			"package com.jgao.dp.proxy;" + rn + 
			"" + rn +
			"import java.text.SimpleDateFormat;" + rn + 
			"import java.util.Date;" + rn + 
			"" + rn +
			"public class TimeProxy implements " + inter.getName() + " {" + rn + //实现的接口由外部指定
			"" + rn +
			"    private "+ inter.getName() +" target;" + rn +   //被代理对象肯定也是实现了该接口的
			"" + rn +
			"    public TimeProxy(" + inter.getName() + " target) {" + rn + 
			"	    this.target = target;" + rn + 
			"    }" + rn + 
			"" + rn +
			
			methodsStr +
			
			"}";
		
		//首先将作为TankTimeProxy类的字符串写入一个文件中
		String path = System.getProperty("user.dir") + "/com/jgao/dp/proxy/TimeProxy.java";
		File f = new File(path);
		if(!f.getParentFile().exists()) {
			f.getParentFile().mkdirs();
		}
		Writer writer = new FileWriter(f);
		writer.write(src);
		writer.flush();
		writer.close();
		
		//动态编译该文件
		JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
		StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
		Iterable<? extends JavaFileObject> units = fileMgr.getJavaFileObjects(f);
		CompilationTask task = compiler.getTask(null, fileMgr, null, null, null, units);
		task.call();
		fileMgr.close();
		
		//load到内存并生成对象,注意:上面动态编译后的二进制文件并没有在classpath中,所以这里要用到URLClassLoader
		URL[] url = new URL[]{new URL("file:/" + System.getProperty("user.dir") + "/")};
		URLClassLoader loader = new URLClassLoader(url);
		Class<?> clazz = loader.loadClass("com.jgao.dp.proxy.TimeProxy");
		Constructor<?> constructor = clazz.getConstructor(inter);
		Object obj = constructor.newInstance(new Tank());
		return obj;
	}

}
注意代码倒数第五行Object obj = constructor.newInstance(new Tank());,这里传入的对象就得实现传入到newProxyInstance方法中那个接口,不然会报错。先不管生成的对象,看看生成的代码如何,当在客户端调用如下:

import java.util.Comparator;

public class Client {

	public static void main(String[] args) throws Exception {
		Proxy.newProxyInstance(Comparator.class);
	}

}
最终生成的代码截图如下:

设计模式之代理模式_第2张图片

所以,只要被代理对象实现了某个接口,就能为此对象做代理,且我们根本不用知道代理的名字,不用知道代理的存在,不用知道代理是如何构建的,只用调用newProxyinstance方法即可。但是,但是,注意观察,这里只能做到了对实现任意接口的对象做代理,但是这里做到的是实现统计方法开始与结束的执行时间,如果需要做日志处理,还是要新建一个Proxy类重新写上做日志处理的代码。

所以,具体的代理逻辑不能写死在代码中,应该由外部指定,这样才能真正做到为实现了任意接口的对象做任意的代理,同样,被代理对象也不应该写死在代码中,所以这里新建一个叫做InvocationHandler的接口,用于同一处理代理的逻辑,如下:(省略了对返回值的处理)

import java.lang.reflect.Method;

public interface InvocationHandler {

	public void invoke(Method m);
}
其中的参数Method m就是被代理对象真正执行的方法,是通过代理传入进来的。为了方便,都假设被代理对象真正执行的方法没有参数,所以在这里只有一个Mthod m的参数指定被代理对象执行的方法,省略了参数。此后,给被代理对象做代理,加上的额外逻辑就应该实现该接口,并加上要代理的逻辑,然后在通过反射,执行被代理对象真正要执行的方法,且被代理对象就应该实现类中保留一个被代理对象引用。比如要给被代理对象加上时间统计的逻辑,那么建立一个类叫做TimeHandler,并实现这里的InvocationHandler接口,如下:

import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TimeHandler implements InvocationHandler {

	private Object target;//被代理对象

	public TimeHandler(Object target) {
		this.target = target;
	}
	
	@Override
	public void invoke(Method m) {
		System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));//要代理的逻辑,开始执行时间
		
		try {
			//执行被代理对象真正要执行的方法,该方法名字是通过代理传进来的,因为被代理对象和代理都实现了同样的接口,所以肯定是能执行指定的方法的
			m.invoke(this.target);
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));//要代理的逻辑,结束执行时间
	}

}
现在剩下的就是如何将该类与代理关联起来,所以应该将代理逻辑处理类传入动态生成的代理对象中,所以剩下的就是继续拼装动态生成的代理类的代码,如下:(由于InvocationHandler接口中省略了返回值的处理,所以这里假设被代理对象实现的接口中的方法都没有返回值)

public class Proxy {

	//传入代理逻辑InvocationHandler handler
	public static Object newProxyInstance(Class<?> inter, InvocationHandler handler) throws Exception{
		String rn = "\r\n";//用于在生成的代码中进换行
		String methodsStr = null;//接口中定义的方法(0个或多个)的完整字符串表示
		
		for(Method m : inter.getMethods()) {
			String returnType = m.getReturnType().getName();//方法返回值
			String methodName = m.getName();//方法名称
			String parameterStr = "";//表示方法参数的字符串
			String actualParameters = "";//要传给被代理对象的实际参数的字符串表示
			Class<?>[] parameterTypes = m.getParameterTypes();
			for(int i = 0; i < parameterTypes.length; i++) {
				String tempActualParameters = "arg" + i;
				parameterStr += parameterTypes[i].getName() + " " + tempActualParameters + ", ";
				actualParameters += tempActualParameters + ", ";
			}
			if(parameterStr != null && !"".equals(parameterStr)) {//截掉最后的", "字符串
				parameterStr = parameterStr.substring(0, parameterStr.length() - 2);
				actualParameters = actualParameters.substring(0, actualParameters.length() - 2);
			}
			String exceptionStr = ""; //方法异常的字符串表示
			Class<?>[] exceptionTypes = m.getExceptionTypes();
			if(exceptionTypes != null && exceptionTypes.length > 0) {
				exceptionStr += " throws ";
				for(int i = 0; i < exceptionTypes.length; i++) {
					exceptionStr += exceptionTypes[i].getName() + ", ";
				}
			}
			if(exceptionStr != null && !"".equals(exceptionStr)) {//截掉最后的", "字符串
				exceptionStr = exceptionStr.substring(0, exceptionStr.length() - 2);
			}
			
			methodsStr = 
			"    @Override" + rn + 
			"    public " + returnType + " " + methodName + "(" + parameterStr + ")" + exceptionStr + " {" + rn + 
			"        try {" + rn +
			"            Method md = " + inter.getName() + ".class.getMethod(\"" + methodName + "\");" + rn +
			"            this.handler.invoke(md);" + rn + //取得被代理对象真正要执行的方法,并传递给InvocationHandler处理,不像之前把代理逻辑写死在这里
			"        }catch(Exception e) {" + rn + 
			"            e.printStackTrace();" + rn +
			"        }" + rn + 
			"    }" + rn + 
			"";
		}
		
		String src = //整个要生成的代理类的字符串表示
			"package com.jgao.dp.proxy;" + rn + 
			"" + rn +
			"import java.lang.reflect.Method;" + rn +
			"" + rn +
			"public class $Proxy1 implements " + inter.getName() + " {" + rn + //实现的接口由外部指定
			"" + rn +
			"    private com.jgao.dp.proxy.InvocationHandler handler;" + rn +   //不再保存被代理对象,而是保留代理逻辑对象
			"" + rn +
			"    public $Proxy1(com.jgao.dp.proxy.InvocationHandler handler) {" + rn + 
			"	    this.handler = handler;" + rn + 
			"    }" + rn + 
			"" + rn +
			
			methodsStr +
			
			"}";
		
		//首先将作为TankTimeProxy类的字符串写入一个文件中
		String path = System.getProperty("user.dir") + "/com/jgao/dp/proxy/$Proxy1.java";
		File f = new File(path);
		if(!f.getParentFile().exists()) {
			f.getParentFile().mkdirs();
		}
		Writer writer = new FileWriter(f);
		writer.write(src);
		writer.flush();
		writer.close();
		
		//动态编译该文件
		JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
		StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
		Iterable<? extends JavaFileObject> units = fileMgr.getJavaFileObjects(f);
		CompilationTask task = compiler.getTask(null, fileMgr, null, null, null, units);
		task.call();
		fileMgr.close();
		
		//load到内存并生成对象,注意:上面动态编译后的二进制文件并没有在classpath中,所以这里要用到URLClassLoader
		URL[] url = new URL[]{new URL("file:/" + System.getProperty("user.dir") + "/")};
		URLClassLoader loader = new URLClassLoader(url);
		Class<?> clazz = loader.loadClass("com.jgao.dp.proxy.$Proxy1");
		Constructor<?> constructor = clazz.getConstructor(com.jgao.dp.proxy.InvocationHandler.class);
		Object obj = constructor.newInstance(handler);//这里不再把被代理对象写死在这里
		
		return obj;
	}

}
客户端调用如下:

public class Client {

	public static void main(String[] args) throws Exception {
		Movable m = new Tank();
		InvocationHandler handler = new TimeHandler(m);
		Movable movable = (Movable)Proxy.newProxyInstance(Movable.class, handler);
		movable.move();
	}

}
让我们来看看最后生成的代理对象的代码截图:

设计模式之代理模式_第3张图片
如图所示,这次就把被代理逻辑、被代理对象和代理类彻底的分开了,生成的代理类的逻辑再也不用再变了,真正坐到了对实现了任意接口的对象的任意方法做任意的代理了,只要调用$Proxy1类的静态方法newProxyInstance,并传入被代理对象实现的接口和代理处理的逻辑对象,就能得到代理类,至于代理类是如何生成的我们不须再关心,不必再像之前的静态代理,需要自己写很多的代理类,在这里我们根本不必关心代理类的存在,最后输出如下:

设计模式之代理模式_第4张图片

三、总结

如上介绍了静态代理和动态代理,其中动态代理不容易理解,在于代理类的字符串拼装上,上面模拟的还不够全面,但是思路也就那样,大同小异。



























你可能感兴趣的:(设计模式,代理,代理模式)