黑马程序员:我对“动态代理”的初步理解

---------------------- android培训java培训、期待与您交流! ----------------------

  1. 从一个小例子开始

定义一个类 Car 实现接口 Movable 并重写接口的方法move(),我想计算move()方法的运行时间,可以这样写:

public class Car implements Movable {

	@Override
	public void move() {
		long start = System.currentTimeMillis();
		System.out.println("车在飞奔……");
		long end = System.currentTimeMillis();
		System.out.println("运行时间:" + (end - start));
	}

}


问题:假如类 Car 是jar包中已经编译好的class文件(看不到Car的源代码),这时怎样计算move()方法的运行时间?

可以想到的方法有2个:

一:定义一个类(假设叫:Car2)继承  Car类并重写move()方法,super.move()前后分别记录时间

二:定义一个类(假设叫:Car3)实现Movable接口并持有Car类的引用car,重写move()时在car.move()前后分别记录时间(聚合

下面分别是两种方案的代码:

public class Car2 extends Car {
	@Override
	public void move() {
		long start = System.currentTimeMillis();
		super.move();
		long end = System.currentTimeMillis();
		System.out.println("运行时间:" + (end - start));
	}
}


 

public class Car3 implements Movable {
	Car car;

	public Car3(Car car) {
		this.car = car;
	}

	@Override
	public void move() {
		long start = System.currentTimeMillis();
		car.move();
		long end = System.currentTimeMillis();
		System.out.println("运行时间:" + (end - start));
	}

}

 

可以发现:在调用Car2或Car3对象的move()方法时都会调用Car的move()方法,因此可以把 Car2、Car3 叫作Tank的代理(时间代理)。

问题:哪种种代理方式有优势?

假设用户有这样繁琐的需求:在Car的move()方法前后记录日志(日志代理) 或 在move()方法开始前判断某用户是否有执行此方法的权限(权限代理) 或 在move()方法开始前开启一个事务并在方法结束时提交(事务代理)。

若用继承的方式实现这些代理(包括各种代理的组合)将会使人厌烦,例如:对move()方法先实现日志代理,再实现时间代理,必须定义一个类继承Car并在重写move()方法时首先加上记录日志的语句;对move()方法先实现时间代理,再实现日志代理,又要定义一个类继承Car并在重写move()时先写记录时间的语句,方法结束再加上记录日志的语句;对move()方法实现时间、日志、权限代理的组合,必须重新定义一个类继承之前的某个类并在重写move()时重新排列各种代理语句!显然这种方式产生了大量相互继承的类,极其繁琐且扩展性低!

 

下面看用“聚合”实现各种代理的情形(例如先记录日志再记录时间):

下面是时间代理类:

public class CarTimeProxy implements Movable {
	private Movable m;

	public CarTimeProxy(Movable m) {
		this.m = m;
	}

	@Override
	public void move() {
		long start = System.currentTimeMillis();
		System.out.println("开始时间:" + start);
		m.move();
		long end = System.currentTimeMillis();
		System.out.println("结束时间:" + end);
	}
}

下面是日志代理类:

public class CarLogProxy implements Movable {
	private Movable m;

	public CarLogProxy(Movable m) {
		this.m = m;
	}

	@Override
	public void move() {
		System.out.println("方法运行……");
		m.move();
		System.out.println("方法结束!");
	}

}

所以在测试类中可以这样写:

public class Test {
	public static void main(String[] args) {
		Car car = new Car();
		CarTimeProxy ctp = new CarTimeProxy(car);
		CarLogProxy clp = new CarLogProxy(ctp);
		Movable m = clp;
		m.move();
	}
}

输出结果:

方法运行……
开始时间:1336526482500
车在飞奔……
结束时间:1336526482500
方法结束!

显然,如果想先记录时间再记录日志,只需要在创建CarTimeProxy类对象时将CarLogProxy对象的实例作为构造函数的参数传入即可,而且如果要实现其它代理方式的组合,也可以通过这种方式一层层包裹。之所以能实现这种代理方式的组合,是因为在每种代理类中都持有Movable类型的对象(也可以说每个代理类中都可以聚合Movable类型的对象),所以每个代理类都可以持有 被代理类 或 其它代理类 的引用。如果将这些不同的组合方式写在配置文件中扩展性就更高了!这只是静态代理

这种方式显然结构清晰、易于维护(要添加新的代理方式时只要定义新的代理类:CarXXXProxy实现Movable接口)。

类关系图如下:

 

问题下面来单独研究时间代理。用户的新需求:有一个代理类(假如叫TimeProxy)可以对任何类(可以是Car、Train、Ship……)的任意方法进行时间代理。显然代理类中聚合的其它代理类的类型就不能是Movable

思路:只要一个类实现了某个接口,就可以生成这个类的代理。假如有多个类实现了相同的接口,则可以实现对某个类的各种代理的组合。

所以这里假设要实现时间代理的类都实现了某个接口!(spring中就是用这种方式实现代理。虽然spring可以实现继承代理,但spring不推荐这样用)

既然不确定对什么类(Car、 Train、 Ship)的对象进行代理,那么就要求有一个类(JDK中是Proxy)能在程序运行期间动态生成代理类的对象(可以是CarTimeProxy、TrainTimeProxy、CarLogProxy)——这样就引出了动态代理(Proxy可以看作CarTimeProxy的代理,叫作代理的总代理)。

下面一步步实现:

首先尝试动态生成代理类ActiveCarTimeProxy:(引入的包没必要贴起来了吧)

public class Test {
	public static void main(String[] args) throws Exception {
		String enter = "\r\n";	//回车换行符
		//要实现动态编译的类用字符串包裹起来
		String compileStr = 
				"package com.suius.designPattern.proxy;" + enter +
				"public class ActiveCarTimeProxy implements Movable {" + enter +
				"	private Movable m;" + enter +
				"	public ActiveCarTimeProxy(Movable m) {" + enter +
				"		this.m = m;" + enter +
				"	}" + enter +
				"	@Override" + enter +
				"	public void move() {" + enter +
				"		long start = System.currentTimeMillis();" + enter +
				"		System.out.println(\"开始时间:\" + start);" + enter +
				"		m.move();" + enter +
				"		long end = System.currentTimeMillis();" + enter +
				"		System.out.println(\"结束时间:\" + end);" + enter +			 	
				"	}" + enter +
				"}";
		//将这个长字符串写入硬盘上的文件ActiveCarTimeProxy.java
		String fileName = System.getProperty("user.dir") + 
				"/src/com/suius/designPattern/proxy/ActiveCarTimeProxy.java";
		File file = new File(fileName);
		FileWriter fw = new FileWriter(file);
		fw.write(compileStr);
		fw.flush();
		fw.close();
		
		//下面编译已经写入的java文件:ActiveCarTimeProxy.java
		JavaCompiler jc = ToolProvider.getSystemJavaCompiler();//得到java编译器
		//得到一个文件管理器
		StandardJavaFileManager sjfm = jc.getStandardFileManager(null, null, null);
		//根据文件目录fileName找到要编译的java文件
		Iterable ite = sjfm.getJavaFileObjects(fileName);
		//根据文件管理器和要编译的文件创建一个编译任务
		CompilationTask ct = jc.getTask(null, sjfm, null, null, null, ite);
		ct.call();
		sjfm.close();
		
		//将编译好的:ActiveCarTimeProxy.class类load进内存 
		URL[] urls = new URL[] {new URL("file:/" + System.getProperty("user.dir") + "/src")};
		//注意:URLClassLoader可以装载任意目录下的类文件
		URLClassLoader ucl = new URLClassLoader(urls);
		Class c = ucl.loadClass("com.suius.designPattern.proxy.ActiveCarTimeProxy");
		//反射机制:得到类ActiveCarTimeProxy中的参数类型为Movable的构造函数
		Constructor cr = c.getConstructor(Movable.class);
		//利用构造函数创建一个对象
		Movable m = (Movable)cr.newInstance(new Car());
		m.move();
	}

}

实现过程:

将这个字符串写入文件,再由java编译器编译成.class文件

代理类编译成class文件load进内存并创建代理类的对象

难点:

将字符串包裹的类动态编译的实现方式:JDK中的API(针对用接口方式实现的代理)、CGLIB、ASM。

Java编译器JavaCompiler的使用

类装载器URLClassLoader的使用

根据反射拿到类的构造方法

问题:现在只是动态生成了实现了Movable接口的类的代理,接下来生成实现任意接口的类的动态代理,并达到这样的效果:

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


在测试类中调用总代理 Proxy 的newProxyInstance(被代理类实现的接口类型)方法,就能得到一个对 实现了某个接口类型的类 的代理类对,在这里甚至不用知道这个代理类是什么名字就能使用它了!

下面我要进一步提高扩展性:对newProxyInstance(……)方法的参数(某个接口)中定义的每个方法都实现代理:

通过反射得到一个类中的所有方法的数组Method[],然后动态生成每个方法的代码:

public class Proxy {
	public static Object newProxyInstance(Class ife) throws Exception {
		String enter = "\r\n";
		String methodStr = "";
		
		Method[] methods = ife.getMethods();
		for(Method m : methods){
			methodStr += "@Override" + enter +
					"public void " + m.getName() + "() {" + enter +
					"		long start = System.currentTimeMillis();" + enter +
					"		System.out.println(\"开始时间:\" + start);" + enter +
					"		m." + m.getName() + "();" + enter +
					"		long end = System.currentTimeMillis();" + enter +
					"		System.out.println(\"结束时间:\" + end);" + enter +	
					"	}";
		}
		
		
		String compileStr = 
				"package com.suius.designPattern.proxy;" + enter +
				"public class ActiveCarTimeProxy implements " + ife.getName() + "{" + enter +
				"	private " + ife.getName() + " m;" + enter +
				"	public ActiveCarTimeProxy(" + ife.getName() + " m) {" + enter +
				"		this.m = m;" + enter +
				"	}" + enter +
				methodStr + enter +
				"}";
		
		String fileName = System.getProperty("user.dir") + 
				"/src/com/suius/designPattern/proxy/ActiveCarTimeProxy.java";
		File file = new File(fileName);
		FileWriter fw = new FileWriter(file);
		fw.write(compileStr);
		fw.flush();
		fw.close();
		
		JavaCompiler jc = ToolProvider.getSystemJavaCompiler();
		StandardJavaFileManager sjfm = jc.getStandardFileManager(null, null, null);
		Iterable ite = sjfm.getJavaFileObjects(fileName);
		CompilationTask ct = jc.getTask(null, sjfm, null, null, null, ite);
		ct.call();
		sjfm.close();
		
		URL[] urls = new URL[] {new URL("file:/" + System.getProperty("user.dir") + "/src")};
		URLClassLoader ucl = new URLClassLoader(urls);
		Class c = ucl.loadClass("com.suius.designPattern.proxy.ActiveCarTimeProxy");
		Constructor cr = c.getConstructor(Movable.class);
		Object o = (Object)cr.newInstance(new Car());
		
		return o;
	}
}


现在可以做到:传入任意的接口都可以生成实现该接口的类的对象

问题:现在只能生成TimeProxy(时间代理)(代理逻辑必须写死,不能动态更改),若用户要记录日志,还得再写newProxyInstance2()……显然扩展性不好!所以下一步要做到让记录时间或日志的代码也能由用户指定:即在newProxyInstance()方法中不仅要生成动态代码,还能实现由用户来指定是记录日志还是判定权限,也就是说动态生成的方法要调用其它具体实现了记录日志或时间的类(这个类的实现由用户决定,凡是可以由用户指定的类都要用到多态

即:这里的代理实现的具体逻辑不能在动态生成的代码中写死

所以现在需要一个能动态指定处理某个方法的类:对方法进行自定义处理

实现方案:在方法newProxyInstance(Class ife)的参数加入另一个参数:InvocationHandler ih,说明产生代理的类型:即要对接口ife 中的方法进行什么处

理,而 ife说明h可以对哪个接口中的方法进行动态代理。这样就能动态指定对方法进行怎样的代理!

具体实现代码如下:

代理实现类,可以在外部指定对哪个对象进行代理(target)

public class TimeHandler implements InvocationHandler {

	private Object target; // 对哪个对象进行代理

	public TimeHandler(Object target) {
		this.target = target;
	}

	@Override
	public void invoke(Object o, Method m) {
		long start = System.currentTimeMillis();
		System.out.println("开始时间:" + start);
		try {
			m.invoke(target); // 调用被代理类的方法
		} catch (Exception e) {
			e.printStackTrace();
		}
		long end = System.currentTimeMillis();
		System.out.println("结束时间:" + end);
	}

}


总代理Proxy类:

public class Proxy {
	public static Object newProxyInstance(Class ife, InvocationHandler ih) throws Exception {
		String enter = "\r\n";
		String methodStr = "";
		
		Method[] methods = ife.getMethods();
				
		for(Method m : methods){
			methodStr += 
					"	@Override" + enter +
					"	public void " + m.getName() + "() {" + enter +
					"		try {" + enter +
					"			Method md = " + ife.getName() + ".class.getMethod(\"" + m.getName() + "\");" + enter +	
					"			ih.invoke(this, md);" + enter +
					"		} catch(Exception e) {e.printStackTrace();}" + enter +
					"	}";
		}
		
		String compileStr = 
				"package com.suius.designPattern.proxy;" + enter +
				"import java.lang.reflect.Method;" + enter +
				"public class ActiveCarTimeProxy implements " + ife.getName() + "{" + enter +
				"	private com.suius.designPattern.proxy.InvocationHandler ih;" + enter +
				"	public ActiveCarTimeProxy(InvocationHandler ih) {" + enter +
				"		this.ih = ih;" + enter +
				"	}" + enter +
				methodStr + enter +
				"}";
		
		String fileName = System.getProperty("user.dir") + 
				"/src/com/suius/designPattern/proxy/ActiveCarTimeProxy.java";
		File file = new File(fileName);
		FileWriter fw = new FileWriter(file);
		fw.write(compileStr);
		fw.flush();
		fw.close();
		
		JavaCompiler jc = ToolProvider.getSystemJavaCompiler();
		StandardJavaFileManager sjfm = jc.getStandardFileManager(null, null, null);
		Iterable ite = sjfm.getJavaFileObjects(fileName);
		CompilationTask ct = jc.getTask(null, sjfm, null, null, null, ite);
		ct.call();
		sjfm.close();
		
		URL[] urls = new URL[] {new URL("file:/" + System.getProperty("user.dir") + "/src")};
		URLClassLoader ucl = new URLClassLoader(urls);
		Class c = ucl.loadClass("com.suius.designPattern.proxy.ActiveCarTimeProxy");
		Constructor cr = c.getConstructor(InvocationHandler.class);
		Object o = (Object)cr.newInstance(ih);
		
		return o;
	}
}

生成的动态代理类如下:

package com.suius.designPattern.proxy;
import java.lang.reflect.Method;
public class ActiveCarTimeProxy implements com.suius.designPattern.proxy.Movable{
	private com.suius.designPattern.proxy.InvocationHandler ih;
	public ActiveCarTimeProxy(InvocationHandler ih) {
		this.ih = ih;
	}
	@Override
	public void move() {
		try {
			Method md = com.suius.designPattern.proxy.Movable.class.getMethod("move");
			ih.invoke(this, md);
		} catch(Exception e) {e.printStackTrace();}
	}
}

这个动态代理类的名字ActiveCarTimeProxy可以任意指定,在JAVA中的名字是$Proxy1,它的名字无关紧要,我们只是用它调用代理类的方法。

 

 测试类中要这样调用:

public class Test {
	public static void main(String[] args) throws Exception {
		Car car = new Car();
		InvocationHandler ih = new TimeHandler(car);
		Movable m = (Movable) Proxy.newProxyInstance(Movable.class, ih);
		m.move();
	}
}

程序执行的具体的流程:创建一个Car类的对象,将这个对象作为参数传入TimeHandler的构造函数,创建一个TimeHandler对象。根据要实现代理的类的类型(这里是Movable)和TimeHandler对象创建一个对实现了Movable接口的类Car的对象car中所有方法的动态时间代理对象返回,接着将它转换成Movable类型(这里是ActiveCarTimeProxy类型的对象 假设对象名是:o),最后调用o的move()方法。而move()方法中又调用TimeHandler(类型是InvocationHandler)的invoke()方法:ih.invoke(this, md); 追根溯源,最终的处理逻辑是在类TimeHandler的invoke(Object o, Method m)方法中,而这个invoke()方法最终调用了类Car中重写的Movable接口中的move()方法,并在前后加了处理逻辑!

 

这个过程看起来复杂,但动态代理针对的问题是要实现代理的类非常多的情况,能够方便快捷地实现扩展:对任意类、任意接口/方法,实现任意的代理

 

spring的AOP/面向切面编程就是这种原理:方法运行期间可以在方法前后加入自己的逻辑(对方法是透明的),而且这个逻辑可插拔(例如将TransactionHandler写在配置文件中,就可以随意更改要实现的代理:时间、日志、权限……而且可叠加)

 

下面是类的关系图:


总结:动态代理就是在不知道对什么类(类要实现某个接口)进行代理的情况下,在程序运行期间根据用户指定的接口(这里是Movable)和代理方式(这里是时间代理)生成对实现了这个接口的类的代理。

 

 

 

 

 


 

 

 


 

 

 

 

 

 

 

 

 

 

 

---------------------- android培训java培训、期待与您交流! ----------------------

 

 

详细请查看:http://edu.csdn.net/heima

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

营赢盈英

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值