美文网首页
Java高级主题(五)——动态代理

Java高级主题(五)——动态代理

作者: 路远处幽 | 来源:发表于2019-12-19 13:44 被阅读0次

    代理可以分为静态代理、动态代理,动态代理又可以分为 jvm的动态代理 和 cglib的动态代理。像spring框架的AOP的底层就使用了动态代理的技术。
    代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能.
    这里使用到编程中的一个思想:不要随意去修改别人已经写好的代码或者方法,如果需改修改,可以通过代理的方式来扩展该方法

    举个例子来说明代理的作用:假设我们想邀请一位明星,那么并不是直接连接明星,而是联系明星的经纪人,来达到同样的目的.明星就是一个目标对象,他只要负责活动中的节目,而其他琐碎的事情就交给他的代理人(经纪人)来解决.这就是代理思想在现实中的一个例子
    用图表示如下:


    image.png

    代理模式的关键点是:代理对象与目标对象.代理对象是对目标对象的扩展,并会调用目标对象

    一、静态代理

    静态代理在使用时,需要定义接口或者父类,被代理对象与代理对象一起实现相同的接口或者是继承相同父类.

    下面举个案例来解释:
    模拟保存动作,定义一个保存动作的接口:IUserDao.java,然后目标对象实现这个接口的方法UserDao.java,此时如果使用静态代理方式,就需要在代理对象(UserDaoProxy.java)中也实现IUserDao接口.调用的时候通过调用代理对象的方法来调用目标对象.
    需要注意的是,代理对象与目标对象要实现相同的接口,然后通过调用相同的方法来调用目标对象的方法
    代码示例:
    接口:IUserDao.java

    /**
     * 接口
     */
    public interface IUserDao {
    
        void save();
    }
    

    目标对象:UserDao.java

    /**
     * 接口实现
     * 目标对象
     */
    public class UserDao implements IUserDao {
        public void save() {
            System.out.println("----已经保存数据!----");
        }
    }
    

    代理对象:UserDaoProxy.java

    /**
     * 代理对象,静态代理
     */
    public class UserDaoProxy implements IUserDao{
        //接收保存目标对象
        private IUserDao target;
        public UserDaoProxy(IUserDao target){
            this.target=target;
        }
    
        public void save() {
            System.out.println("开始事务...");
            target.save();//执行目标对象的方法
            System.out.println("提交事务...");
        }
    }
    

    测试类:App.java

    /**
     * 测试类
     */
    public class App {
        public static void main(String[] args) {
            //目标对象
            UserDao target = new UserDao();
    
            //代理对象,把目标对象传给代理对象,建立代理关系
            UserDaoProxy proxy = new UserDaoProxy(target);
    
            proxy.save();//执行的是代理的方法
        }
    }
    

    静态代理总结:
    1.可以做到在不修改目标对象的功能前提下,对目标功能扩展.
    2.缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多.同时,一旦接口增加方法,目标对象与代理对象都要维护.

    如何解决静态代理中的缺点呢?答案是可以使用动态代理方式

    二、动态代理

    2.1、JDK接口动态代理

    2.1.1 动态代理特点

    1.代理对象,不需要实现接口
    2.代理对象的生成,是利用JDK的API,动态的在内存中构建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)
    3.动态代理也叫做:JDK代理,接口代理

    JDK中生成代理对象的API
    代理类所在包:java.lang.reflect.Proxy
    JDK实现代理只需要使用newProxyInstance方法,但是该方法需要接收三个参数,完整的写法是:
    static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h )
    注意该方法是在Proxy类中是静态方法,且接收的三个参数依次为:

    • ClassLoader loader,:指定当前目标对象使用类加载器,获取加载器的方法是固定的
    • Class<?>[] interfaces,:目标对象实现的接口的类型,使用泛型方式确认类型
    • InvocationHandler h:事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入
    2.1.2 代码示例

    接口类IUserDao.java以及接口实现类,目标对象UserDao是一样的,没有做修改.在这个基础上,增加一个代理工厂类(ProxyFactory.java),将代理类写在这个地方,然后在测试类(需要使用到代理的代码)中先建立目标对象和代理对象的联系,然后代用代理对象的中同名方法

    代理工厂类:ProxyFactory.java

    /**
     * 创建动态代理对象
     * 动态代理不需要实现接口,但是需要指定接口类型
     */
    public class ProxyFactory{
    
        //维护一个目标对象
        private Object target;
        public ProxyFactory(Object target){
            this.target=target;
        }
    
       //给目标对象生成代理对象
        public Object getProxyInstance(){
            return Proxy.newProxyInstance(
                    target.getClass().getClassLoader(),
                    target.getClass().getInterfaces(),
                    new InvocationHandler() {
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            System.out.println("开始事务2");
                            //执行目标对象方法
                            Object returnValue = method.invoke(target, args);
                            System.out.println("提交事务2");
                            return returnValue;
                        }
                    }
            );
        }
    
    }
    

    测试类:App.java

    /**
     * 测试类
     */
    public class App {
        public static void main(String[] args) {
            // 目标对象
            IUserDao target = new UserDao();
            // 【原始的类型 class cn.itcast.b_dynamic.UserDao】
            System.out.println(target.getClass());
    
            // 给目标对象,创建代理对象
            IUserDao proxy = (IUserDao) new ProxyFactory(target).getProxyInstance();
            // class $Proxy0   内存中动态生成的代理对象
            System.out.println(proxy.getClass());
    
            // 执行方法   【代理对象】
            proxy.save();
        }
    }
    

    总结:
    代理对象不需要实现接口(需实现InvocationHandler接口),但是目标对象一定要实现接口,否则不能用动态代理。

    2.1.3 原理分析

    首先是定义一个Person接口:

    /**
     * 创建Person接口
     * @author Gonjan
     */
    public interface Person {
        //上交班费
        void giveMoney();
    }
    

    创建需要被代理的实际类:

    public class Student implements Person {
        private String name;
        public Student(String name) {
            this.name = name;
        }
    
        @Override
        public void giveMoney() {
            try {
              //假设数钱花了一秒时间
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
           System.out.println(name + "上交班费50元");
        }
    }
    

    再定义一个检测方法执行时间的工具类,在任何方法执行前先调用start方法,执行后调用finsh方法,就可以计算出该方法的运行时间,这也是一个最简单的方法执行时间检测工具。

    public class MonitorUtil {
    
        private static ThreadLocal<Long> tl = new ThreadLocal<>();
    
        public static void start() {
            tl.set(System.currentTimeMillis());
        }
    
        //结束时打印耗时
        public static void finish(String methodName) {
            long finishTime = System.currentTimeMillis();
            System.out.println(methodName + "方法耗时" + (finishTime - tl.get()) + "ms");
        }
    }
    

    创建StuInvocationHandler类,实现InvocationHandler接口,这个类中持有一个被代理对象的实例target。InvocationHandler中有一个invoke方法,所有执行代理对象的方法都会被替换成执行invoke方法。

    再再invoke方法中执行被代理对象target的相应方法。当然,在代理过程中,我们在真正执行被代理对象的方法前加入自己其他处理。这也是Spring中的AOP实现的主要原理,这里还涉及到一个很重要的关于java反射方面的基础知识。

    public class StuInvocationHandler<T> implements InvocationHandler {
       //invocationHandler持有的被代理对象
        T target;
    
        public StuInvocationHandler(T target) {
           this.target = target;
        }
    
        /**
         * proxy:代表动态代理对象
         * method:代表正在执行的方法
         * args:代表调用目标方法时传入的实参
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("代理执行" +method.getName() + "方法");
         */   
            //代理过程中插入监测方法,计算该方法耗时
            MonitorUtil.start();
            Object result = method.invoke(target, args);
            MonitorUtil.finish(method.getName());
            return result;
        }
    }
    

    做完上面的工作后,我们就可以具体来创建动态代理对象了,上面简单介绍了如何创建动态代理对象,我们使用简化的方式创建动态代理对象:

    public class ProxyTest {
        public static void main(String[] args) {
    
            //创建一个实例对象,这个对象是被代理的对象
            Person zhangsan = new Student("张三");
    
            //创建一个与代理对象相关联的InvocationHandler
            InvocationHandler stuHandler = new StuInvocationHandler<Person>(zhangsan);
    
            //创建一个代理对象stuProxy来代理zhangsan,代理对象的每个执行方法都会替换执行Invocation中的invoke方法
            Person stuProxy = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, stuHandler);
    
           //代理执行上交班费的方法
            stuProxy.giveMoney();
        }
    }
    

    我们执行这个ProxyTest类,先想一下,我们创建了一个需要被代理的学生张三,将zhangsan对象传给了stuHandler中,我们在创建代理对象stuProxy时,将stuHandler作为参数了的,上面也有说到所有执行代理对象的方法都会被替换成执行invoke方法,也就是说,最后执行的是StuInvocationHandler中的invoke方法。所以在看到下面的运行结果也就理所当然了。

    运行结果:

    代理执行giveMoney方法
    张三上交班费50元
    giveMoney方法耗时1001ms

    上面说到,动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。是因为所有被代理执行的方法,都是通过在InvocationHandler中的invoke方法调用的,所以我们只要在invoke方法中统一处理,就可以对所有被代理的方法进行相同的操作了。例如,这里的方法计时,所有的被代理对象执行的方法都会被计时,然而我只做了很少的代码量。

    动态代理的过程,代理对象和被代理对象的关系不像静态代理那样一目了然,清晰明了。因为动态代理的过程中,我们并没有实际看到代理类,也没有很清晰地的看到代理类的具体样子,而且动态代理中被代理对象和代理对象是通过InvocationHandler来完成的代理过程的,其中具体是怎样操作的,为什么代理对象执行的方法都会通过InvocationHandler中的invoke方法来执行。带着这些问题,我们就需要对java动态代理的源码进行简要的分析,弄清楚其中缘由。
    **1.Java 动态代理创建出来的动态代理类 **
    上面我们利用Proxy类的newProxyInstance方法创建了一个动态代理对象,查看该方法的源码,发现它只是封装了创建动态代理类的步骤(红色标准部分):

    public static Object newProxyInstance(ClassLoader loader,
                                              Class<?>[] interfaces,
                                              InvocationHandler h)
            throws IllegalArgumentException
        {
            Objects.requireNonNull(h);
    
            final Class<?>[] intfs = interfaces.clone();
            final SecurityManager sm = System.getSecurityManager();
            if (sm != null) {
                checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
            }
    
            /*
             * Look up or generate the designated proxy class.
             */
            Class<?> cl = getProxyClass0(loader, intfs);
    
            /*
             * Invoke its constructor with the designated invocation handler.
             */
            try {
                if (sm != null) {
                    checkNewProxyPermission(Reflection.getCallerClass(), cl);
                }
    
                final Constructor<?> cons = cl.getConstructor(constructorParams);
                final InvocationHandler ih = h;
                if (!Modifier.isPublic(cl.getModifiers())) {
                    AccessController.doPrivileged(new PrivilegedAction<Void>() {
                        public Void run() {
                            cons.setAccessible(true);
                            return null;
                        }
                    });
                }
                return cons.newInstance(new Object[]{h});
            } catch (IllegalAccessException|InstantiationException e) {
                throw new InternalError(e.toString(), e);
            } catch (InvocationTargetException e) {
                Throwable t = e.getCause();
                if (t instanceof RuntimeException) {
                    throw (RuntimeException) t;
                } else {
                    throw new InternalError(t.toString(), t);
                }
            } catch (NoSuchMethodException e) {
                throw new InternalError(e.toString(), e);
            }
        }
    

    其实,我们最应该关注的是 Class

    byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0", Student.class.getInterfaces());
            String path = "G:/javacode/javase/Test/bin/proxy/StuProxy.class";
            try(FileOutputStream fos = new FileOutputStream(path)) {
                fos.write(classFile);
                fos.flush();
                System.out.println("代理类class文件写入成功");
            } catch (Exception e) {
               System.out.println("写文件错误");
            }
    

    对这个class文件进行反编译,我们看看jdk为我们生成了什么样的内容:

    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.lang.reflect.UndeclaredThrowableException;
    import proxy.Person;
    
    public final class $Proxy0 extends Proxy implements Person
    {
      private static Method m1;
      private static Method m2;
      private static Method m3;
      private static Method m0;
    
      /**
      *注意这里是生成代理类的构造方法,方法参数为InvocationHandler类型,看到这,是不是就有点明白
      *为何代理对象调用方法都是执行InvocationHandler中的invoke方法,而InvocationHandler又持有一个
      *被代理对象的实例,不禁会想难道是....? 没错,就是你想的那样。
      *
      *super(paramInvocationHandler),是调用父类Proxy的构造方法。
      *父类持有:protected InvocationHandler h;
      *Proxy构造方法:
      *    protected Proxy(InvocationHandler h) {
      *         Objects.requireNonNull(h);
      *         this.h = h;
      *     }
      *
      */
      public $Proxy0(InvocationHandler paramInvocationHandler)
        throws 
      {
        super(paramInvocationHandler);
      }
    
      //这个静态块本来是在最后的,我把它拿到前面来,方便描述
       static
      {
        try
        {
          //看看这儿静态块儿里面有什么,是不是找到了giveMoney方法。请记住giveMoney通过反射得到的名字m3,其他的先不管
          m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
          m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
          m3 = Class.forName("proxy.Person").getMethod("giveMoney", new Class[0]);
          m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
          return;
        }
        catch (NoSuchMethodException localNoSuchMethodException)
        {
          throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
        }
        catch (ClassNotFoundException localClassNotFoundException)
        {
          throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
        }
      }
    
      /**
      * 
      *这里调用代理对象的giveMoney方法,直接就调用了InvocationHandler中的invoke方法,并把m3传了进去。
      *this.h.invoke(this, m3, null);这里简单,明了。
      *来,再想想,代理对象持有一个InvocationHandler对象,InvocationHandler对象持有一个被代理的对象,
      *再联系到InvacationHandler中的invoke方法。嗯,就是这样。
      */
      public final void giveMoney()
        throws 
      {
        try
        {
          this.h.invoke(this, m3, null);
          return;
        }
        catch (Error|RuntimeException localError)
        {
          throw localError;
        }
        catch (Throwable localThrowable)
        {
          throw new UndeclaredThrowableException(localThrowable);
        }
      }
    
      //注意,这里为了节省篇幅,省去了toString,hashCode、equals方法的内容。原理和giveMoney方法一毛一样。
    
    }
    

    jdk为我们的生成了一个叫$Proxy0(这个名字后面的0是编号,有多个代理类会一次递增)的代理类,这个类文件时放在内存中的,我们在创建代理对象时,就是通过反射获得这个类的构造方法,然后创建的代理实例。通过对这个生成的代理类源码的查看,我们很容易能看出,动态代理实现的具体过程。

    我们可以对InvocationHandler看做一个中介类,中介类持有一个被代理对象,在invoke方法中调用了被代理对象的相应方法。通过聚合方式持有被代理对象的引用,把外部对invoke的调用最终都转为对被代理对象的调用。

    代理类调用自己方法时,通过自身持有的中介类对象来调用中介类对象的invoke方法,从而达到代理执行被代理对象的方法。也就是说,动态代理通过中介类实现了具体的代理功能。

    生成的代理类:$Proxy0 extends Proxy implements Person,我们看到代理类继承了Proxy类,所以也就决定了java动态代理只能对接口进行代理,Java的继承机制注定了这些动态代理类们无法实现对class的动态代理。
    上面的动态代理的例子,其实就是AOP的一个简单实现了,在目标对象的方法执行之前和执行之后进行了处理,对方法耗时统计。Spring的AOP实现其实也是用了Proxy和InvocationHandler这两个东西的。

    2.2 Cglib动态代理

    2.2.1 Cglib动态代理特点

    上面的静态代理和动态代理模式都是要求目标对象是实现一个接口的目标对象,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候就可以使用以目标对象子类的方式类实现代理,这种方法就叫做:Cglib代理

    Cglib代理,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展.

    • JDK的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口,如果想代理没有实现接口的类,就可以使用Cglib实现.
    • Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展java类与实现java接口.它广泛的被许多AOP的框架使用,例如Spring AOP和synaop,为他们提供方法的interception(拦截)
    • Cglib包的底层是通过使用一个小而块的字节码处理框架ASM来转换字节码并生成新的类.不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉.

    Cglib子类代理实现方法:

    • 1.需要引入cglib的jar文件,但是Spring的核心包中已经包括了Cglib功能,所以直接引入pring-core-3.2.5.jar即可.
    • 2.引入功能包后,就可以在内存中动态构建子类
    • 3.代理的类不能为final,否则报错
    • 4.目标对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法.
    2.2.2 代码示例

    目标对象类:UserDao.java

    /**
     * 目标对象,没有实现任何接口
     */
    public class UserDao {
    
        public void save() {
            System.out.println("----已经保存数据!----");
        }
    }
    

    Cglib代理工厂:ProxyFactory.java

    /**
     * Cglib子类代理工厂
     * 对UserDao在内存中动态构建一个子类对象
     */
    public class ProxyFactory implements MethodInterceptor{
        //维护目标对象
        private Object target;
    
        public ProxyFactory(Object target) {
            this.target = target;
        }
    
        //给目标对象创建一个代理对象
        public Object getProxyInstance(){
            //1.工具类
            Enhancer en = new Enhancer();
            //2.设置父类
            en.setSuperclass(target.getClass());
            //3.设置回调函数
            en.setCallback(this);
            //4.创建子类(代理对象)
            return en.create();
    
        }
    
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            System.out.println("开始事务...");
    
            //执行目标对象的方法
            Object returnValue = method.invoke(target, args);
    
            System.out.println("提交事务...");
    
            return returnValue;
        }
    }
    

    测试类:

    /**
     * 测试类
     */
    public class App {
    
        @Test
        public void test(){
            //目标对象
            UserDao target = new UserDao();
    
            //代理对象
            UserDao proxy = (UserDao)new ProxyFactory(target).getProxyInstance();
    
            //执行代理对象的方法
            proxy.save();
        }
    }
    

    在Spring的AOP编程中:
    如果加入容器的目标对象有实现接口,用JDK代理
    如果目标对象没有实现接口,用Cglib代理

    2.2.3 Cglib代理原理分析

    如果我们了解了CGLIB创建代理类的原理,那么其局限性也就一目了然。我们现在做个实验,将UserDao类加上final修饰符,使其不可被继承:

    再次执行测试代码,这次就报错了: Cannot subclass final class XXXX。

    所以通过CGLIB成功创建的动态代理,实际是被代理类的一个子类。那么如果被代理类被标记成final,也就无法通过CGLIB去创建动态代理。

    3、通过编译期提供的API动态创建代理类

    假设我们确实需要给一个既是final,又未实现任何接口的ProductOwner类创建动态代码。除了InvocationHandler和CGLIB外,我们还有最后一招:

    我直接把一个代理类的源代码用字符串拼出来,然后基于这个字符串调用JDK的Compiler(编译期)API,动态的创建一个新的.java文件,然后动态编译这个.java文件,这样也能得到一个新的代理类。

    package proxy;
    
    import java.io.File;
    import java.io.FileWriter;
    import java.io.IOException;
    import java.io.Writer;
    import java.lang.reflect.Constructor;
    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 ProductOwnerSourceCodeProxy  {
    
        public static void main(String[] arg) throws Exception {
            Class<?> c = getProxyClass();
            Constructor<?>[] constructor = c.getConstructors();
            Object POProxy = constructor[0].newInstance("Ross");
            Method defineBackLog = c.getDeclaredMethod("defineBackLog");
            defineBackLog.invoke(POProxy);
        }
    
        private static String getSourceCode() {
            String src = "package proxy;\n\n"
                    + "public final class ProductOwnerSCProxy {\n" 
                    + "\tprivate String name;\n\n"
                    + "\tpublic ProductOwnerSCProxy(String name){\n" 
                    + "\t\tthis.name = name;\n" + "\t}\n\n"
                    + "\t\tpublic void defineBackLog(){\n"
                    + "\t\tSystem.out.println(\"PO writes some document before defining BackLog\");"
                    + "\t\tSystem.out.println(\"PO: \" + name + \" defines Backlog.\");}}\n";
            return src;
        }
    
        private static String createJavaFile(String sourceCode) {
            String fileName = "C:\\Users\\i042416\\git\\JavaTwoPlusTwoEquals5\\src\\proxy\\ProductOwnerSCProxy.java";
            File javaFile = new File(fileName);
            Writer writer;
            try {
                writer = new FileWriter(javaFile);
                writer.write(sourceCode);
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return fileName;
        }
    
        private static void compile(String fileName) {
            try {
                JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
                StandardJavaFileManager sjfm = compiler.getStandardFileManager(null, null, null);
                Iterable<? extends JavaFileObject> iter = sjfm.getJavaFileObjects(fileName);
                CompilationTask ct = compiler.getTask(null, sjfm, null, null, null, iter);
                ct.call();
                sjfm.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        private static Class<?> loadClass() {
            URL[] urls;
            String path = "file:\\C:\\Users\\i042416\\git\\JavaTwoPlusTwoEquals5\\src\\";
            Class<?> c = null;
            try {
                urls = new URL[] { (new URL(path)) };
                URLClassLoader ul = new URLClassLoader(urls);
                c = ul.loadClass("proxy.ProductOwnerSCProxy");
                ul.close();
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
            return c;
        }
    
        private static Class<?> getProxyClass() {
            String sourceCode = getSourceCode();
            String javaFile = createJavaFile(sourceCode);
            compile(javaFile);
            return loadClass();
        }
    }
    

    参考
    Java代理设计模式(Proxy)的四种具体实现
    java 代理 理解原理及实现
    Java的三种代理模式

    相关文章

      网友评论

          本文标题:Java高级主题(五)——动态代理

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