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

  • 相关阅读:
    关于React的脚手架
    yarn和npm
    谈谈NPM和Webpack的关系
    php开发环境和框架phalcon的搭建
    Centos6.5--svn搭建
    System.Diagnostics.Process.Start(ProcessStartInfo)
    PHP错误:call to undefined function imagecreatetruecolor
    PostgreSQL删除表中重复数据行
    URL存在http host头攻击漏洞-修复方案
    for循环的执行顺序
  • 原文地址:https://www.cnblogs.com/manongxiao/p/13449480.html
Copyright © 2011-2022 走看看