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

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

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

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

本质上,动态代理是在程序运行过程中创建生成一个类并且将它加载到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
模拟JDK动态代理 ; 自己动手模拟实现java动态代理_第1张图片

输出结果:

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,里面只有一个方法,该方法用于对被代理对象的调用。

类图如下
模拟JDK动态代理 ; 自己动手模拟实现java动态代理_第2张图片

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());

你可能感兴趣的:(设计模式)