代理模式(八)

JDK动态代理

       在JDK1.3以后提供了动态代理的技术,允许开发者在运行期创建接口的代理实例。在Sun刚推出动态代理时,还很难想象它有多大的实际用途,现在动态代理是实现AOP的绝好底层技术。

       JDK的动态代理主要涉及Java.lang.reflect包中的两个类:Proxy和InvocationHandler。其中InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编织在一起。而Proxy为InvocationHandler实现类动态创建一个复合某一接口的代理的实例。

JDK动态代理的使用

       要想模拟JDK动态代理的实现,首先要明白它的内在机理,知道它对外开放的接口方法。借用之前文章中JDK动态代理时的简单示例,仅包括两个类:TimeHandler类和Test类,来回顾下JDK动态代理的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.jdkproxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
* JDK动态代理步骤:
* 1创建一个实现InvocationHandler的类,必须实现invoke方法
* 2.创建被代理的类和接口
* 3.调用Proxy的静态方法,创建一个代理类
* 4.通过代理调用方法.
*
*/
public class TimeHandler implements InvocationHandler {

private Object object;

public TimeHandler(Object object) {
super();
//代理对象
this.object = object;
}

/**
* proxy: 代理对象
* method: 被代理对象的方法
* args:方法的参数
* return: 调用方法的返回值
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

long startTime = System.currentTimeMillis();
System.out.println("汽车开始行驶");

method.invoke(object);

long endTime = System.currentTimeMillis();
System.out.println("汽车停止行驶,行驶时间为:"+(endTime-startTime)+"毫秒");
return null;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.jdkproxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

import com.proxy.Car;
import com.proxy.Moveable;

public class Test {

public static void main(String[]args){
Car car = new Car();
InvocationHandler h = new TimeHandler(car);
Class<?> cls = car.getClass();
/**
* loader:类加载器
* interfaces:实现的接口
* h InvocationHandler
*/
Moveable m = (Moveable) Proxy.newProxyInstance(
cls.getClassLoader(), cls.getInterfaces(), h);
m.move();
}
}

       运行结果:

1
2
3
汽车开始行驶
汽车行驶中
汽车停止行驶,行驶时间为:56毫秒

JDK动态代理实现思路

       如上Test类中,通过Proxy的newProxyInstance()方法动态产生一个代理。模拟JDK动态代理实现流程,同样要自定义一个Proxy类并在类中实现产生动态代理的方法,这也是实现JDK动态代理的关键点和主要功能。

       动态代理实现步骤如下:

  1. 声明一段源码(动态产生代理)
  2. 编译源码(JDK Compiler API),产生代理类
  3. 将新产生的代理类load到内存当中,产生一个新的对象即代理对象
  4. 在newProxyInstance方法中返回这个代理对象

声明一段源码(动态产生代理)

       将源码定义为字符串,Proxy类中代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.wy.proxy.myproxy;

public class Proxy {

public static Object newProxyInstance(){
String rt = "\r\t";
String str =
"package com.myJDKproxy;" + rt +
"public class $Proxy0 implements Moveable{"+ rt +
"private Moveable m;"+ rt +
" public $Proxy0(Moveable m) {"+ rt +
" super();"+ rt +
" this.m = m;"+ rt +
" }"+ rt +
" @Override"+ rt +
" public void move() {"+ rt +
" long startTime = System.currentTimeMillis();"+ rt +
" System.out.println(\"汽车开始行驶\");"+ rt +
" m.move();"+ rt +
" long endTime = System.currentTimeMillis();"+ rt +
" System.out.println(\"汽车行驶结束,行驶时间为:\"+(endTime-startTime)+\" 毫秒\");"+ rt +
" }"+ rt +
"}";
return null;
}
}

       由于JDK动态代理产生的类名为$Proxy0,所以在此模仿此类名。在接下来的工作中要对源码进行编译,所以要将源码写入一个java文件。

       在Proxy类中增加以下代码:

1
2
3
String filename = System.getProperty("user.dir") +"/bin/com/wy/proxy/myproxy/$Proxy0.java";
File file = new File(filename);
FileUtils.writeStringToFile(file, str);

       其中FileUtils类需要导入包commons-io.jar,运行以上代码后可在Navigator视窗看到对应包下生成了$Proxy.java文件。

       由于动态代理要实现对任意接口的任意方法进行代理,所以要对以上的源码字符串进行更改,更改思路即将接口当作参数传入newProxyInstance方法,更改之后的Proxy类如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.wy.proxy.myproxy;

import java.io.File;
import java.lang.reflect.Method;

import org.apache.commons.io.FileUtils;

public class Proxy {

public static Object newProxyInstance(Class infce) throws Exception{
String rt = "\r\n";
String methodStr = "";
for(Method m : infce.getMethods()){
methodStr += " @Override" + rt +
" public void " + m.getName() + "() {" + rt +
" long starttime = System.currentTimeMillis();" + rt +
" System.out.println(\"汽车开始行驶\");" + rt +
" m." + m.getName() + "();" + rt +
" long endtime = System.currentTimeMillis();" + rt +
" System.out.println(\"汽车行驶结束,行驶时间为:\" " + rt +
" + (endtime - starttime) + \"毫秒\");" + rt +
" }" ;
}

String str =
"package com.wy.proxy.myproxy;" + rt +
"public class $Proxy0 implements " + infce.getName() + " {" + rt +
"private " +infec.getName()+ " m;"+ rt +
" public $Proxy0(" + infce.getName() + " m) {" + rt +
" super();" + rt +
" this.m = m;" + rt +
" }" + rt +
" private " + infce.getName() + " m;" + rt +
methodStr + rt +
"}" ;
//产生代理类的java文件
String filename = System.getProperty("user.dir") +"/bin/com/wy/proxy/myproxy/$Proxy0.java";
//System.out.println(filename);
File file = new File(filename);
FileUtils.writeStringToFile(file, str);

return null;

}
}

       Test测试类中将接口参数传入:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.wy.proxy.myproxy;

public class Client {

/**
* 测试类
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Proxy.newProxyInstance(Moveable.class);
}

}

       运行后检查生成的$Proxy.java文件是否正确。

编译源码(JDK Compiler API),产生代理类

       编译即相当于我们在黑窗口时用的javac命令,在程序中编译的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
//拿到编译器
JavaCompiler complier = ToolProvider.getSystemJavaCompiler();
//文件管理者
StandardJavaFileManager fileMgr =
complier.getStandardFileManager(null, null, null);
//获取文件
Iterable units = fileMgr.getJavaFileObjects(filename);
//编译任务
CompilationTask t = complier.getTask(null, fileMgr, null, null, null, units);
//进行编译
t.call();
fileMgr.close();

       测试运行成功后可发现在新生成的代理类java文件同目录下生成了对应的class文件,即$Proxy0.class。

将新产生的代理类load到内存当中,产生一个新的对象即代理对象

       关键代码如下:

1
2
3
4
5
6
7
//load 到内存
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class c = cl.loadClass("com.wy.proxy.myproxy.$Proxy0");
//System.out.println(c.getName());
//创建代理对象
Constructor ctr = c.getConstructor(infce);
return ctr.newInstance(new Car());

       在Test类中测试是否创建成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.wy.proxy.myproxy;

public class Client {

/**
* 测试类
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Moveable m = (Moveable)Proxy.newProxyInstance(Moveable.class);
m.move();
}

}

       运行结果:

1
2
3
汽车开始行驶
汽车行驶中
汽车行驶结束,行驶时间为:68 毫秒

       可以看到通过使用自己定义的Proxy.newProxyInstance方法成功返回了代理对象,与一开始的时候使用JDK动态代理得到同样的结果。以下为自定义Proxy类的全部代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.wy.proxy.myproxy;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

import org.apache.commons.io.FileUtils;

import javax.tools.JavaCompiler.CompilationTask;

public class Proxy {

public static Object newProxyInstance(Class infce) throws Exception{
String rt = "\r\n";
String methodStr = "";
for(Method m : infce.getMethods()){
methodStr += " @Override" + rt +
" public void " + m.getName() + "() {" + rt +
" long starttime = System.currentTimeMillis();" + rt +
" System.out.println(\"汽车开始行驶\");" + rt +
" m." + m.getName() + "();" + rt +
" long endtime = System.currentTimeMillis();" + rt +
" System.out.println(\"汽车结束行驶,行驶时间为:\" " + rt +
" + (endtime - starttime) + \"毫秒\");" + rt +
" }" ;
}

String str =
"package com.wy.proxy.myproxy;" + rt +
"public class $Proxy0 implements " + infce.getName() + " {" + rt +
" public $Proxy0(" + infce.getName() + " m) {" + rt +
" super();" + rt +
" this.m = m;" + rt +
" }" + rt +
" private " + infce.getName() + " m;" + rt +
methodStr + rt +
"}" ;
//产生代理类的java文件
String filename = System.getProperty("user.dir") +"/bin/com/wy/proxy/myproxy/$Proxy0.java";
//System.out.println(filename);
File file = new File(filename);
FileUtils.writeStringToFile(file, str);

//编译
//拿到编译器
JavaCompiler complier = ToolProvider.getSystemJavaCompiler();
//文件管理者
StandardJavaFileManager fileMgr =
complier.getStandardFileManager(null, null, null);
//获取文件
Iterable units = fileMgr.getJavaFileObjects(filename);
//编译任务
CompilationTask t = complier.getTask(null, fileMgr, null, null, null, units);
//进行编译
t.call();
fileMgr.close();

//load 到内存
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class c = cl.loadClass("com.wy.proxy.myproxy.$Proxy0");
//System.out.println(c.getName());
Constructor ctr = c.getConstructor(infce);
return ctr.newInstance(new Car());

}
}

       目前这个Proxy类实现了对任意接口任意方法的代理,但是这只不过完成了一半,因为其中代理的逻辑是写死的,只能实现时间记录。这就需要继续改进,在JDK动态代理中是通过InvocationHandler来实现灵活改变代理逻辑的,继续模仿,我们自定义一个接口也叫做InvocationHandler。

1
2
3
4
5
6
7
8
package com.wy.proxy.myproxy;

import java.lang.reflect.Method;

public interface InvocationHandler {

public void invoke(Object o,Method m);
}

       这个接口需要的功能就是要实现对方法代理逻辑的灵活增改,传入参数为需要代理的对象和方法。然后创建一个具体的代理类实现InvocationHandler 接口,在其中具体实现代理逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.wy.proxy.myproxy;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class TimeHandler implements InvocationHandler {
//被代理对象
private Object target;

public TimeHandler(Object target) {
super();
this.target = target;
}
// 参数o为代理对象
@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) + "毫秒");
} catch (Exception e) {
e.printStackTrace();
}
}

}

       定义好对方法的事务处理器后,在Proxy类中引入使用。Proxy类修改后完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.wy.proxy.myproxy;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import javax.tools.JavaCompiler.CompilationTask;

import org.apache.commons.io.FileUtils;

public class Proxy {

@SuppressWarnings("unchecked")
public static Object newProxyInstance(Class infce,InvocationHandler h) throws Exception{
String rt = "\r\n";
String methodStr = "";
for(Method m : infce.getMethods()){
methodStr += " @Override" + rt +
" public void " + m.getName() + "() {" + rt +
" try{" + rt +
" Method md = " + infce.getName() + ".class.getMethod(\""
+ m.getName() + "\");" + rt +
" h.invoke(this,md);" +rt+
" }catch(Exception e){ e.printStackTrace();}" + rt +
" }" ;
}

String str =
"package com.wy.proxy.myproxy;" + rt +
"import java.lang.reflect.Method;" + rt +
"import com.wy.proxy.myproxy.InvocationHandler;" + rt+
"public class $Proxy0 implements " + infce.getName() + " {" + rt +
" public $Proxy0(InvocationHandler h) {" + rt +
" this.h = h;" + rt +
" }" + rt +
" private InvocationHandler h;" + rt+
methodStr + rt +
"}" ;
//产生代理类的java文件
String filename = System.getProperty("user.dir") +"/bin/com/wy/proxy/myproxy/$Proxy0.java";
File file = new File(filename);
FileUtils.writeStringToFile(file, str);

//编译
//拿到编译器
JavaCompiler complier = ToolProvider.getSystemJavaCompiler();
//文件管理者
StandardJavaFileManager fileMgr =
complier.getStandardFileManager(null, null, null);
//获取文件
Iterable units = fileMgr.getJavaFileObjects(filename);
//编译任务
CompilationTask t = complier.getTask(null, fileMgr, null, null, null, units);
//进行编译
t.call();
fileMgr.close();

//load 到内存
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class c = cl.loadClass("com.wy.proxy.myproxy.$Proxy0");

Constructor ctr = c.getConstructor(InvocationHandler.class);
return ctr.newInstance(h);
}

}

       测试如下:

1
2
3
汽车开始行驶
汽车行驶中
汽车行驶结束,行驶时间为:680 毫秒

       上述代码中并没有贴出Car类的代码,下面补出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.wy.proxy.myproxy;

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) {
e.printStackTrace();
}
}

}

       到此就完成了对JDK动态代理的简单模拟,大致思路就是首先声明一段源码,这段源码用来动态产生代理类。然后把源码转换生成为Java文件,然后对这个Java文件进行编译,生成对应的class文件。之后再用类加载器加载到内存中,并生成代理对象,返回之。

       在使用时需要先创建代理的具体逻辑即事务处理器InvocationHandler,事务处理器可以实现时间记录功能或日志功能。然后将写好的事务处理作为参数传入Proxy的newProxyInstance的方法,这样返回的就是我们需要的代理对象。

       代码详见 PatternTest

参考资料:
王英豪 模拟JDK动态代理实现
David 模式的秘密——代理模式

Fork me on GitHub