美文网首页
动态代理学习记录

动态代理学习记录

作者: 寻找傅里叶 | 来源:发表于2021-06-22 19:54 被阅读0次

    JDK动态代理

    使用

    1. 首先定义一个接口类
    // Person.java
    package com.may.learning;
    
    public interface Person {
        void check();
        void checkout();
    }
    
    1. 再定义实现该接口的类,用以生成需要被代理的实例对象
    // Employee.java
    package com.may.learning;
    
    public class Employee implements Person{
        private String name;
    
        public Employee(String name) {
            this.name = name;
        }
        public void check() {
            System.out.printf("%s 今天也要好好上班!\n", name);
        }
    
        public void checkout() {
            System.out.printf("%s 明天见\n", name);
        }
    }
    
    1. 使用InvocationHandler定义invoke方法,用以替代被代理对象执行时的方法,并且使用newProxyInstance创建一个代理类.
    // Welcome.java
    package com.may.learning;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    public class Welcome {
    
        static class LogHandler implements InvocationHandler {
            Object obj;
            public LogHandler(Object obj) {
                this.obj = obj;
            }
    
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("准备打卡");
                method.invoke(obj, args);
                System.out.println("打卡完成");
                return null;
            }
        }
    
        public static void main(String[] args) {
            Person p = new Employee("littlemay");
            LogHandler logHandler = new LogHandler(p);
            p = (Person) Proxy.newProxyInstance(p.getClass().getClassLoader(), p.getClass().getInterfaces(), logHandler);
            p.check();
            p.checkout();
        }
    }
    

    运行结果:

    准备打卡
    littlemay 今天也要好好上班!
    打卡完成
    准备打卡
    littlemay 明天见
    打卡完成
    

    说明

    1. 进行调用的对象p事实上是根据Person接口的字节码临时构造的一个对象.通过Proxy.newProxyInstance方法进行构造,接收三个参数:
    • loader: 代理类的ClassLoader
    • interfaces:代理类需要实现的接口
    • handler:调用处理器实例,在这里是logHandler
    1. InvocationHandlerinvoke方法接收三个参数:
    • proxy: 代理后的对象,在这里是p
    • method: 被代理对象需要代理的方法
    • args: method执行时需要的参数

    在实现InvocationHandler中,传入的proxy对象,和方法调用时的obj,需要区分开,并不是一个东西:
    虽然proxy看似无用,但是却是可以实现链式调用进行返回的.参照:Understanding “proxy” arguments of the invoke method of java.lang.reflect.InvocationHandler.
    obj是需要传入的被代理对象实例.
    比如在此基础上做一个边上班边增加工资的无聊功能:

    // Person.java
    package com.may.learning;
    
    public interface Person {
        void check();
        void checkout();
        // new
        Person addSalary(double money);
        double getSalary();
    }
    
    // Employee.java
    package com.may.learning;
    
    
    public class Employee implements Person{
        private String name;
        private Double money;
    
        public Employee(String name, Double money) {
            this.name = name;
            this.money = money;
        }
        public void check() {
            System.out.printf("%s 今天也要好好上班!\n", name);
        }
    
        public void checkout() {
            System.out.printf("%s 明天见\n", name);
        }
        // new
        public Person addSalary(double money){
            this.money += money;
            return this;
        }
    
        public double getSalary() {
            return money;
        }
    }
    
    // Welcome.java
    package com.may.learning;
    
    import java.io.IOException;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    public class Welcome {
    
        public static void main(String[] args) throws IOException {
            Person p = new Employee("littlemay", 10000.0);
            LogHandler logHandler = new LogHandler(p);
            p = (Person) Proxy
                .newProxyInstance(p.getClass().getClassLoader(), p.getClass().getInterfaces(),
                    logHandler);
            p.check();
            System.out.println("工资余额: "+ p.getSalary());
            // 链式调用
            p.addSalary(100).addSalary(100).addSalary(1000);
            p.checkout();
            System.out.println("工资余额: "+ p.getSalary());
        }
    
        static class LogHandler implements InvocationHandler {
    
            Object obj;
    
            public LogHandler(Object obj) {
                this.obj = obj;
            }
    
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if ("check".equals(method.getName())) {
                    System.out.println("准备打卡");
                    method.invoke(obj, args);
                    System.out.println("打卡成功");
                } else if ("addSalary".equals(method.getName())) {
                    method.invoke(obj, args);
                    System.out.println("工资增加了");
                } else{
                    return method.invoke(obj, args);
                }
                return proxy;
            }
        }
    }
    

    输出结果:

    准备打卡
    littlemay 今天也要好好上班!
    打卡成功
    工资余额: 10000.0
    工资增加了
    工资增加了
    工资增加了
    littlemay 明天见
    工资余额: 11200.0
    

    为什么说JDK代理只能基于接口实现呢

    newProxyInstance是生成代理对象的class文件,更改代码获得生成的class文件:

        public static void main(String[] args) throws IOException {
            Person p = new Employee("littlemay");
            LogHandler logHandler = new LogHandler(p);
            p = (Person) Proxy.newProxyInstance(p.getClass().getClassLoader(), p.getClass().getInterfaces(), logHandler);
            // 获取字节码
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass("com.may.learning.$Proxy0", p.getClass().getInterfaces(),
                Modifier.PUBLIC);
            File file = new File("$Proxy0.class");
            OutputStream outputStream = new FileOutputStream(file);
            outputStream.write(proxyClassFile);
            // end
            p.check();
            p.checkout();
        }
    

    查看字节码:

    // $Proxy0.class
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by Fernflower decompiler)
    //
    
    package com.may.learning;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.lang.reflect.UndeclaredThrowableException;
    
    public class $Proxy0 extends Proxy implements Person {
        private static Method m1;
        private static Method m2;
        private static Method m4;
        private static Method m3;
        private static Method m0;
    
        public $Proxy0(InvocationHandler var1) throws  {
            super(var1);
        }
    
        public final boolean equals(Object var1) throws  {
            try {
                return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
            } catch (RuntimeException | Error var3) {
                throw var3;
            } catch (Throwable var4) {
                throw new UndeclaredThrowableException(var4);
            }
        }
    
        public final String toString() throws  {
            try {
                return (String)super.h.invoke(this, m2, (Object[])null);
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
    
        public final void check() throws  {
            try {
                super.h.invoke(this, m4, (Object[])null);
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
    
        public final void checkout() throws  {
            try {
                super.h.invoke(this, m3, (Object[])null);
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
    
        public final int hashCode() throws  {
            try {
                return (Integer)super.h.invoke(this, m0, (Object[])null);
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
    
        static {
            try {
                m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
                m2 = Class.forName("java.lang.Object").getMethod("toString");
                m4 = Class.forName("com.may.learning.Person").getMethod("check");
                m3 = Class.forName("com.may.learning.Person").getMethod("checkout");
                m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            } catch (NoSuchMethodException var2) {
                throw new NoSuchMethodError(var2.getMessage());
            } catch (ClassNotFoundException var3) {
                throw new NoClassDefFoundError(var3.getMessage());
            }
        }
    }
    

    可以发现$Proxy0已经继承了Proxy类,因为java不支持多继承,所以只能采用实现Person接口的方式来实现.

    参考资料:
    你真的完全了解Java动态代理吗?看这篇就够了
    Java动态代理
    为什么JDK的动态代理要基于接口实现而不能基于继承实现?
    Java中InvocationHandler接口中第一个参数proxy详解

    cglib动态代理

    由于JDK动态代理只能基于接口实现,如果要对类进行代理,那么可以使用cglib动态代理.它是一个基于ASM的字节码生成库,允许在运行时对字节码进行修改和动态生成。

    使用

    1. 添加依赖
            <dependency>
                <groupId>cglib</groupId>
                <artifactId>cglib</artifactId>
                <version>2.2.2</version>
            </dependency>
    
    1. 编写一个类
    // UserDao.java
    package com.may.learning;
    
    public class UserDao {
    
        public void update() {
            System.out.println("update...");
        }
    
        public void select() {
            System.out.println("select...");
        }
    
        public final void show(){
            System.out.println("show");
        }
    }
    
    1. 编写代理,intercept有四个参数,obj表示代理对象,method表示目标类中的方法,args表示方法参数,proxy表示代理方法的MethodProxy对象
    // UserDaoInterceptor.java
    package com.may.learning;
    
    import java.lang.reflect.Method;
    import org.springframework.cglib.proxy.MethodInterceptor;
    import org.springframework.cglib.proxy.MethodProxy;
    
    public class UserDaoInterceptor implements MethodInterceptor {
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            // System.out.println(obj.getClass().getName());
            System.out.println("transaction open");
            Object o = proxy.invokeSuper(obj, args);
            System.out.println("transaction close");
            return o;
        }
    }
    

    需要调用invokeSuper方法,如果调用invoke会死循环导致栈溢出.原因在于:

    • invoke最终执行的是fci.f1.invoke(fci.i1, obj, args);
    • invokeSuper最终执行的是fci.f2.invoke(fci.i2, obj, args)
      至于f1,f2分别代表什么,需要查看FastClassInfo.(FastClass机制就是对一个类的方法建立索引,通过索引来直接调用相应的方法.)
        private static class FastClassInfo {
            FastClass f1;  // 被代理对象的fastclass
            FastClass f2;  // 代理后对象的fastclass
            int i1; // 被代理方法在f1中的索引
            int i2; // 代理后方法在f2中的索引
    }
    

    因此如果使用invoke,最终还是调用被代理对象的被代理方法,仍然会进入到intercept中,如此循环下去.

    1. 利用enhancer.create()获取代理对象,进行调用
    package com.may.learning;
    
    import org.springframework.cglib.core.DebuggingClassWriter;
    import org.springframework.cglib.proxy.Enhancer;
    
    public class TestCglib {
    
        public static void main(String[] args) {
            System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"/home/may/learning/java");
            Enhancer enhancer = new Enhancer();
            // 设置要代理的类
            enhancer.setSuperclass(UserDao.class);
             //设置回调
            enhancer.setCallback(new UserDaoInterceptor());
             // 生成代理对象
            UserDao userDao = (UserDao)enhancer.create();
            userDao.select();
            userDao.update();
            userDao.show();
        }
    }
    

    可以利用System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"/home/may/learning/java");查看生成的字节码

    说明

    所有非final的方法都会转发到UserDaoInterceptor.invoke()方法中.所以show方法不会有事务开启关闭的语句输出.调用结果为:

    transaction open
    select...
    transaction close
    transaction open
    update...
    transaction close
    show
    

    因为每一次打印都会调用toString方法,所以如果在invoke方法中执行打印obj,会导致stackoverflow,这在两种动态代理方法都会出现.并且打断点调试会输出更多的语句.

    查看生成的UserDao$$EnhancerByCGLIB$$8a7c533a.class文件,发现如果存在拦截器,会使用拦截器执行:

        public final void update() {
            MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
            if (var10000 == null) {
                CGLIB$BIND_CALLBACKS(this);
                var10000 = this.CGLIB$CALLBACK_0;
            }
    
            if (var10000 != null) {
                var10000.intercept(this, CGLIB$update$1$Method, CGLIB$emptyArgs, CGLIB$update$1$Proxy);
            } else {
                super.update();
            }
        }
    
    

    参考资料:
    Java Proxy和CGLIB动态代理原理
    cglib源码分析(四):cglib 动态代理原理分析
    CGLib动态代理

    相关文章

      网友评论

          本文标题:动态代理学习记录

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