深入浅出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

原文地址:https://www.cnblogs.com/manongxiao/p/13449480.html