前言
上一讲【什么是代理(Proxy)?】我们已经演示过静态代理有一个重大缺陷,就是容易产生类爆炸。所以我们的代理对象不能自己写出来,也就意味着我们没有代理对象的.java
文件,没有办法new
出来这个东西。但是我又需要使用它,那我们应该怎么办呢?本篇就是要解决这个问题,通过手动实现一个动态代理,希望能给大家理解动态代理提供一个新的思路。更多Spring内容进入【Spring解读系列目录】。
代理对象的生成
Java里一个对象的生成一般是用new
语句来做的,在new
之前还有.class
文件,.class
文件之前还有.java
文件也就是类文件。就是说有一个java
对象的前提是必须要有一个类。那么为了避免类爆炸,不允许手动产生一个类文件,应该怎么办呢?有一个显而易见的答案,我们可以通过静态方法强行创建一个Object
类,然后强转过来。虽然现在还是null,但是至少有解决方案了。所以newInstance()
这个方法就是我们实现动态代理的舞台了。
public class ProxyUtil {
public static Object newInstance(){
return null;
}
}
public class MainTest {
public static void main(String[] args) {
QueryDao target=new QueryDaoImpl();
QueryDao proxy= (QueryDao) ProxyUtil.newInstance();
}
}
那么通过上面的分析,想要揍一个类出来,首先要有.java
文件,然后要有.class
文件,最后new
出来。那么第一步就是如何动态产生一个有内容可编译的.java
文件。比如我们要生成的就是下面这个代理
package com.demo.proxyImpl;
import com.demo.dao.QueryDao;
public class $Proxy implements QueryDao {
private QueryDao target;
public QueryDaoLog(QueryDao target) {
this.target = target;
}
@Override
public void query() {
System.out.println("---interface self proxy log---");
queryDao.query();
}
}
从0构建一个类
怎么从0构建呢?.java
文件里都是字符,所以我们就要把上面的整个文件里面的内容敲出来。所以我们就在newInstance()
把上面的类手动敲到系统里。
public class ProxyUtil {
public static Object newInstance(Object targetInfo) {
Class target=targetInfo.getClass().getInterfaces()[0]; //从接口里拿出类对象
Object proxy = null; //构造返回出去的代理对象
String line = "\n"; //换行
String tab = "\t"; //空格
String targetName = target.getSimpleName(); //构造类名行
//构造包行,对应package com.demo.proxyImpl;
String packagePath = "package com.demo.proxyDyn;" + line;
//构造导入行,对应import com.demo.dao.QueryDao;
String importPath = "import " + target.getName() + ";" + line;
//构造类定义行,对应public class $Proxy implements QueryDao
String classDefine = "public class $Proxy implements " + targetName + " {" + line;
//构造字段行,对应private QueryDao target;
String fieldDefine = tab + "private " + targetName + " target;" + line;
//构造构造方法行,对应public QueryDaoLog(QueryDao target) { ... }
String constructorDefine = tab + "public $Proxy (" + targetName + " target){ " + line
+ tab + tab + "this.target = target; " + line
+ tab + "}" + line;
//得到接口的所有方法,用数组存起来
Method[] methods = target.getDeclaredMethods();
String methodDefine = ""; //构造方法定义行
for (Method method : methods) { //循环遍历拿到的方法
String returnType = method.getReturnType().getSimpleName(); //拿到返回值
String methodName = method.getName(); //拿到方法名字
//得到方法的所有参数,用数组存起来
Class[] params = method.getParameterTypes();
String param = "";
String paramsLine = ""; //构造参数行
int count = 0;
for (Class obj : params) { //循环遍历参数类型
String temp = obj.getSimpleName(); //拿到参数名字
param += temp + " a" + count + ","; //构建一个循环的参数,这里就是a0
paramsLine += "a" + count + ", "; //拼成一个参数行
count++; //参数名+1
}
if (param.length() > 0) { //如果拿出来有参数
param = param.substring(0, param.lastIndexOf(",") - 1); //去掉最后的","
paramsLine = paramsLine.substring(0, paramsLine.lastIndexOf(",") - 1); //参数行去掉最后的","
}
methodDefine += tab + "public " + returnType + " " + methodName + "(" + param + ") {" + line
+ tab + tab + "System.out.println(\"---interface self proxy log---\");" + line
+ tab + tab + "target." + methodName + "(" + paramsLine + ");" + line
+ tab + "}"; //拼成方法行,对应:public void query() {...}
}
//最后把所有的字段组装在一起
String content=packagePath+importPath+classDefine+fieldDefine+constructorDefine+methodDefine+line+"}";
//写出去。
File file=new File("d:\\com\\demo\\proxyDyn");
try {
if (!file.exists()){
file.mkdirs();
file=new File("d:\\com\\demo\\proxyDyn\\$Proxy.java");
file.createNewFile();
}else {
file=new File("d:\\com\\demo\\proxyDyn\\$Proxy.java");
file.createNewFile();
}
FileOutputStream fw = new FileOutputStream (file);
fw.write(content.getBytes());
fw.flush();
fw.close();
}catch (Exception e) {
e.printStackTrace();
}
return proxy;
}
}
通过这个步骤,就能够生成一个java类了,运行一下就在D:\com\demo\proxyDyn
目录下得到了$Proxy.java
这个动态代理类。
package com.demo.proxyDyn;
import com.demo.dao.QueryDao;
public class $Proxy implements QueryDao {
private QueryDao target;
public $Proxy (QueryDao target){
this.target = target;
}
public void query() {
System.out.println("---interface self proxy log---");
target.query();
}
}
接着我们要用java的编译器动态构造一个.class
文件。
创建.class文件
怎么构造.class
文件呢?以往我们都是用maven
、eclipse
或者idea
帮助我们编译的。这次我们手动来,感谢JDK提供的有JavaCompiler
这个编译类,然后通过ToolProvider
拿到编译对象,拿到以后就能够动态编译文件了。JDK会使用一个文件管理器去做这个事情,所以new了一个文件管理器StandardJavaFileManager
。然后把上面的file
对象传递给文件管理器,最后把构造的管理器交给编译任务对象CompilationTask
,再有这个任务对象调用call()
方法执行编译,就得到了.class
文件。下面的代码,我们直接就贴到上面的try-catch
里面,直接运行就可以了。
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
Iterable units = fileMgr.getJavaFileObjects(file);
JavaCompiler.CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);
t.call();
fileMgr.close();
通过这个步骤,.class
文件也搞定了,下面就该产生一个对象了。
产生一个可用对象
我们现在有类了,有编译字节码了,但是我们直接不能new,因为我们的类不在我们的项目里,在磁盘上,编译器看不到$Proxy这个类。这时候我们就必须要借助ClassLoader
来做了。这时候就要用到加载外部的类加载器UrlclassLoader
来处理了。
//外部获取java类
//拿到目标url,因为我们下面配置的有类的包名,而包早已生成,所以这里只写d盘就可以
URL[] urls = new URL[]{new URL("file:D:\\\\")};
//给类加载器配置上url
URLClassLoader urlClassLoader = new URLClassLoader(urls);
//加载类配置上我们的代理类
Class clazz = urlClassLoader.loadClass("com.demo.proxyDyn.$Proxy");
//得到构造方法,能够构造一个对象的方法
Constructor constructor = clazz.getConstructor(target);
//通过构造方法生成对象
proxy = constructor.newInstance(targetInfo);
把上面代码一样贴到fileMgr.close();后面,我们就完成了一个动态代理类。那么我们去测试一下。
public class MainTest {
public static void main(String[] args) {
QueryDao target=new QueryDaoImpl();
QueryDao proxy= (QueryDao) ProxyUtil.newInstance(target);
proxy.query();
}
}
结果,完成了我们的代理,没有手动生成Log类,一样完成了log的打印
---interface self proxy log---
查询数据库内容
因为我们写到还有参数,所以我们还可以改造一下QueryDao
,让里面的方法有参数,我们就打印一下这个参数。
public interface QueryDao {
void query(String a);
}
public class QueryDaoImpl implements QueryDao{
@Override
public void query(String a) {
System.out.println("查询数据限定"+a);
}
}
public class MainTest {
public static void main(String[] args) {
QueryDao target=new QueryDaoImpl();
QueryDao proxy= (QueryDao) ProxyUtil.newInstance(target);
proxy.query("name=tom");
}
}
结果,完成了我们的代理,没有手动生成Log类,一样完成了log的打印,而且传递的参数也是有效的
---interface self proxy log---
查询数据限定name=tom
结论
通过模拟一个动态代理的过程,可以很明现的看到,动态代理不需要手动创建类,一样可以把我们的log
的内容---interface self proxy log---
载入到目标方法中,可以有效的避免类爆炸的问题。我们通过接口反射生成一个类文件,然后JDK提供的JavaCompiler
完成动态编译这个过程,去产生class文件。字节码文件产生后再利用UrlclassLoader
这个外部获取类文件的类加载器把这个动态编译的类加载到jvm
当中,最后通过反射把这个类实例化。
但是我们也要看到缺点,首先要生成文件,其次还要动态编译文件 class
,而且必须要引入一个URLclassloader
才能完成整个流程。因此这个代理类的最终性能完全被限制在IO
上。因此我们应该由衷的感谢Java界的各位大神,为我们创造了Spring Framework,JDK动态代理甚至CGLIB,等等动态代理技术,使我们的代码量呈指数级的减少,并且只要关注业务逻辑就可以了。相关博客【JDK动态代理牛在哪里】。