模拟JDK动态代理 ; 自己动手模拟实现java动态代理

    xiaoxiao2021-03-26  26

    大家在看java设计模式之 代理模式这篇文章的时候, 可以发现动态代理无非就是以下四个步骤,我们完全可以自己模拟实现。因为java的class文件格式是公开的,只要最终生成的class格式正确并且可以加载到JVM中我们就可以正常使用啦。

    创建代理类的源码;对源码进行编译成字节码;将字节码加载到内存;实例化代理类对象并返回给调用者;

    使用聚合模式实现静态代理

    本质上,动态代理是在程序运行过程中创建生成一个类并且将它加载到JVM中,通过上面的实现步骤,他是把额外的代码(spring中叫切面)植入到被代理类(方法)中以后合成一个类。与静态代理的实现是一样的.

    下面这段代码就是聚合模式实现的静态代理:

    public interface HelloWorld { void sayHello(String name); } public class HelloWorldImpl implements HelloWorld { @Override public void sayHello(String name) { try { Thread.sleep(1500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我是RealSubject。 Hello " + name); } } /** * Created by liubenlong on 2017/2/4. * 聚合模式实现静态代理 */ public class HelloWorldProxy implements HelloWorld { private HelloWorldImpl helloWorld; public HelloWorldProxy(HelloWorldImpl helloWorld){ this.helloWorld = helloWorld; } @Override public void sayHello(String name) { System.out.println("before log..."); helloWorld.sayHello(name); System.out.println("end log...。该方法总共执行时间是10毫秒。"); } }

    测试代码:

    HelloWorldProxy helloWorldProxy = new HelloWorldProxy(new HelloWorldImpl()); helloWorldProxy.sayHello("zhangsan");

    结果:

    before log... 我是RealSubject。 Hello zhangsan end log...。该方法总共执行时间是10毫秒。

    使用JDK自带的JavaCompiler

    必须jdk6及以上

    假设我们已经组装好了代码(其实就是上面那段静态代理的代码),我们就可以将其加载到JVM中来使用了。

    这里说的组装代码的过程在JDK中是使用反射来完成的。

    package com.lbl.proxy; import org.apache.commons.io.FileUtils; import javax.tools.JavaCompiler; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; import java.io.File; import java.lang.reflect.Constructor; import java.net.URL; import java.net.URLClassLoader; public class Proxy { private static final String RT = "\r\n"; public static Object newProxyInstance(Class classObj) throws Exception{ //声明一段源码 String sourceCode = "package com.lbl.proxy;" + RT + "/**" + RT + " * Created by liubenlong on 2017/2/4." + RT + "* 聚合模式实现静态代理" + RT + "*/" + RT + "public class $Proxy1 implements HelloWorld {" + RT + " private HelloWorld helloWorld;" + RT + " public $Proxy1(HelloWorld helloWorld){" + RT + " this.helloWorld = helloWorld;" + RT + " }" + RT + " @Override" + RT + " public void sayHello(String name) {" + RT + " System.out.println(\"before log...\");" + RT + " helloWorld.sayHello(name);" + RT + " System.out.println(\"end log...。该方法总共执行时间是10毫秒。\");" + RT + " }" + RT + "}"; String filename = System.getProperty("user.dir") + "/src/main/java/com/lbl/proxy/$Proxy1.java"; File file = new File(filename); //使用org.apache.commons.io.FileUtils.writeStringToFile()将源码写入磁盘 //编写到处,可以运行一下程序,可以在当前目录中看到生成的.java文件 FileUtils.writeStringToFile(file,sourceCode); //获得当前系统中的编译器 JavaCompiler complier = ToolProvider.getSystemJavaCompiler(); System.out.println(complier.getClass().getName()); //获得文件管理者 StandardJavaFileManager fileMgr = complier.getStandardFileManager(null, null, null); Iterable its = fileMgr.getJavaFileObjects(filename); //编译任务 JavaCompiler.CompilationTask task = complier.getTask(null, fileMgr, null, null, null, its); //开始编译,执行完可在当前目录下看到.class文件 task.call(); fileMgr.close(); //load到内存 URL[] urls = new URL[]{new URL("file:/" + System.getProperty("user.dir") + "/src")}; URLClassLoader urlClassLoader = new URLClassLoader(urls); Class cls = urlClassLoader.loadClass("com.lbl.proxy.$Proxy1"); //生成代理类对象 Constructor ct = cls.getConstructor(HelloWorld.class);//找一个参数类型是HelloWorldImpl.class的构造方法 return ct.newInstance(new HelloWorldImpl()); } } class test{ public static void main(String[] args) throws Exception { HelloWorld proxyObj = (HelloWorld) Proxy.newProxyInstance(HelloWorld.class); proxyObj.sayHello("zhangsan"); } }

    代码的含义上面注释已经写得很清楚了。运行一下,可以看到在项目目录下出现两个类:$Proxy1.java和$Proxy1.class。

    输出结果:

    com.sun.tools.javac.api.JavacTool before log... 我是RealSubject。 Hello zhangsan end log...。该方法总共执行时间是10毫秒。

    深度模拟

    上面的模拟代码中,只能支持实现了HelloWOrld接口的类。那么怎么才能实现与接口无关,支持任何接口,并且将植入的代码独立出来呢?也就是把生成代理类的代码独立出来与业务无关,这也满足设计模式中的**低耦合、职责单一**原则。

    要想支持任何接口,即生成代理代码的类与实际的接口无关。这个比较简单,因为我们的类都是实现了接口的,而接口中有哪些方法和参数,都是可以通过反射获取到的,那么我们只需要将其实现的接口传递过去即可。

    那么怎么将具体的和业务相关的代理逻辑代码抽离出来呢?前面提到了反射,反射可以通过Method调用某个方法,那么我们就可以在调用方法前后做一些事情了。

    通过上面的简单分析可以发现,关键点在于反射。我们可以把接口和代理实现作为参数传递进去就好啦。

    我们要做的就是把接口中定义的方法通过反射的方式获取到,进行生成代码。并且将其内部实现转移到Handler中去。Handler类是具体的要植入的代码类。就是把生成的下面这段代码修改掉:

    @Override public void sayHello(String name) { System.out.println("before log..."); helloWorld.sayHello(name); System.out.println("end log...。该方法总共执行时间是10毫秒。"); }

    代理可以有多种多样,怎么取写代码呢?每当出现多种不同的实现的时候我们就应该考虑到java的多态了。我们把代理抽象出一个接口MyInvocationHandler,里面只有一个方法,该方法用于对被代理对象的调用。

    类图如下

    import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * Created by liubenlong on 2017/2/4. */ public interface MyInvocationHandler { public Object invoke(Method method, Object o, Object ... params) throws InvocationTargetException, IllegalAccessException; }

    我们写两个实现类:日志代理类和计时代理类:

    import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * Created by liubenlong on 2017/2/4. */ public class MyLogProxy implements MyInvocationHandler { @Override public Object invoke(Method method, Object o, Object ... params) throws InvocationTargetException, IllegalAccessException { System.out.println("aa"); Object invoke = method.invoke(o, params); System.out.println("bb"); return invoke; } } package com.lbl.proxy; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * Created by liubenlong on 2017/2/4. */ public class MyTimeProxy implements MyInvocationHandler { @Override public Object invoke(Method method, Object o, Object ... params) throws InvocationTargetException, IllegalAccessException{ long begin = System.currentTimeMillis(); Object invoke = method.invoke(o, params); System.out.println(method.getName() + " 执行总共耗时:"+(System.currentTimeMillis() - begin) + "毫秒。"); return invoke; } }

    修改后的代理生成类:

    package com.lbl.proxy; import org.apache.commons.io.FileUtils; import javax.tools.JavaCompiler; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; import java.io.File; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.util.concurrent.atomic.AtomicLong; /** * */ public class Proxy2 { private static final String RT = "\r\n"; public static final AtomicLong proxyNum = new AtomicLong(0); /** * @param interfaceObj 参数接口 * @return * @throws Exception */ public static Object newProxyInstance(Class interfaceObj, Object o, MyInvocationHandler handler) throws Exception{ Method[] methods = interfaceObj.getMethods(); StringBuilder b = new StringBuilder(); for (Method method : methods) { int parameterCount = method.getParameterCount(); Class<?>[] parameterTypes = method.getParameterTypes(); StringBuilder paramStr = new StringBuilder(); StringBuilder paramClasses = new StringBuilder(); for(int i = 0 ; i < parameterCount ; i ++){ paramStr.append(parameterTypes[i].getName()).append(" ").append("arg").append(i).append(","); paramClasses.append(parameterTypes[i].getName()).append(".class,"); } b.append(" @Override" + RT ) .append(" public void "+method.getName()+"(" + paramStr.substring(0, paramStr.length()-1) + ") {" + RT)//这里我们假设所有方法都没有参数 .append("try {") .append(" Method method = HelloWorld.class.getMethod(\""+method.getName()+"\", "+paramClasses.substring(0, paramClasses.length()-1)+");") .append(" handler.invoke(method, obj, arg0);" + RT ) .append("} catch (Exception e) {e.printStackTrace();}") .append(" }" + RT) ; } long num = proxyNum.getAndIncrement(); //声明一段源码 String sourceCode = "package com.lbl.proxy;" + RT + "import java.lang.reflect.Method;" + RT + "/**" + RT + " * Created by liubenlong on 2017/2/4." + RT + "* 聚合模式实现静态代理" + RT + "*/" + RT + "public class $Proxy"+num+" implements " + interfaceObj.getName() + " {" + RT + " private "+interfaceObj.getName()+" obj;" + RT + " private MyInvocationHandler handler;" + RT + " public $Proxy" + num +"("+interfaceObj.getName()+" obj, MyInvocationHandler handler){" + RT + " this.obj = obj;" + RT + " this.handler = handler;" + RT + " }" + RT + b.toString() + "}"; String filename = System.getProperty("user.dir") + "/src/main/java/com/lbl/proxy/$Proxy" + num + ".java"; File file = new File(filename); //使用org.apache.commons.io.FileUtils.writeStringToFile()将源码写入磁盘 //编写到处,可以运行一下程序,可以在当前目录中看到生成的.java文件 //并不是一定要生成文件,如果可以直接生成二进制代码load到JVM中,则不必要那么麻烦生成文件了 FileUtils.writeStringToFile(file,sourceCode); //获得当前系统中的编译器 JavaCompiler complier = ToolProvider.getSystemJavaCompiler(); System.out.println(complier.getClass().getName()); //获得文件管理者 StandardJavaFileManager fileMgr = complier.getStandardFileManager(null, null, null); Iterable its = fileMgr.getJavaFileObjects(filename); //编译任务 JavaCompiler.CompilationTask task = complier.getTask(null, fileMgr, null, null, null, its); //开始编译,执行完可在当前目录下看到.class文件 task.call(); fileMgr.close(); //load到内存 URL[] urls = new URL[]{new URL("file:/" + System.getProperty("user.dir") + "/src")}; URLClassLoader urlClassLoader = new URLClassLoader(urls); Class cls = urlClassLoader.loadClass("com.lbl.proxy.$Proxy" + num); //生成代理类对象 Constructor ct = cls.getConstructor(interfaceObj, MyInvocationHandler.class);//找一个参数类型是HelloWorldImpl.class的构造方法 return ct.newInstance(o, handler); } } class test2{ public static void main(String[] args) throws Exception { HelloWorld proxyObj = (HelloWorld) Proxy2.newProxyInstance(HelloWorld.class, new HelloWorldImpl(), new MyTimeProxy()); proxyObj.sayHello("zhangsan"); } }

    运行一下,我们查看一下输出结果:

    com.sun.tools.javac.api.JavacTool 我是RealSubject。 Hello zhangsan sayHello 执行总共耗时:1502毫秒。

    然后看一下代码生成的代理类

    import java.lang.reflect.Method; /** * Created by liubenlong on 2017/2/4. * 聚合模式实现静态代理 */ public class $Proxy0 implements com.lbl.proxy.HelloWorld { private com.lbl.proxy.HelloWorld obj; private MyInvocationHandler handler; public $Proxy0(com.lbl.proxy.HelloWorld obj, MyInvocationHandler handler){ this.obj = obj; this.handler = handler; } @Override public void sayHello(java.lang.String arg0) { try { //最关键的两行代码 Method method = HelloWorld.class.getMethod("sayHello", java.lang.String.class); handler.invoke(method, obj, arg0); } catch (Exception e) { e.printStackTrace(); } } }

    到此为止,代理类可以永远不需要改变了。

    每当需要新的代理需求,则编写一个实现了MyInvocationHandler的类就行,然后调用也相当的方便,只需要修改对应的这一行代码的参数即可:HelloWorld proxyObj = (HelloWorld) Proxy2.newProxyInstance(HelloWorld.class, new HelloWorldImpl(), new MyTimeProxy());

    转载请注明原文地址: https://ju.6miu.com/read-658465.html

    最新回复(0)