美文网首页
动态代理

动态代理

作者: 联想桥南 | 来源:发表于2017-12-27 22:56 被阅读0次

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

    静态代理

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

    /**
     * 业务接口
     * 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();
        }
    }
    

    执行客户端调用类结果:


    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字节码动态代理,比较了下,竟然完全一样。


    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

    相关文章

      网友评论

          本文标题:动态代理

          本文链接:https://www.haomeiwen.com/subject/uhnoextx.html