设计模式之动态代理(一)

代理

代理的意思就是把一些事情让别人去做,比如你想吃一家饭店的东西,你可以点外卖,让外卖小哥帮你送到家,你就不用自己去饭店买了,还有你想出国留学,那你可以把信息给留学结构,让他帮你办理一些留学的证件,像这样的事情都是代理,让别人帮你去做,受益的主体还是你自己。

静态代理

在计算机中,静态代理是一种比较好理解的行为,就是专门为某一个类或者接口专门生成一个代理类。

1
2
3
4
public interface Animal {
void eat();
void sleep();
}

1
2
3
4
5
6
7
8
9
10
11
12
public class Dog implements Animal {

@Override
public void eat() {
System.out.println("Dog eat...");
}

@Override
public void sleep() {
System.out.println("Dog sleep...");
}
}

上面是一个Animal接口,有两个方法eat和sleep,有一个实现类Dog,实现了eat和sleep方法。现在我们想知道eat这个方法在系统里运行了多长时间,大家可以想一想有什么实现的方法。
方法一 继承:

1
2
3
4
5
6
7
8
9
10
public class DogExtend extends Dog {

@Override
public void eat() {
long start = System.currentTimeMillis();
super.eat();
long end = System.currentTimeMillis();
System.out.println("Dog run time is " + (end - start));
}
}

方法二 聚合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DogHave implements Animal {

private Dog dog;

public DogHave(Dog dog) {
this.dog = dog;
}

@Override
public void eat() {
long start = System.currentTimeMillis();
dog.eat();
long end = System.currentTimeMillis();
System.out.println("Dog run time is " + (end - start));
}

@Override
public void sleep() {
}
}

上面两种方法都可以完成我们的需求,那我们现在接着提需求,我们现在还想做一个日志记录,在eat开始和结束分别打印日志。
方法一 继承:
public class DogExtend extends Dog {

@Override
public void eat() {
    long start = System.currentTimeMillis();
    System.out.println("Dog start...");
    super.eat();
    System.out.println("Dog end...");
    long end = System.currentTimeMillis();
    System.out.println("Dog run time is " + (end - start));
}

}
方法二 聚合1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DogHave implements Animal {

private Dog dog;

public DogHave(Dog dog) {
this.dog = dog;
}

@Override
public void eat() {
long start = System.currentTimeMillis();
System.out.println("Dog start...");
dog.eat();
System.out.println("Dog end...");
long end = System.currentTimeMillis();
System.out.println("Dog run time is " + (end - start));
}

@Override
public void sleep() {
}
}

方法三 聚合2:
再实现一个打印日志的聚合类叫DogLog,把之前的DogHave改为DogTime。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DogTime implements Animal {

private Animal dog;

public DogTime(Animal dog) {
this.dog = dog;
}

@Override
public void eat() {
long start = System.currentTimeMillis();
dog.eat();
long end = System.currentTimeMillis();
System.out.println("Dog run time is " + (end - start));
}

@Override
public void sleep() {
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DogLog implements Animal {

private Animal dog;

public DogLog(Animal dog) {
this.dog = dog;
}

@Override
public void eat() {
System.out.println("Dog eat start...");
dog.eat();
System.out.println("Dog eat end...");
}

@Override
public void sleep() {

}
}

main方法的调用顺序为dogTime.eat->dogLog.eat->dog.eat:

1
2
3
4
5
6
7
8
9
public class main {

public static void main(String[] args) throws Exception {
Dog dog = new Dog();
DogLog dogLog = new DogLog(dog);
DogTime dogTime = new DogTime(dogLog);
dogTime.eat();
}
}

结果为:

1
2
3
4
Dog eat start...
Dog eat...
Dog eat end...
Dog run time is 1

上面这三种实现方法看起来都没什么问题,那我们就接着再提需求,刚才的实现是先执行日志的打印,然后再执行时间记录的,我们现在想先执行时间记录,再进行开始结束日志的打印。再来看下实现:
方法一 继承:
要新写一个继承类,然后调整代理方法里面的顺序。
方法二 聚合1:
同方法一改动一样。
方法三 聚合2:
只用改动main方法中的代码顺序就能实现:

1
2
3
4
5
6
7
8
9
public class main {

public static void main(String[] args) throws Exception {
Dog dog = new Dog();
DogLog dogLog = new DogLog(dog);
DogTime dogTime = new DogTime(dogLog);
dogTime.eat();
}
}

改为

1
2
3
4
5
6
7
8
9
public class main {

public static void main(String[] args) throws Exception {
Dog dog = new Dog();
DogTime dogTime = new DogTime(dog);
DogLog dogLog = new DogLog(dogTime);
dogLog.eat();
}
}

调用顺序就变成了dogLog.eat->dogTime.eat->dog.eat。
可以看到方法三的实现方法最简单,而且如果通过继承的方式实现代理的话,DogExtend只能实现Dog的代理,比如我们现在想打印一个Cat类(实现了Animal接口)的运行时间,通过继承的方式就要新写一个继承类,而通过聚合方式实现代理可以实现任意Animal实现类的eat方法,DogTime也可以改名为AnimalTime了。这个就是多态的作用。

1
2
3
4
5
6
7
8
9
public class main {

public static void main(String[] args) throws Exception {
Cat cat = new Cat();
DogTime dogTime = new DogTime(cat);
DogLog dogLog = new DogLog(dogTime);
dogLog.eat();
}
}

上面的方法三已经很好的满足了我们的上述的需求,但是还没有看到我们所谓的动态代理,我们就接着提需求,既然我们已经实现了eat的方法运行时间的计算和日志的打印,那现在也想实现sleep方法的时间计算和日志打印,我们是不是就要在DogTime和DogLog上写和eat方法重复的代码来实现这些需求,假设Animal接口有100个方法都要这样做,那我们就要实现100次重复的代码。

动态代理

我们试着想象一下,如果我们可以编写一个TimeProxy的类,然后可以实现任意对象和任意方法的代理,那就变得很完美了,这样我们想实现哪个方法的代理都可以。要实现这个类就必须要用到动态生成代码的技术,恰好Java提供了这样的能力,可以通过操作字节码来动态生成代码,比较出名的开源库有cglib和asm,我们这里使用JavaPoet这个第三方库来帮我们生成TimeProxy这个代理类。

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
public class Proxy {
public static Object newProxyInstance() throws Exception {
Class clazz = Animal.class;
TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("TimeProxy")
.addSuperinterface(clazz);

FieldSpec fieldSpec = FieldSpec.builder(clazz, "animal", Modifier.PRIVATE).build();
typeSpecBuilder.addField(fieldSpec);

MethodSpec constructorMethodSpec = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(clazz, "animal")
.addStatement("this.animal = animal")
.build();
typeSpecBuilder.addMethod(constructorMethodSpec);

Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
MethodSpec methodSpec = MethodSpec.methodBuilder(method.getName())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(method.getReturnType())
.addStatement("long start = $T.currentTimeMillis()", System.class)
.addCode("\n")
.addStatement("this.animal." + method.getName() + "()")
.addCode("\n")
.addStatement("long end = $T.currentTimeMillis()", System.class)
.addStatement("$T.out.println(\""+ method.getName() + " run time \" + (end - start))", System.class)
.build();
typeSpecBuilder.addMethod(methodSpec);
}

String sourcePath = "/Users/feidao/Desktop/";
JavaFile javaFile = JavaFile.builder("com.feidao.proxy", typeSpecBuilder.build()).build();
// 为了看的更清楚,我将源码文件生成到桌面
javaFile.writeTo(new File(sourcePath));

JavaCompiler.compile(new File(sourcePath + "com/feidao/proxy/TimeProxy.java"));

return null;
}


}

上面通过动态生成TimeProxy.java类,然后通过JavaCompiler.compile(new File(sourcePath + “com/feidao/proxy/TimeProxy.java”))编译成TimeProxy.class。
Proxy.newProxyInstance()可以生成TimeProxy.java和TimeProxy.class,可以看下生成的TimeProxy.java的代码:

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
package com.feidao.proxy;

import java.lang.Override;
import java.lang.System;
import proxy.Animal;

class TimeProxy implements Animal {
private Animal animal;

public TimeProxy(Animal animal) {
this.animal = animal;
}

@Override
public void sleep() {
long start = System.currentTimeMillis();

this.animal.sleep();

long end = System.currentTimeMillis();
System.out.println("sleep run time " + (end - start));
}

@Override
public void eat() {
long start = System.currentTimeMillis();

this.animal.eat();

long end = System.currentTimeMillis();
System.out.println("eat run time " + (end - start));
}
}

然后再通过反射把TimeProxy加载到内存,分别调用代理类的eat和sleep方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class main {

public static void main(String[] args) throws Exception {
// 动态生成TimeProxy
Proxy.newProxyInstance();

// 加载到内存
String sourcePath = "/Users/feidao/Desktop/";
URL[] urls = new URL[] {new URL("file:" + sourcePath)};
URLClassLoader classLoader = new URLClassLoader(urls);
Class clazz = classLoader.loadClass("com.feidao.proxy.TimeProxy");
Constructor constructor = clazz.getConstructor(Animal.class);
constructor.setAccessible(true);
Animal animal = (Animal) constructor.newInstance(new Dog());

animal.eat();

animal.sleep();
}
}

运行结果:

1
2
3
4
Dog eat...
eat run time 0
Dog sleep...
sleep run time 0

可以看到,我们实现了一个TimeProxy就可以对Animal的任意方法进行时间计算的代理了,到现在并没有实现对任意对象的任意方法的代理,我们还要进行改进,可以把我们要代理的接口传入Proxy.newProxyInstance方法,然后拿到接口的所有方法,进行代理。代码改为:

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
public class Proxy {
public static Object newProxyInstance(Class clazz) throws Exception {
TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("TimeProxy")
.addSuperinterface(clazz);

FieldSpec fieldSpec = FieldSpec.builder(clazz, "animal", Modifier.PRIVATE).build();
typeSpecBuilder.addField(fieldSpec);

MethodSpec constructorMethodSpec = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(clazz, "animal")
.addStatement("this.animal = animal")
.build();
typeSpecBuilder.addMethod(constructorMethodSpec);

Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
MethodSpec methodSpec = MethodSpec.methodBuilder(method.getName())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(method.getReturnType())
.addStatement("long start = $T.currentTimeMillis()", System.class)
.addCode("\n")
.addStatement("this.animal." + method.getName() + "()")
.addCode("\n")
.addStatement("long end = $T.currentTimeMillis()", System.class)
.addStatement("$T.out.println(\""+ method.getName() + " run time \" + (end - start))", System.class)
.build();
typeSpecBuilder.addMethod(methodSpec);
}

String sourcePath = "/Users/feidao/Desktop/";
JavaFile javaFile = JavaFile.builder("com.feidao.proxy", typeSpecBuilder.build()).build();
// 为了看的更清楚,我将源码文件生成到桌面
javaFile.writeTo(new File(sourcePath));

JavaCompiler.compile(new File(sourcePath + "com/feidao/proxy/TimeProxy.java"));

return null;
}
}

我们实验一下看能不能对其他接口进行代理,新建一个Human和Man的实现类:

1
2
3
public interface Human {
void work();
}

1
2
3
4
5
6
public class Man implements Human {
@Override
public void work() {
System.out.println("Man work...");
}
}

调用代理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class main {

public static void main(String[] args) throws Exception {
// 动态生成TimeProxy
Proxy.newProxyInstance(Human.class);

// 加载到内存
String sourcePath = "/Users/feidao/Desktop/";
URL[] urls = new URL[] {new URL("file:" + sourcePath)};
URLClassLoader classLoader = new URLClassLoader(urls);
Class clazz = classLoader.loadClass("com.feidao.proxy.TimeProxy");
Constructor constructor = clazz.getConstructor(Human.class);
constructor.setAccessible(true);
Human human = (Human) constructor.newInstance(new Man());

human.work();
}
}

运行结果:

1
2
Man work...
work run time 0

所以到目前为止,我们就实现了对任意对象的任意方法进行时间计算的代理。但是如果我们要实现一个日志打印的代理,还要新写一个Proxy进行对日志打印的代理,就是代理逻辑和动态生成字节码的耦合比较严重,我们可以把代理的逻辑提取出来,新写一个InvocationHandler的接口,代理类的逻辑可以实现这个接口,就把动态生成字节码和代理逻辑解耦。我们来看看实现:
InvocationHandler接口:

1
2
3
public interface InvocationHandler {
void invoke(Object proxy, Method method, Object[] args);
}

TimeInvocationHandler接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TimeInvocationHandler implements InvocationHandler {

private Object realObject;

public TimeInvocationHandler(Object realObject) {
this.realObject = realObject;
}

@Override
public void invoke(Object proxy, Method method, Object[] args) {
long start = System.currentTimeMillis();
try {
method.invoke(realObject, new Object[]{});
} catch (Exception e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();

System.out.println(method.getName() + "run time " + (end - start));
}
}

Proxy.newProxyInstance方法:

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
public class Proxy {
public static Object newProxyInstance(Class clazz, InvocationHandler handler) throws Exception {
Class handlerClass = InvocationHandler.class;
TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("TimeProxy")
.addSuperinterface(clazz);

FieldSpec fieldSpec = FieldSpec.builder(handlerClass, "handler", Modifier.PRIVATE).build();
typeSpecBuilder.addField(fieldSpec);

MethodSpec constructorMethodSpec = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(handlerClass, "handler")
.addStatement("this.handler = handler")
.build();
typeSpecBuilder.addMethod(constructorMethodSpec);

Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
MethodSpec methodSpec = MethodSpec.methodBuilder(method.getName())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(method.getReturnType())
.addCode("try {\n")
.addStatement("\t$T method = " + clazz.getName() + ".class.getMethod(\"" + method.getName() + "\")", Method.class)
// 为了简单起见,这里参数直接写死为空
.addStatement("\tthis.handler.invoke(this, method, null)")
.addCode("} catch(Exception e) {\n")
.addCode("\te.printStackTrace();\n")
.addCode("}\n")
.build();
typeSpecBuilder.addMethod(methodSpec);
}

String sourcePath = "/Users/feidao/Desktop/";
JavaFile javaFile = JavaFile.builder("com.feidao.proxy", typeSpecBuilder.build()).build();
// 为了看的更清楚,我将源码文件生成到桌面
javaFile.writeTo(new File(sourcePath));

JavaCompiler.compile(new File(sourcePath + "com/feidao/proxy/TimeProxy.java"));

// 使用反射load到内存
URL[] urls = new URL[] {new URL("file:" + sourcePath)};
URLClassLoader classLoader = new URLClassLoader(urls);
Class proxyClass = classLoader.loadClass("com.feidao.proxy.TimeProxy");
Constructor constructor = proxyClass.getConstructor(handlerClass);
constructor.setAccessible(true);
Object obj = constructor.newInstance(handler);

return obj;
}
}

main方法:

1
2
3
4
5
6
7
8
9
public class main {
public static void main(String[] args) throws Exception {
TimeInvocationHandler timeHandler = new TimeInvocationHandler(new Man());
// 动态生成TimeProxy
Human human = (Human) Proxy.newProxyInstance(Human.class, timeHandler);

human.work();
}
}

我们再看看生成的TimeProxy类:

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.feidao.proxy;

import java.lang.Override;
import java.lang.reflect.Method;
import proxy.Human;
import proxy.InvocationHandler;

class TimeProxy implements Human {
private InvocationHandler handler;

public TimeProxy(InvocationHandler handler) {
this.handler = handler;
}

@Override
public void work() {
try {
Method method = proxy.Human.class.getMethod("work");
this.handler.invoke(this, method, null);
} catch(Exception e) {
e.printStackTrace();
}
}
}

从TimeProxy中可以看到,TimeProxy代理类中调用了handler.invoke方法,而invoke方法中调用了method.invoke(realObject, new Object[]{}),所以整个调用流程就变成了TimeProxy.work->handler.invoke->man.work,handler里面拥有被代理的对象,这样做的好处就是,当我们想实现一个LogProxy时,只需要把代理逻辑写到LogInvocationHandler里面,不用修改Proxy.newProxyInstance方法了。

总结

其实这就是JDK动态代理的实现原理,只不过有一些细节我们没涉及到,比如方法的入参和返回值,还有一些问题我还没想清楚,比如,这种方式实现的动态代理能不能嵌套,比如有两个handler,一个是LogHandler、一个TimeHandler,那么动态代理能同事实现这两个代理逻辑吗?还有就是动态代理必须通过接口实现吗?通过CGLIB实现动态代理的原理是怎么样的?等下次把这些问题弄清楚,我再更新一篇动态代理的文章。

<参考> https://juejin.im/post/5a99048a6fb9a028d5668e62#heading-11

Feidao wechat
关注我,一起打怪升级吧!