上一篇讲了静态代理,我们发现静态代理需要显式的编写代理类,并且同样的代理功能(比如加日志),需要横向重复编写(即针对每个类型的接口都要使用相同代码进行实现),这会让项目整体变得十分臃肿,肯定不可取。我们来列举一下静态代理的弊端:
- 横向:需要显式编写的代理类太多,针对每个类型的接口都要编写功能的代理类。
- 纵向:相同的代理功能(如记录时间、记录日志),在每个代理类上都要重复编写。
基于静态代理的上述弊端,思考是否可以设计一个功能来进行简化。我们发现,其实前面写的那些静态代理类,它们变化的地方是:
- 不同类型的代理对象。
- 不同的代理功能。
除此之外,其实它们没什么区别。那么,假设我们可以设计一个Proxy类,并且有一个方法可以通过用户传入的参数来动态的获取其代理对象:
public class Proxy {
/**
* 动态地通过给定的参数获取代理对象
* @param clazz 被代理对象的类型
* @param proxyType 代理功能(还没确定方案,暂时用字符串代替)
* @return 代理对象
*/
public static Object newProxyInstance(Class clazz, String proxyType);
}
我们发现,如果这个方法真的能实现的话,那前边我们所使用的静态代理类诸如MovableTimeProxy、MovableLogProxy就完全不需要我们自己去实现了,只需要告诉该方法我们要对Movable进行代理,并且代理功能是time或是log就可以了。
我们先对该方法进行简化版的实现,假设现在仅仅是对Movable接口实现加日志的代理功能,我们该如何通过该方法给用户返回一个Movable的代理对象MovableLogProxy呢?在之前讲的静态代理中,由于我们显式的编写了一个代理类MovableLogProxy.java,因此可以通过new的方式来创建该代理对象并返回给用户。但是在我们这个方法的设计中,MovableLogProxy.java压根就不存在,那我们要返回什么代理对象给用户呢?我们先来思考一个问题,用户感知不到MovableTimeProxy、MovableLogProxy这些代理对象的存在,但是这些代理对象是否真的不存在呢?
答案一定是否定的,因为我们设计的这个方法要返回的就是MovableTimeProxy、MovableLogProxy这些代理对象,我们要返回的对象一定是在被代理对象方法调用的基础上进行方法增强,只是这些对象并不需要调用者感知到而已。那么,此时我们面临的问题便是,环境中没有MovableLogProxy.java这种用于做代理的类文件,但是我们最终不仅需要这样的类,还需要创建该类的实例并返回给调用者。此时我有一个思路,我们按照这个思路去试验是否能行得通:
- 通过IO写一个这样的代理类的.java文件。
- 将写好的文件,在程序运行时做类似javac的操作进行编译。
- 通过classLoader将编译好的class文件加载到内存。
- 利用反射将class进行实例化。
- 将该实例返回给调用者。
自动生成代理对象的代理类:
import cn.rain.design.proxy.demo1.model.Movable;
import cn.rain.design.proxy.demo1.model.Car;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileWriter;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLClassLoader;
/**
* description: 设计一个方法,根据用户传入的类型和方法增强方式,生成相应的代理类。这里为了简化,
* 我们先不考虑用户传入的类型和方法增强方式,假定现在为上一篇中的Car进行代理,方法增强方式记录日志
* @author 左边
* @date 2018-03-19 16:25:50
*/
public class MyProxy {
/**
* 调用该方法可以获取Movable的日志代理对象
* @return Movable的日志代理对象
*/
public static Object newProxyInstance() throws Exception {
// 换行符,为了使生成的.java文件的具备可读性
String rt = "\r\n";
// Movable的日志代理对象的字符串
String src =
"package cn.rain.design.proxy.demo2;" + rt +
"import cn.rain.design.proxy.demo1.model.Movable;" + rt + rt +
"/**" + rt +
" * 这是由CompileTest生成的java文件,并且已经进行了编译!!!" + rt +
" *" + rt +
" * @author 左边" + rt +
" * @date 2020-03-12 14:39:50" + rt +
" */" + rt +
"public class TempProxy implements Movable {" + rt + rt +
" private Movable movableThing;" + rt + rt +
" public TempProxy(Movable movableThing) {" + rt +
" this.movableThing = movableThing;" + rt +
" }" + rt + rt +
" @Override"+ rt +
" public void move() {" + rt +
" System.out.println(\"logger is start!\");" + rt +
" movableThing.move();" + rt +
" System.out.println(\"logger is end!\");" + rt +
" }" + rt +
"}" ;
// 获取项目所在路径
String projectPath = System.getProperty("user.dir");
// 通过项目路径拼成一个路径,该路径与当前类路径相同
String filePath = projectPath + "/src/main/java/cn/rain/design/proxy/demo2/TempProxy.java";
// 使用IO将src写入文件
File file = new File(filePath);
FileWriter writer = new FileWriter(file);
writer.write(src);
writer.flush();
writer.close();
// 获取JDK中的JavaCompiler (jdk 1.6以上)
JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
/*
* 编译文件必须要通过StandardJavaFileManager,因此我们先通过javaCompiler获取它。
* 第一个参数diagnosticListener是传入一个编译过程用于非致命错误诊断的监听器,传入null则使用默认监听器。
* 后两个参数是用于国际化的,传null意味着使用我们系统默认的。
*/
StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(null, null, null);
// 编译前目标文件将会被封装成文件对象,我们通过fileManager获取到目标文件的对象的迭代器。
Iterable<? extends JavaFileObject> targetFiles = fileManager.getJavaFileObjects(filePath);
// 获取编译任务,准备编译
JavaCompiler.CompilationTask task = javaCompiler.getTask(null, fileManager, null, null, null, targetFiles);
//执行编译
task.call();
//关闭fileManager
fileManager.close();
// 由于ClassLoader要求class文件必须在classpath路径下存在,由于classpath是可以删掉的(随时可以生产)
// 因此这里我们使用URLClassLoader,可以任意指定路径
URL[] urlArr = new URL[]{new URL("file:/" + System.getProperty("user.dir") + "/src")};
URLClassLoader urlClassLoader = new URLClassLoader(urlArr);
Class<?> clazz = null;
try {
// 将该路径的class文件load到内存
clazz = urlClassLoader.loadClass("cn.rain.design.proxy.demo2.TempProxy");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}finally {
urlClassLoader.close();
}
// 通过反射获取实例
Constructor<?> constructor = clazz.getConstructor(Movable.class);
return constructor.newInstance(new Car());
}
}
测试获取代理对象:
public class CompileTest {
public static void main(String[] args) throws Exception {
Movable moveProxy = (Movable) MyProxy.newProxyInstance();
moveProxy.move();
}
}
生成的TempProxy.java文件和编译后的TempProxy.class文件:
调用代理对象move()方法的输出结果:
logger is start!
Car moving....
logger is end!
结果和我们预期的一样,newProxyInstance()方法返回了Movable的日志代理对象,当我们调用该代理对象的move方法时,在我们原有的输出语句前后都加了日志。作为用户我们所要做的仅仅是调用 MyProxy.newProxyInstance()获取代理对象即可,但是该方法中的一系列操作我们是毫无感知的,我们甚至都不知道有TempProxy这样一个在运行中被编译的对象。
但是该方法还没有达到我们最终想要的结果,现在还有一些地方是在内部写死的,而我们希望可以灵活控制它们:
- 返回的代理对象不应该是固定的,而是根据用户传入的类型生成相应的代理对象。
- 对于原有的方法增强,不应该是固定的加日志的方式,而是允许用户灵活控制怎样增强。
如果上面两点我们能够改进的话,那么这个方法就非常的灵活了,详情请看下一篇的分析。