java-代理模式

作者 uunnfly 日期 2019-03-26
java-代理模式

本文参考自10分钟看懂动态代理设计模式

预备知识: 关联、聚合和组合

  • 关联(association),仅仅是两个对象有联系,一个对象用到了另一个对象
1
2
3
4
public class Foo { 
void Baz(Bar bar) {
}
};
  • 组合(composition),在一个对象生命周期内创建了另一个对象,当Foo死亡时,Bar也会死亡

    1
    2
    3
    public class Foo {
    private Bar bar = new Bar();
    }
  • 聚合(aggregation),一个对象借到了另一个对象,但是Foo的生命周期结束时,Bar的生命周期不一定结束

    1
    2
    3
    4
    5
    6
    public class Foo { 
    private Bar bar;
    Foo(Bar bar) {
    this.bar = bar;
    }
    }

What is the difference between association, aggregation, and composition?

静态代理模式

现在我们假设一个场景:老司机开车

先来个接口

1
2
3
public interface Dirver {
void drive();
}

再来个老司机

1
2
3
4
5
public class Veteran implements Dirver{
public void drive() {
System.out.println("I am driving!");
}
}

运行一下

1
2
3
4
5
6
public class Main {
public static void main(String[] args){
Driver driver = new Veteran();
driver.drive();
}
}

得到结果

1
I am driving!

可老司机觉得一个人开车很无聊,有个人就说:我为你计时吧,看看你有多快。这个帮老司机干与“开车”无关的事的人我们称为“agent”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class agent implements Driver{
private Driver driver;

public agent(Driver driver){
this.driver = driver;
}

public void drive() {
long start = System.currentTimeMillis();
System.out.println("before driving...");

driver.drive();

long end = System.currentTimeMillis();
System.out.println("after driving...");
System.out.println("drive time = " + (end - start));
}
}

上面的代码用到了聚合,agent从别的地方“借”到了一个driver。

现在我们再让老司机开车

1
2
3
4
5
6
public class Main {
public static void main(String[] args){
Driver driver = new Agent(new Veteran());
driver.drive();
}
}

1
2
3
4
before driving...
I am driving!
after driving...
drive time = 1

注意到上面我们调用的是agent的drive方法,agent再让veteran开车。

现在一切ok

动态代理

现在又有了新问题,老司机想让这个agent记录他干所有事的时间,比如eat(), sleep()…等等一百个方法,现在agent傻眼了,难不成他也要跟着写一百个方法吗?

现在我们想让同一个代理类(取个名字LogProxy)就能打印一个对象的任意方法的日志,而不需要反复实现相同的逻辑,怎么解决?

能不能利用反射动态地读取Driver的方法,比如eat(),drive(),sleep(),然后动态地写逻辑,也就是把原先Agent的drive()方法中的driver.drive()这一句动态地替换成driver.eat(),driver.sleep(),然后正常编译,运行,不就解决了?

首先我们需要把逻辑抽象出来

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

这个接口抽象的是“怎么做”,也就是代理类的处理逻辑,比如这里是记录日志这个功能。

下面我们会用到JavaPoet 这个工具类库,它用来动态生成java源代码,也就是.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
34
35
36
37
38
39
40
41
42
43
public class MyProxy {
public static Object newProxyInstance(Class inf, MyInvocationHandler handler) throws Exception{
TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("LogProxy")
.addSuperinterface(inf);

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

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

Method[] methods = inf.getDeclaredMethods();

for(Method method : methods){

MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder(method.getName())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(method.getReturnType())
.addCode("try {\n")
.addStatement("\t$T method = " + inf.getName() + ".class.getMethod(\"" + method.getName() + "\")", Method.class);
String statement;
//写死参数为null
String code = "this.handler.invoke(this, method, null)";
if(method.getReturnType().getName() == "void")
statement = "\t" + code;
else
statement = "\treturn " + code;

methodSpecBuilder.addStatement(statement)
.addStatement("\t")
.addCode("} catch(Exception e) {\n")
.addCode("\te.printStackTrace();\n")
.addCode("}\n");

typeSpecBuilder.addMethod(methodSpecBuilder.build());
}
}
}

newProxyInstance()的第一个参数是指委托类,也就是这里的driver,第二个参数是指代理类怎么做的规则。

1
2
3
4
String path = "./";
File file = new File(path);
JavaFile javaFile = JavaFile.builder("com.uunnfly.proxy",typeSpecBuilder.build()).build();
javaFile.writeTo(file);

生成源文件,我们也要让它编译

1
2
3
4
5
6
7
8
9
10
public class JavaCompiler {
public static void compile(File javaFile)throws IOException {
javax.tools.JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(null,null,null);
Iterable iterable = fileManager.getJavaFileObjects(javaFile);
javax.tools.JavaCompiler.CompilationTask task = javaCompiler.getTask(null,fileManager,null,null,null,iterable);
task.call();
fileManager.close();
}
}

回到MyProxy.java文件编译,加载并且在内存新建一个代理

1
2
3
4
5
6
7
8
9
10
JavaCompiler.compile(new File(path + "/com/uunnfly/proxy/LogProxy.java"));

URL[] urls = new URL[]{file.toURI().toURL()};
URLClassLoader classLoader = new URLClassLoader(urls);
Class clazz = classLoader.loadClass("com.uunnfly.proxy.LogProxy");

Constructor constructor = clazz.getConstructor(MyInvocationHandler.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(handler);
return obj;

现在我们再来试一下

1
2
3
4
5
6
7
    public static void main(String[] args)throws Exception{
// Driver driver = new Agent(new Veteran());
// driver.drive();

Driver driver = (Driver)MyProxy.newProxyInstance(Driver.class,new DynamicProxy(new Veteran()));
driver.drive();
}

你应该可以找到在本项目文件夹内生成的LogProxy.class和LogProxy.java文件

jdk中的动态代理

jdk已经原生提供了Proxy

Proxy.java

InvocationHandler

只要你在newProxyInstance方法中指定代理需要实现的接口,指定用于自定义处理的InvocationHandler对象,整个代理的逻辑处理都在你自定义的InvocationHandler实现类中进行处理。至此,而我们终于可以从不断地写代理类用于实现自定义逻辑的重复工作中解放出来了,从此需要做什么,交给InvocationHandler

源代码 https://github.com/UUNNFLY/myProxy