动态代理技术详解

虽然对于Spring的基本思想Aop是基于动态代理和CGlib这一点很早就有所认识,但是什么是动态代理却不甚清楚。为了对Spring加深理解,我觉得好好学习一下java的动态代理是非常有必要的。

静态代理

在学习动态代理之前我先花一点时间了解一下静态代理,从静态代理出发了解代理到底是怎么一回事,以及了解静态代理的局限性,进而明白为什么要发展及使用动态代理技术。相信使用过Spring框架的同学都知道Spring利用Aop完成声明式事务管理以及其他的代理增强,也就是在方法执行前后加上一些譬如时间、日志、权限控制等。假如现在我们从比较复杂的Spring Aop中跳出来,那么,有什么简单的方法能够对我们的方法进行增强呢?

继承实现

最简单的方法就是继承,子类继承父类并重写父类的方法,在重写的过程总就可以对原有的方法进行增强。下面代码就是这种代理增强思想的实现。

package cn.proxy;

public class Tank implements Moveable{

    @Override
    public void move() {
        System.out.println("tank moveing...");        
    }

}

// son
package cn.proxy;

public class LogProxyTank extends Tank {
    
    public void move() {
        System.out.println("tank start...");
        super.move();
        System.out.println("tank stop...");
    }

}

可以看见,通过重写父类方法可以非常方便实现代理增强,但是就一个日志功能的代理增强,假如涉及到100个类呢?那就要为100个类实现子类。这还不算麻烦,假如涉及到日志、时间、权限等多个功能的增强的时候,先时间后日志和先日志后时间可不是一回事,那么就要分别实现两个代理的子类。想一想这其中涉及到许多增强的功能和许多被代理类时,就会造成代理类爆炸。显然这种方式是很不灵活的。

聚合实现

被代理类和代理类实现同一个接口,同时代理类不再与被代理类存在继承关系,而是代理类包含一个被代理类类型的成员变量。

package cn.proxy;

public class LogProxy implements Moveable {
    
    private Moveable m;
    
    public LogProxy(Moveable m) {
        super();
        this.m = m;
    }

    @Override
    public void move() throws Exception {
        System.out.println("moveable start...");
        m.move();
        System.out.println("moveable stop...");
    }
    
}

可以看见,相较于继承方式,聚合的方式实现代理增强,通过传入不同的被代理类,可以实现对不同的被代理进行增强,但是这种方式实现不同的增强还是需要写不同的代理类,灵活性上还不是很完美。

动态代理

我们都知道,一个类要经过编写、编译、加载进JVM最终才能进行实例化。通常这些工作是分开进行的。那么有没有可能将这些过程封装到一个方法里面呢?答案是可以的。现在大致讲一下步骤:1. 首先将可以将源码保存成字符串,并将增强的代码加进去,然后通过Java IO技术将其写入到一个java文件中2. 使用JavaCompiler进行编译生成.class的二进制文件3. 使用一个类加载器(这里使用URLClassLoader)将二进制文件加载进内存4. 实例化代理对象基本思路就是这样,现在要实现灵活的java动态代理,问题就是如何动态的确定对什么类进行代理,进行怎样的代理增强。关键就在步骤1,源码字符串不能写死,而应该动态生成。那么这个方法的参数就需要有被代理对象和增强的逻辑。被代理类很容易理解,就是要通过这个被代理类知道要对哪些方法进行代理增强,通过反射就可以获取被代理类非所有方法。还有一个重要的接口是InvocationHandler,这个接口有一个方法invoke,在这个方法中可以定义代理逻辑以及调用被代理类的实例对象的欲代理方法来完成主要的方法逻辑,因为代理类是不能完成被代理类的方法逻辑的。就像歌星(被代理类)的经纪人(代理类)可以帮助歌星去接演出、安排行程,但是不能代替歌星去唱歌一样。为了进一步了解Java动态代理,可以对JDK中的Proxy类进行一个简单的模拟,代码如下:

package cn.proxy;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

public class Proxy {
    
    public static Object newProxyInstance(Class interfacee, InvocationHandler handler) throws Exception {
        
        String rt = "\r\n";
        // 源码
        String methodStr = "";
        Method[] methods = interfacee.getMethods();
        for (Method m : methods) {
            methodStr +=
                    rt +
            "    @Override" + rt +        
            "    public void " + m.getName() + "() throws Exception {" + rt +
            "        Method md = " + interfacee.getName()+ ".class.getMethod(\"" + m.getName() + "\");" + rt +
            "        handler.invoke(this, md);" + rt +
            "    }" + rt;
        }
        
        String src = 
        "package cn.proxy;" + rt + rt +
        
        "import java.lang.reflect.Method;" + rt +
        "import cn.proxy.InvocationHandler;" + rt + rt +

        "public class TankTimeProxy implements " +  interfacee.getName() + "{" + rt +
            
        "    private InvocationHandler handler;" + rt + rt +
            
        "    public TankTimeProxy(InvocationHandler handler) {" + rt +
        "        super();" + rt +
        "        this.handler = handler;" + rt +
        "    }" + rt +
        
        methodStr +

        "}";
        
        // 生成一个java源码
        String fileName = System.getProperty("user.dir") + "/src/cn/proxy/TankTimeProxy.java";
        File file = new File(fileName);
        FileWriter fileWriter = new FileWriter(file);
        fileWriter.write(src);
        fileWriter.flush();
        fileWriter.close();
        
        // 使用jdk编译api动态编译
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjects(fileName);
        CompilationTask task = compiler.getTask(null, fileManager, null, null, null, units);
        task.call();
        fileManager.close();
        
        // 将二进制文件加载进入内存
        URL[] urls = new URL[] {new URL("file:/" + System.getProperty("user.dir") + "src")};
        URLClassLoader cl = new URLClassLoader(urls);
        Class<?> clazz = cl.loadClass("cn.proxy.TankTimeProxy");
        System.out.println(clazz);
        
        // 实例化新对象
        Constructor<?> constructor = clazz.getConstructor(InvocationHandler.class);
        Object m = constructor.newInstance(handler);
        
        return m;
    }
}

仔细分析一下上面的代码可以看出,基本思路就是上面所述的四步走。通过反射技术来动态解析传入的被代理类,获取欲代理的方法,而每个方法的具体实现交由InvocationHandler的具体实现类来完成,其invoke方法完成代理增强并调用被代理类的相应的被代理方法。
为了对InvocationHandler有一个直观的了解,写一个简单的InvocationHandler接口以及实现类,具体代码如下:

package cn.proxy;

import java.lang.reflect.Method;

public interface InvocationHandler {
    
    void invoke(Object o, Method m);

}
package cn.proxy;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class TimeHandler implements InvocationHandler {
    
    // 被代理类
    private Tank t;
    
    public TimeHandler(Tank t) {
        this.t = t;
    }

    @Override
    public void invoke(Object o, Method m) {
        long start = System.currentTimeMillis();
        System.out.println("start time: " + start);
        try {
            m.invoke(t, new Object[]{});
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("end time: " + end);
    }

}

InvocationHandler的实现类中有一个要注意:要有一个被代理类类型的成员变量,要通过这个变量才能过调用被代理类的相应方法来完成主要的方法功能。通过 Proxy.newProxyInstance 创建的代理对象是在jvm运行时动态生成的一个对象,它并不是InvocationHandler类型,也不是定义的那接口的类型,而是在运行是动态生成的一个对象。

总结

简单理解,代理模式就是在一个方法执行前后加入代理增强的代码。我们不断探索新的方式,就是要实现更好的灵活性,可以对任意的被代理类实现任意的代理增强。动态代理技术通过传入被代理类(这就可以任意传入),反射解析这个类来获得欲代理的方法。通过传入InvocationHandler的实现类来实现方法的代理增强,可以根据传入的实现类的不同来实现任意的代理,而且这个实现类可以做到复用,就像机械零件一样,自由组装实现不同形式的代理组合。Java具体的代理类Proxy与本文中模仿的代理类有些许不同,包括可以传入方法参数实现对有参方法的代理,代理类被命名为$proxy1等等。但这些是细节问题,主要的思想还是一样的。

===========================================================================================================================本文只是我现阶段的学习心得总结而成,内容可能不够深入,由于水平所限,不保证所有内容正确,欢迎有同学在评论中指正,万分感谢!保证每一个字的原创性!作为一个程序员,我所能做的就是每一天都在进步,面对技术保持一颗赤子之心,这是我人生现阶段全部的追求。"Stay hungry, stay foolish"!===========================================================================================================================