zoukankan      html  css  js  c++  java
  • 深入浅出java反射应用一一动态代理

    Java高级之反射

    反射应用之动态代理

    问题的起源

    1. 适逢学生暑期,现在驾校里有许多学生趁着假期开始学车,目前正在练习科目二,整体流程固定,如下:
    /**
     * 驾校学生接口
     */
    interface DrivingStudent{
        //准备科目二的考试
        void prepare();
    }
    /**
     * 正常驾校学生的考试流程
     */
    class CommonStudents implements DrivingStudent{
        @Override
        public void prepare() {
            System.out.println("在驾校正常上班时间点,听教练讲要点、然后练习,熟练后去考试!");
        }
    }
    
    1. 一切都在按部就班的进行着。这天,新加入了一个学员,她是驾校老板的女儿,作为老板的女儿,教练为了让老板开心好给自己加薪,自然要给她VVIP的练车待遇:在prepare()方法执行之前,先好好的跟她讲一些注意事项,prepare()方法执行之后,也就是驾校下班后,单独指导她练车技巧以及传授经验。

    2. 现在,科二整体流程固定,可以比喻为类CommonStudents已经加载到内存,成为运行时的类,众所周知,java在运行时的类是不允许被修改的,那么教练该如何实现给老板女儿开小灶的功能?

    问题的扩展

    • 回到java编程世界中,Spring框架在当下非常热门,AOPIOC这些耳熟能详的词在java编程世界中经常出现,那么AOP解决了码农的什么样需求?
    • 联想到开发应用程序过程中,我们或多或少都会遇到类似于这样的需求:比如为方便排查问题,要在某些函数(例如上面的:prepare()函数)的调用前后加上相应的日志记录;为了保持数据的安全性,要给某些函数加上事务的支持等等。xml和注解的盛行,使我们程序员可以在xml和注解中声明要在哪一类函数前后加上日志以及日志等,但是这些类已经是运行状态,我们声明了要在这些运行中的函数前后加功能,但是运行时的类java是不允许被修改的,那么该如何实现此需求呢?

    问题的思路

    分析

    • 在上篇反射1中我们提到,java世界中有这么一个类java.lang.Class,它是描述类的类,它对应的便是那一个个运行时的类,我们可以通过它去拿到运行时的类的所有类型信息,包括方法、属性等。也可以通过它去创建一个运行时的类的对象。
    • 回到驾校的问题:教练希望能给老板女儿开小灶,但是对外宣称也必须是走的是驾校的正常流程,也就是调用的是prepare()方法;回到java编程世界,比如有个添加数据的函数add(),我们希望在add()函数前后加上日志以打印出入参出参。但是在调用方看来,他应该只是调用了add()函数,他并不知道额外做了添加日志其他的操作。
    • 换句话说,我们通过Class获取到了运行时的prepare()add()方法,并在这些方法前后添加了自己的额外功能,而调用方还认为自己只是调用的是正常的方法。

    方案整理

    • 为了实现了新的功能,我们可以在类运行时动态的去创建一个新的类,这个新的类便是我们要实现某个函数的类(即被代理类也是目标对象)的代理类。
    • 以教练为例,创建目标对象CommonStudents的代理类的需要我们考虑两个问题:
      1. CommonStudents被加载到内存中,成为运行时的类,如何通过它创建一个代理类?
      2. CommonStudents类中的prepare()方法被调用时,如何让它去调用代理类中在prepare()方法的基础上添加了额外功能的方法?

    问题的解决

    解决流程

    • 解决问题1:Proxy 是专门完成代理的操作类,通过该类为一个或多个接口动态地生成实现类。它是所有动态代理类的父类;创建一个代理对象的方法源码如下:

      public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) throws IllegalArgumentException{
            **********
      }
      
    • 解决问题2:创建一个类实现接口InvocationHandler的类,在该类中实现invoke()方法,在该方法中添加上我们需要添加的逻辑。这样在调用目标对象的方法时,调用的是代理类中的invoke()方法。

    具体实现

    • 解决问题2的代码,也就是当调用'目标对象'CommonStudentsprepare()方法时,如何使其重定向到一个新的方法invoke(),也就是有额外功能的方法。

      class MyInvocationHandler implements InvocationHandler{
      		//目标对象,可以通过反射来调用目标对象的方法
          private Object target;
          public MyInvocationHandler(Object target){
              this.target = target;
          }
        //重写invoke方法,添加上自己的功能
          @Override
          public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
              System.out.println("开始练车之前,好好跟你讲一些注意事项,以及快速上手的技巧!");
              //反射机制调用目标对象的方法
              Object result = method.invoke(target, args);
              System.out.println("下班后,单独对你做一些练车的指导以及加练!");
              return result;
          }
      }
      
    • 主方法,内含解决问题1的代码。

      public static void main(String[] args) {
              System.out.println("========正常学生流程开始===========");
              DrivingStudent student = new CommonStudents();
              //正常学生的流程
              student.prepare();
              System.out.println("========正常学生流程结束===========");
              System.out.println("*********************************");
              System.out.println("=======老板女儿练车流程开始==========");
              //老板女儿的练车流程
        			//当CommonStudents方法被调用时,走invoke()方法,因此要将CommonStudents当作参数传入。
              InvocationHandler handler = new MyInvocationHandler(student);
              //java提供了动态代理来解决在运行时动态创建一个代理类的方案
              DrivingStudent bossStudent = (DrivingStudent)Proxy.newProxyInstance(student.getClass().getClassLoader(),
                      student.getClass().getInterfaces(),
                      handler);
              bossStudent.prepare();
              System.out.println("=======老板女儿练车流程结束==========");
      
          }
      
    • 运行结果

      image-20200802095522566

    问题解决的思考

    • jdk动态代理为什么需要接口?

    • 简单理解下源码:

      1. Proxy.newProxyInstance中几行重要代码:
      //
      public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h) throws IllegalArgumentException {
              Objects.requireNonNull(h);
      				......
             //这一步是查找并生成代理类的Class
              Class<?> cl = getProxyClass0(loader, intfs);
        			......
                //生成的代理类中存在参数为InvocationHandler的构造函数
                  final Constructor<?> cons = cl.getConstructor(constructorParams);
        			......
                  //将实现了InvocationHandler的类当作参数传入到newProxyInstance方法后,这一行是利用这个参数创建一个代理类
                  return cons.newInstance(new Object[]{h});
              ......
          }
      
      1. getProxyClass0(loader, intfs)中逻辑:
      private static Class<?> getProxyClass0(ClassLoader loader,
                                                 Class<?>... interfaces) {
              ......
              //重点在:ProxyClassFactory
              return proxyClassCache.get(loader, interfaces);
          }
      
      1. ProxyClassFactory的主要操作:
      private static final class ProxyClassFactory
              implements BiFunction<ClassLoader, Class<?>[], Class<?>>
          {
        		......
            {
            	//验证参数、接口等  		
        			......
              		//生成byte类型的代理类,并将其返回
                  byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                      proxyName, interfaces, accessFlags);
               ......
              }
          }
      
      1. 重点理解一下生成的byte[]类型的代理类。将byte[]类型的代理类写出到文件上,并且进行反编译,代码如下:

        byte[] classFile = ProxyGenerator.generateProxyClass(
                        "$Proxy0", new Class[]{DrivingStudent.class});
        //自定义的目录
        String path = "*****"+ "$Proxy0" + ".class";
        try (FileOutputStream fos = new FileOutputStream(path)) {
             fos.write(classFile);
             fos.flush();
             System.out.println("byte[]类型代理类class文件写入成功");
        } catch (Exception e) {
             System.out.println("byte[]类型代理类发生错误");
        }
        
      2. 写入成功后,对生成的代理类进行反编译后得到的类如下:

        public final class $Proxy0 extends Proxy implements DrivingStudent {
          private static Method m1;
          private static Method m2;
          private static Method m3;
          private static Method m0;
          //看到这里我们能看到,自动生成的代理类对象有一个入参是InvocationHandler的构造函数,这样在创建代理类时,通过该构造方法便将被代理类(实现了DrivingStudent)和InvocationHandler联系了起来。
          public $Proxy0(InvocationHandler paramInvocationHandler) {
            super(paramInvocationHandler);
          }
          //省略的是equals和toString方法
          ......
          //当调用接口中的prepare()方法时,实际上走的是invoke()方法。invoke()方法中有我们自己加的逻辑
          public final void prepare() {
            try {
              this.h.invoke(this, m3, null);
              return;
            } catch (Error|RuntimeException error) {
              throw null;
            } catch (Throwable throwable) {
              throw new UndeclaredThrowableException(throwable);
            } 
          }
           //省略的是hashCode方法
          ......
          static {
            try {
              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("com.practice.reflect.DrivingStudent").getMethod("prepare", new Class[0]);
              m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
              return;
            } catch (NoSuchMethodException noSuchMethodException) {
              throw new NoSuchMethodError(noSuchMethodException.getMessage());
            } catch (ClassNotFoundException classNotFoundException) {
              throw new NoClassDefFoundError(classNotFoundException.getMessage());
            } 
          }
        }
        
    • 回到最初的问题:jdk生成的动态代理类是继承于Proxy类的,而java类是不允许多继承的,为了使代理类和目标对象建立联系,就必须实现一个接口。

    结论

    • java动态代理是根据被代理对象动态的创建了一个新类,当调用被代理对象的方法时,会跳转到调用InvocationHandler中的invoke()方法,在创建代理类方法newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)的入参我们看到,它是根据字节码文件自动生成的代理类,并且传入了 InvocationHandler参数。
    • 通过反编译生成的动态代理类得到的代码,我们看到代理类继承了Proxy类以及实现了目标对象实现的接口,并且该代理类中的构造函数用到了上面传入的 InvocationHandler参数,这样便将目标对象和InvocationHandler联系了起来,InvocationHandler中的invoke()方法中定义了我们需要新添加的功能,当调用目标对象的方法时,使其跳转到调用invoke()方法,这样便实现了在运行时创建新的类以满足我们新增功能的需求。

    原创不易,欢迎转载,转载时请注明出处,谢谢!
    作者:潇~萧下
    原文链接:https://www.cnblogs.com/manongxiao/p/13449480.html

  • 相关阅读:
    洛谷p1017 进制转换(2000noip提高组)
    Personal Training of RDC
    XVIII Open Cup named after E.V. Pankratiev. Grand Prix of Eurasia
    XVIII Open Cup named after E.V. Pankratiev. Grand Prix of Peterhof.
    Asia Hong Kong Regional Contest 2019
    XVIII Open Cup named after E.V. Pankratiev. Grand Prix of Siberia
    XVIII Open Cup named after E.V. Pankratiev. Ukrainian Grand Prix.
    XVIII Open Cup named after E.V. Pankratiev. GP of SPb
    卜题仓库
    2014 ACM-ICPC Vietnam National First Round
  • 原文地址:https://www.cnblogs.com/manongxiao/p/13449480.html
Copyright © 2011-2022 走看看