​ 动态代理是java语言中常用的设计模式,java在1.3版本以后也提供了动态代理技术,允许开发者在运行期间创建接口的代理对象。 很多框架底层都使用了java的动态代理技术来实现的,比如大名鼎鼎的springAOP;这篇文章将带你一步一步揭开JDK动态代理技术的神秘面纱。

​ 我们先来定义一个接口:

package com.yanghui.study.proxy;
public interface IFlyable {
    int fly(int x,int y);
}

再来一个实现类:

package com.yanghui.study.proxy;
public class Plane implements IFlyable{
    @Override
    public int fly(int x, int y) {
        int result = x * x + y * y;
        try {
            Thread.sleep(new Random().nextInt(700));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return result;
    }
}

如果我们要统计一下这个fly方法的运行时间,该怎么做呢?很简单,可以修改源码在方法fly方法里面加上两句代码①、②,这样就打印出方法的运行时间了,如下:

//省略不必要代码......
public int fly(int x, int y) {
    long start = System.currentTimeMillis();//①记录开始时间
    int result = x * x + y * y;
    try {
        Thread.sleep(new Random().nextInt(700));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");//②结束时间减去开始时间
    return result;
}

但是如果我们没有这个方法的源码,这个类是别人写好打好jar包提供给我们用的,这时如果你还想统计下这个方法运行时间,又该怎么办呢?至少有两种方式可以来实现:

1、使用继承,写一个类继承Plane,重写fly方法,在调用父类的fly方法前后加上①②处的代码,这样就可以统计fly方法的执行时间了。

package com.yanghui.study.proxy;
public class PlaneTimerProxy1 extends Plane{
    @Override
    public int fly(int x, int y) {
        long start = System.currentTimeMillis();//①
        int result = super.fly(x, y);
        System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");//②
        return result;
    }
}

2、使用聚合的方式,写一个类PlaneTimerProxy2实现跟Plane一样的接口,并且持有IFlyable的引用,当调用fly方法时,实际调用的是IFlyable的fly方法,这样就可以在方法调用前后加上①②处的代码统计fly方法的执行的时间。

public class PlaneTimerProxy2 implements IFlyable{
    private IFlyable flyable;
    public PlaneTimerProxy2(IFlyable flyable) {
        this.flyable = flyable;
    }
    @Override
    public int fly(int x, int y) {
        long start = System.currentTimeMillis();//①
        int result = this.flyable.fly(x, y);
        System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");//②
        return result;
    }
}

这两种方式都可以实现,那么哪种方式更好呢?答案是聚合的方式更好,为什么呢?想象一下,如果我还想实现更多的功能,比如给fly方法执行前后加上日志,事务控制,权限控制,这时用继承的方式你会需要新建更多的类来实现,可能你会想,聚合的实现方式不也是要新建更多的类来实现吗?是的,但是如果我要你先记录日志再记录时间,有如果我要你先记录时间再记录日志,需要实现这样随意的组合的功能,继承就显得很麻烦了,而聚合的方式就会很灵活了。在思考下,如果想给不同类的100个方法记录下时间和日志,那么你想想看是不是要产生100个代理类呢?类的数量又在不停的膨胀了。如果我们能够为实现了某个接口的类动态生成代理类就好了?想法很好,先来新建一个类Proxy,提供一个方法newProxyInstance,这个方法可以为一个实现了IFlyable接口的类产生代理类,那么客户端调用就可以这样做:

package com.yanghui.study.proxy.custom;
public class Client {
    public static void main(String[] args) {
        IFlyable flyable = (IFlyable)Proxy.newProxyInstance();
        flyable.fly(1, 2);
    }
}

那么我们如何在newProxyInstance方法里面动态的生成一个代理类呢?为了模拟JDK的实现,先定义一个接口InvocationHandler:

package com.yanghui.study.proxy.custom;
import java.lang.reflect.Method;
public interface InvocationHandler {
    Object invoke(Object proxy,Method method,Object[] args)throws Throwable;
}

下面来个完整代码:

public class Proxy {
    private static final Map<String,byte[]> bytesMap = new HashMap<>();
    private static final AtomicInteger count = new AtomicInteger();
    public static Object newProxyInstance(Class<?> intaface,InvocationHandler handler) {
        //代码①处
        String rn = "\r\n";
        String className = "Proxy" + count.getAndIncrement();
        String str = "package com.yanghui.study.proxy.custom;" + rn +
                     "public class " + className + " implements " + intaface.getName() + "{" + rn +
                     "    private InvocationHandler handler;" + rn +
                     "    public " + className + "(InvocationHandler handler){" + rn + 
                     "        this.handler=handler;" + rn + 
                     "    }" + rn;
        
        String methodStr = "";
        for(Method m : intaface.getMethods()) {
            methodStr = methodStr + "    @Override" + rn +
             "    public " + m.getReturnType().getName() + " " + m.getName() + "(";
            String parameterStr = "";
            String psType = "";
            String pname = "";
            for(Parameter p : m.getParameters()) {
                parameterStr = parameterStr + p + ",";
                psType = psType + p.getType().getName() + ".class,";
                pname = pname + p.getName() + ",";
            }
            if(!parameterStr.equals("")) {
                parameterStr = parameterStr.substring(0, parameterStr.length() - 1);
            }
            parameterStr = parameterStr + "){" + rn + 
                     "        try{" + rn +
                     "            " + Method.class.getName() + " method = " + intaface.getName() + ".class.getDeclaredMethod(\"" + m.getName() + "\"";
            if(!psType.equals("")) {
                psType = psType.substring(0, psType.length() - 1);
                parameterStr = parameterStr + "," + psType + ");" + rn;
            }else {
                parameterStr = parameterStr + ");" + rn;
            }
            if(pname.length() > 0) {
                pname = pname.substring(0, pname.length() - 1);
            }
            String returnStr = "";
            if(!"void".equals(m.getReturnType().getName())) {
                returnStr = returnStr + "            return (" + m.getReturnType().getName() + ")";
            }
            parameterStr = parameterStr + 
                    returnStr + "this.handler.invoke(this,method," + (pname.length() == 0 ? "null" : "new Object[]{" + pname + "}") + ");" + rn +
             "        } catch (Throwable e) {" + rn +
             "        throw new RuntimeException(e);" + rn +
             "    }" + rn +
             "    }" + rn;
            methodStr = methodStr + parameterStr;
        }
        String endStr = "}";
        str = str + methodStr + endStr;
        String path = Thread.currentThread().getContextClassLoader().getResource("").getPath() + "com/yanghui/study/proxy/custom/";
        String fileStr = path + className + ".java";
        //代码②处
        //写入文件
        writeToFile(fileStr, str);
        //代码③处
        //动态编译
        String className1 = "com.yanghui.study.proxy.custom." + className;
        return compileToFileAndLoadclass(className1, fileStr, handler);
    }
    
    /**
     * 从源文件到字节码文件的编译方式
     * @param className
     * @param fileStr
     * @param handler
     * @return
     */
    private static Object compileToFileAndLoadclass(String className,String fileStr,InvocationHandler handler) {
        //获取系统Java编译器
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        //获取Java文件管理器
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        //定义要编译的源文件
        File file = new File(fileStr);
        //通过源文件获取到要编译的Java类源码迭代器,包括所有内部类,其中每个类都是一个 JavaFileObject,也被称为一个汇编单元
        Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(file);
        //生成编译任务
        JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
        //执行编译任务
        task.call();
        try {
            fileManager.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            Class<?> c = Thread.currentThread().getContextClassLoader().loadClass(className);
            Constructor<?> ct = c.getConstructor(InvocationHandler.class);
            Object object = ct.newInstance(handler);
            return object;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    private static void writeToFile(String file,String context) {
        FileWriter fw = null;
        try {
            fw = new FileWriter(new File(file));
            fw.write(context);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(fw != null) {
                try {
                    fw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

我来解释下上面代码的意思:

1、代码①处,根据传入的接口动态生成java代码的字符串,类名取名为Proxy+序号,该类实现了传入的接口,真正的方法调用将委托传入InvocationHandler的实现类来实现。

2、代码②处,将生成的java代码的字符串写入文件

3、代码③处,真正的核心,动态编译2步生成的java文件,再通过classLoader把编译生成的class文件加载进内存,然后反射创建实例。

接下来客户端就可以这样使用了:

public class Client {
    public static void main(String[] args) {
        Plane plane = new Plane();
        IFlyable flyable = (IFlyable)Proxy.newProxyInstance(IFlyable.class,new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                long start = System.currentTimeMillis();
                Object result = method.invoke(plane, args);
                System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");
                return result;
            }
        });
        System.out.println(flyable.fly(1, 2));
    }
}

到目前为止,我们实现的Proxy类可以为任何接口生成代理类了,是不是很神奇。当然我们这里只是模拟实现了JDk的动态代理,还有很多细节是没有考虑的,有兴趣的同学可以自己阅读JDK源码,相信您理解了其背后的原理后,看起来也不会太费力了。

扩展

在上面我们实现了动态生成java文件,动态编译java文件,需要把文件写入磁盘,也会在java源文件的目录生成编译后的.class文件,那么可以不可以只在内存中编译加载呢?答案是可以的,代码如下(方法是Proxy类下的方法):

/**
     * 从内存到内存的编译方式
     * @param className
     * @param code
     * @param handler
     * @return
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    private static Object compileMemoryToMemoryAndLoadClass(String className,String code,InvocationHandler handler) {
        if(bytesMap.get(className) != null) {
            return loadClass(className, bytesMap.get(className), handler);
        }
        //获取系统Java编译器
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        //获取Java文件管理器
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        ForwardingJavaFileManager fjf = new ForwardingJavaFileManager(fileManager) {
            @Override
            public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind,
                    FileObject sibling) throws IOException {
                if(kind == JavaFileObject.Kind.CLASS) {
                    return new SimpleJavaFileObject(URI.create(""), JavaFileObject.Kind.CLASS) {
                        public OutputStream openOutputStream() {
                            return new FilterOutputStream(new ByteArrayOutputStream()) {
                                public void close() throws IOException{
                                    out.close();
                                    ByteArrayOutputStream bos = (ByteArrayOutputStream) out;
                                    bytesMap.put(className, bos.toByteArray());
                                }
                            };
                        }
                    };
                }else{
                    return super.getJavaFileForOutput(location, className, kind, sibling);
                }
            }
        };
        
        SimpleJavaFileObject sourceJavaFileObject = new SimpleJavaFileObject(URI.create(className.replace('.', '/') + Kind.SOURCE.extension),JavaFileObject.Kind.SOURCE){
            @Override
            public CharBuffer getCharContent(boolean b) {
                return CharBuffer.wrap(code);
            }
        };
        //生成编译任务
        JavaCompiler.CompilationTask task = compiler.getTask(null, fjf, null, null, null, Arrays.asList(new JavaFileObject[] {sourceJavaFileObject}));
        //执行编译任务
        task.call();
        try {
            fileManager.close();
            fjf.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return loadClass(className, bytesMap.get(className), handler);
    }
    
    private static Object loadClass(String className,byte[] bytes,InvocationHandler handler) {
        try {
            Class<?> c = new MyClassLoader(bytes).loadClass(className);
            Constructor<?> ct = c.getConstructor(InvocationHandler.class);
            Object object = ct.newInstance(handler);
            return object;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

首先通过自己定义sourceJavaFileObject类来加载java格式的字符串,通过ForwardingJavaFileManager类来重新定义编译文件的输出行为,这里我直接写入内存,用一个map(bytesMap)来保存,key就是类名,value就是编译好的.class的二进制文件。


杨辉
92 声望18 粉丝

程序猿