什么是动态代理?通过手动实现动态代理来理解

前言

上一讲【什么是代理(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文件呢?以往我们都是用maveneclipse或者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动态代理牛在哪里】

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值