动态代理

根据代理类的生成时间不同可以将代理分为静态代理动态代理两种。

静态代理

先看下静态代理的例子。以链家中介代理租赁买房的例子。

/**
 * 业务接口
 * Created by jiapeng on 2017/12/26.
 */
public interface Handle {
    //租赁
    public void rent();
    //购买
    public void purchase();
}
/**
 * 业务处理类,哈登登哥
 * Created by jiapeng on 2017/12/26.
 */
public class Harden implements Handle{

    public void rent() {
        System.out.println("哈登租赁了xx房屋");
    }
    public void purchase() {
        System.out.println("哈登购买了xx房屋");
    }
}
/**
 * 代理类链接中介
 * 哈登登哥太忙,没空去跑腿办手续,跟卖家沟通细节
 * Created by jiapeng on 2017/12/26.
 */
public class HomeLinkProxy implements Handle{

    private Harden harden;

    public HomeLinkProxy(Harden harden){
        this.harden = harden;
    }

    public void rent() {
        preAction();
        harden.rent();
        afterAction();
    }

    public void purchase() {
        preAction();
        harden.purchase();
        afterAction();
    }

    /**
     * 链家代理,之前的动作
     */
    private void preAction(){
        System.out.println("链家收定金,跑腿,帮助准备材料and so on");
    }

    /**
     * 链家代理,之后的动作
     */
    private void afterAction(){
        System.out.println("链接收尾款,跑腿,帮助确保交接房屋顺利and so on");
    }
}
/**
 * 客户端调用类
 * Created by jiapeng on 2017/12/26.
 */
public class Client {

    public static void main(String[] args) {
        Handle proxy = new HomeLinkProxy(new Harden());
        proxy.purchase();
    }
}

执行客户端调用类结果:


动态代理_第1张图片
image.png

以上例子就能说明,代理类的作用,跟现实生活中的场景一样。代理类在实际业务处理(哈登买房)的前后加上了,通用的业务操作。
这其实就是AOP的思想,只不过spring AOP是用动态代理实现的。后边继续体会下。

可以看出,所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。

动态代理

动态代理从实现上,可以分为:

  1. JDK动态代理,jdk自带的不需要依赖别的包,但是功能有限制。
  2. 使用第三方字节码工具包实现。
  • ASM:是一个java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能。

ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or dynamically generate classes, directly in binary form.

ASM是低级的字节码生成工具,使用ASM已经近乎在于使用Javabytecode编程,对开发人员要求较高,也是性能最好的一种动态代理生成工具。但ASM的使用是在过于繁琐,而且性能也没有数量级的提升。所以有了高级的字节码生成库CGLIB和Javassist。

  • cglib,第三方包,高级的字节码生成库,使用会相对简单些,动态生成字节码文件。底层依赖asm包,所以使用需要依赖引入asm包。spring aop的实现提供jdk动态代理和cglib两种选择。

  • javassist,第三方包,高级的字节码生成库,使用会相对简单些,是由日本东京工业大学的数学和计算机科学系的xxx牛人所创建的。目前它已加入了开放源代码JBoss项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。

JDK动态代理

代理类:

/**
 * 使用jdk实现的动态代理
 * 要绑定接口(这是一个限制缺陷,cglib弥补了这一缺陷)
 * Created by jiapeng on 2017/12/27.
 */
public class JdkDynamicProxy implements InvocationHandler {

    private Object target;

    /**
     * 绑定委托对象并返回一个代理类
     * @param target
     * @return
     */
    public Object bind(Object target) {
        this.target = target;
        //取得代理对象
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(), this);
    }


    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        preAction();
        result = method.invoke(target, args);
        afterAction();
        return result;
    }

    /**
     * 链家(jdk动态)代理,之前的动作
     */
    private void preAction() {
        System.out.println("链家收定金,跑腿,帮助准备材料and so on");
    }

    /**
     * 链家(jdk动态)代理,之后的动作
     */
    private void afterAction() {
        System.out.println("链接收尾款,跑腿,帮助确保交接房屋顺利and so on");
    }

}

客户端调用

/**
 * 客户端调用类
 * Created by jiapeng on 2017/12/26.
 */
public class Client {
    
    public static void main(String[] args) {
        //静态代理
//        Handle proxy = new HomeLinkProxy(new Harden());
//        proxy.purchase();
        //jdk动态代理
        //看源码可以知道,设置这个属性,会把代理类写到磁盘上
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        JdkDynamicProxy jdkDynamicProxy = new JdkDynamicProxy();
        Handle handle = (Handle) jdkDynamicProxy.bind(new Harden());
        handle.purchase();
    }
}

执行结果跟上边的静态代理类一样。
但是在com/sun/proxy的路径下,动态生成了一个代理类$Proxy0。反编译字节码看下这个代理类的代码,可以看到代理类继承反射包的Proxy类,实现自定义的业务接口Handle。

/**
 * jdk动态代理生成的代理类
 * Created by jiapeng on 2017/12/27.
 */
public final class $Proxy0 extends Proxy implements Handle {

    private static Method m3;
    private static Method m4;
    static {
        try {
            m3 = Class.forName("com.lxqn.jiapeng.proxy.Handle").getMethod("rent", new Class[0]);
            m4 = Class.forName("com.lxqn.jiapeng.proxy.Handle").getMethod("purchase", new Class[0]);
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }

    public $Proxy0(InvocationHandler var1) throws Throwable{
        super(var1);
    }

    public final void rent(){
        try {
            super.h.invoke(this, m3, null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void purchase(){
        try {
            super.h.invoke(this, m4, null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
}

上边代码的原理,关键是反射包下的Proxy.newProxyInstance方法和Method.invoke方法。
翻下源码,加深下了解。
我们都知道,动态代理最关键的部分,肯定是运行时字节码的生成。所以,jdk肯定是调用的某个类的 可以生成字节码文件api。

  1. 对这组接口进行一定程度的安全检查
  2. 从 loaderToCache 映射表中获取以类装载器对象为关键字所对应的缓存表
  3. 动态创建代理类的类对象
  4. 根据结果更新缓存表

代码过程分这四步,其实关键的就是第三部,其余的是考虑安全问题,本地缓存优化,多线程请求考虑。
字节码的生成在Proxy类的ProxyClassFactory的内部类apply方法里(jdk1.8)。截取其中关键的一行代码:

        /*
         * Generate the specified proxy class.
         */
         byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
             proxyName, interfaces, accessFlags);
         try {
             return defineClass0(loader, proxyName,
                                 proxyClassFile, 0, proxyClassFile.length);
         } catch (ClassFormatError e) {
         }

这里使用了神秘的ProxyGenerator类,这个类是jdk里sun.misc包下提供的一个类,在早的jdk版本里,这个类的源码是没有提供的。1.8是能看到,粗略的看一眼。能看到saveGeneratedFiles变量是sun.misc.ProxyGenerator.saveGeneratedFiles控制的。代码里充斥着写文件流,byte字节码的定义。

    public static byte[] generateProxyClass(final String var0, Class[] var1, int var2) {
        ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);
        final byte[] var4 = var3.generateClassFile();
        if(saveGeneratedFiles) {
            AccessController.doPrivileged(new PrivilegedAction() {
                public Void run() {
                    try {
                        int var1 = var0.lastIndexOf(46);
                        Path var2;
                        if(var1 > 0) {
                            Path var3 = Paths.get(var0.substring(0, var1).replace('.', File.separatorChar), new String[0]);
                            Files.createDirectories(var3, new FileAttribute[0]);
                            var2 = var3.resolve(var0.substring(var1 + 1, var0.length()) + ".class");
                        } else {
                            var2 = Paths.get(var0 + ".class", new String[0]);
                        }

                        Files.write(var2, var4, new OpenOption[0]);
                        return null;
                    } catch (IOException var4x) {
                        throw new InternalError("I/O exception saving generated file: " + var4x);
                    }
                }
            });
        }

        return var4;
    }

defineClass0方法也是一个native的方法。

以上贴了这么多,有个关键点结论

JDK提供了sun.misc.ProxyGenerator.generateProxyClass(String proxyName,class[] interfaces) 底层方法来产生动态代理类的字节码。这就是jdk动态代理,动态生成字节码的底层依赖。

so,我们可以把这部分抽出来,自定义一个工具类,自己将生成的动态代理类保存到硬盘中。实现动态代理。

/**
 * 工具类
 * 使用ProxyGenerator类创建动态代理
 * Created by jiapeng on 2017/12/27.
 */
public class ProxyUtils {

    /**
     * 生成动态代理类方法
     * @param clazz 需要生成动态代理类的类
     * @param proxyName 为动态生成的代理类的名称
     */
    public static void generateClassFile(Class clazz, String proxyName) {
        //根据类信息和提供的代理类名称,生成字节码
        byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, clazz.getInterfaces());
        String paths = clazz.getResource(".").getPath();
        System.out.println(paths);
        FileOutputStream out = null;

        try {
            //保留到硬盘中
            out = new FileOutputStream(paths + proxyName + ".class");
            out.write(classFile);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
ProxyUtils.generateClassFile(new Harden().getClass(),"DynamicProxyFromUtil");

客户端执行工具方法,会在同级目录生成DynamicProxyFromUtil.class字节码动态代理,比较了下,竟然完全一样。


动态代理_第2张图片
image.png

问题:如果生成的动态代理字节码完全一样的话,还用反射包的Proxy干什么。有区别吗?

cglib动态代理

cglib动态代理的使用需要引入cglib和asm的依赖jar包。

/**
 * cglib实现的动态代理类
 * Created by jiapeng on 2017/12/27.
 */
public class CglibDynamicProxy implements MethodInterceptor {

    private Object target;

    public Object getProxyInstance(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.target.getClass());
        // call back method
        enhancer.setCallback(this);
        // create proxy instance
        return enhancer.create();  
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        preAction();
        Object result = methodProxy.invokeSuper(o, objects);
        afterAction();
        return result;
    }

    /**
     * 链家(cglib动态)代理,之前的动作
     */
    private void preAction() {
        System.out.println("链家收定金,跑腿,帮助准备材料and so on");
    }

    /**
     * 链家(cglib动态)代理,之后的动作
     */
    private void afterAction() {
        System.out.println("链接收尾款,跑腿,帮助确保交接房屋顺利and so on");
    }
}

客户端代码调用

/**
 * cglib动态代理客户端调用类
 * Created by jiapeng on 2017/12/26.
 */
public class Client {

    public static void main(String[] args) {

        //设置代理类字节码生成到指定位置  
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, ".");
        CglibDynamicProxy proxy = new CglibDynamicProxy();
        Handle handle = (Handle) proxy.getProxyInstance(new Harden());
        handle.purchase();
    }
}

执行结果跟之前的例子一样。可以看出来,跟jdk动态代理的使用方式很像。cglib使用的是asm底层字节码框架,动态的生成字节码代理类。

参考文章:
https://www.ibm.com/developerworks/cn/java/j-lo-proxy1/index.html
http://blog.csdn.net/dreamrealised/article/details/12885739

你可能感兴趣的:(动态代理)