Java反射机制与动态代理的原理是什么?

典型回答

反射机制是Java语言提供的一种基础功能,赋予程序在运行时自省(introspect,官方用语)的能力。通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或构造对象,甚至可以运行时修改类定义。

动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制做到的。比如用来包装RPC调用、面向切面的编程(AOP)。

实现动态代理的方式很多,比如JDK自身提供的动态代理,就是主要利用了反射机制。还有其他的实现方式,比如利用传说中更高性能的字节码操作机制,类似ASM、cglib(基于ASM)、Javassist等。

知识扩展

1、反射机制及其演进

对于Java语言的反射机制本身,如果你去看一下java.lang或java.lang.reflect包下的相关抽象,就会有一个很直观的印象了。Class、Field、Method、Constructor等,这些完全就是我们去操作类和对象的元数据对应。反射的各种典型用例有太多的文章进行过详细的介绍,这里就不再赘述。

关于反射,有一点需要特意提一下,就是反射提供的AccessibleObject.setAccessible(boolean flag)。它的子类也大都重写了这个方法,这里的所有accessible可以理解成修饰成员的public、protected、private,这意味着我们可以在运行时修改成员访问限制!

setAccessible的应用场景非常普遍,遍布我们的日常开发、测试、依赖注入等各种框架中。比如,在O/R Mapping框架中,我们为一个Java实体对象,运行时自动生成setter、getter的逻辑,这对于加载或者持久化数据是非常必要的,框架通常可以利用反射做这个事情,而不需要开发者手动写类似的重复代码。

另一个典型场景就是绕过API访问控制。我们日常开发时可能被迫要调用内部API去做些事情,比如,自定义的高性能NIO框架需要显式地释放DirectBuffer,使用反射绕开限制是一种常见办法。

但是,在Java 9以后,这个方法的使用可能会存在一些争议,因为Jigsaw项目新增的模块化系统,出于强封装性的考虑,对反射访问进行了限制。Jigsaw引入了所谓Open的概念,只有当被反射操作的模块和指定的包对反射调用者模块Open,才能使用setAccessible;否则,被认为是不合法操作。

因为反射机制使用广泛,根据社区讨论,目前Java 9仍然保留了兼容Java 8的行为,但是很有可能在未来版本,完全启用这里提到的针对setAccessible的限制。

2、动态代理

一起看看,动态代理到底是解决什么问题?

首先,它是一个代理机制。如果熟悉设计模式中的代理模式,我们会知道,代理可以看做是对调用目标的一个包装,这样我们对目标代码的调用不是直接发生的,而是通过代理完成。

通过代理可以让调用者与实现者之间解耦。比如进行RPC调用,框架内部的寻址、序列化、反系列化等,对于调用者往往是没有太大意义的,通过代理,可以提供更加友善的界面。

代理的发展经历了静态到动态的过程,源于静态代理引入的额外工作。类似早期的RMI之类古董技术,还需要rmic之类工具生成静态stub等各种文件,增加了很多繁琐的准备工作,而这又和我们的业务逻辑没有关系。利用动态代理机制,相应的stub等类,可以在运行时生成,对应的调用操作也是动态完成,极大地提高了我们的生产力。改进后的RMI已经不再需要手动去准备这些了,虽然它仍然是相对古老落后的技术,未来也许会逐步被移除。

这么说可能不够直观,我们可以看JDK动态代理的一个简单例子。下面指示加了一句print,在生产系统中,我们可以轻松扩展类似逻辑进行诊断、限流等。

public class MyDynamicProxy {

  public static void main(String[] args) {
    HelloImpl hello = new HelloImpl();
    MyInvocationHandler handler = new MyInvocationHandler(hello);
    // 构造代码实例
    Hello proxyHello = (Hello) Proxy.newProxyInstance(
        HelloImpl.class.getClassLoader(),
        HelloImpl.class.getInterfaces(),
        handler);
    // 调用代理方法
    proxyHello.sayHello();
  }

  static interface Hello {
    void sayHello();
  }

  static class HelloImpl implements Hello {
    @Override
    public void sayHello() {
      System.out.println("Hello World");
    }
  }

  static class MyInvocationHandler implements InvocationHandler {
    private Object target;

    public MyInvocationHandler(Object target) {
      this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      System.out.println("Invoking sayHello");
      Object result = method.invoke(target, args);
      return result;
    }
  }
}

上面的JDK Proxy例子,非常简单地实现了动态代理的构建和代理操作。首先,实现对应的InvocationHandler;然后,以接口Hello为纽带,为被调用目标构建代理对象,进而应用程序就可以使用代理对象间接运行调用目标的逻辑,代理为应用插入额外逻辑(这里是println)提供了便利的入口。

从API设计和实现的角度,这种实现仍然有局限性,因为它是以接口为中心的,相当于添加了一种对于被调用者没有太大意义的限制。我们实例化的是Proxy对象,而不是真正的被调用类型,这在实践中还是可能带来各种不便和能力退化。

如果被调用者没有实现接口,而我们还是希望利用动态代理机制,那么可以考虑其它方式。我们知道Spring AOP支持两种模式的动态代理,JDK Proxy或者cglib,如果我们选择cglib方式,你会发现对接口的依赖被克服了。

cglib动态代理采取的是创建目标类型的子类的方式,因为是子类话,我们可以达到近似使用被调用者本身的效果。

那我们在开发中怎样选择呢?简单对比两种方式各自优势。

JDK Proxy的优势:

  • 最小化依赖关系,减少依赖意味着简化开发和维护,JDK本身的支持,可能比cglib更加可靠。
  • 平滑进行JDK版本升级,而字节码类库通常需要进行更新以保证在新版Java上能够使用。
  • 代码实现简单。

基于类似cglib框架的优势:

  • 有的时候调用目标可能不便实现额外接口,从某种角度看,限定调用者实现接口是有些侵入性的时间,类似cglib动态代理就没有这种限制。
  • 只操作我们关心的类,而不必为其它相关类增加工作量。
  • 高性能。

【完】

猜你喜欢

转载自blog.csdn.net/qweqwruio/article/details/81359939