问题分析: 这道题目考察的是Java的另外一种基础机制:反射,就像是一种魔法,引入运行时自省能力,赋予了java令人意外的活力,通过运行时操作元数据或对象,java可以灵活的操作运行时才能确定的信息。而动态代理,则是延伸出来的一种广泛应用于产品开发中的技术,很多繁琐的重复编程,都可以被动态代理机制优雅的解决。 注意:我在前面的Spring框架中对动态代理的实际应用做了简单的介绍,有需要的话可以去看一下,仅是个人见解。
本题涉及的知识点比较多,因此可以扩展的内容就很多,如:
对反射机制的了解和掌握程度动态代理解决了什么问题,在业务系统中的应用场景JDK动态代理在设计和实现上与cglib等方式有什么不同,如何进行取舍对于Java反射机制本身,如果去看java.lang或java.lang.reflect包下的相关抽象,就会有一个很直观的印象。Class、Field、Method、Constructor等等,这些完全就是去操作类和对象的元数据对应。我们至少要掌握基本场景编程,官方的参看文档:https://docs.oracle.com/javase/tutorial/reflect/index.html
对于反射,有一点需要特别提醒一下,就是反射提供的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;否则,被认为是不合法( illegal)操作。如果我们的实体类是定义在模块里面,我们需要在模块描述符中明确声明:
module MyEntities { // Open for refection opens com.mycorp to java.persisence; }因为反射机制使用广泛,根据社区的讨论,目前Java9仍然保留了兼容Java8的行为,但是很有可能在以后的版本,完全启用前面提到的针对setAccessible的限制,即只有当反射机制操作的模块和指定的包对反射调用者模块Open,才能使用setAccessible,可以使用下面的参数显示设置:
--illegal-access={ permit | warn | deny }现在我们来看一下动态代理是解决什么问题。
首先,它是一个代理机制。如果熟悉设计模式中的代理模式,会知道代理可以看做是对调用目标的一个包装,这样我们队目标代码的调用不是直接发生的,而是通过代理完成,其实很多动态代理场景,可以看做是装饰器(Decorator)模式的应用。
通过代理可以让调用者与实现者之间解耦。比如RPC调用,框架内部的寻址、序列化、反序列化等,对于调用者往往是没有太大意义的,通过代理可以提供更加友善的界面。
代理的发展历史经历了静态到动态的过程,源于静态代理引入的额外工作。类似早期的RMI之类古董技术,还需要rmic之类的工具生成静态stub等各种文件,增加了很多繁琐的准备工作,而这又和业务逻辑没有关系。利用动态代理机制,相应的stub等类,可以在运行时生成,对应的调用操作也是动态生成,极大提高了生产力。改进的RMI已经不再需要手动去准备这些了,虽然仍然是相对比较古老落后的技术,未来可能会被移除。
下面来看一个jdk动态代理的一个简答例子,只是加了一句print,在生产系统中,可以轻松扩展类似逻辑进行诊断、限流等。
public class MyDynamicProxy { public satic 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(); } } interface Hello { void sayHello(); } class HelloImpl implements Hello { @Override public void sayHello() { System.out.println("Hello World"); } } 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为纽带,为被调用目标构建代理对象,进而应用程序就可以使用代理对象间接运行调用目标的逻辑,代理为应用插入额外逻辑(这里是print)提供了便利的入口。
从API设计和实现的角度,这种实现仍然有局限性,因为它是以接口为中心的,相当于添加了一种对于被调用者没有太大意义的限制。实例化的Proxy对象,而不是真正的被调用类型,这在实践中还是可能带来各种不变和能力退化。
如果被调用者没有实现接口,而我们还是希望利用动态代理机制,那么可以考虑其他方式。Spring AOP支持两种模式的动态代理,JDK Proxy或者cglib。如果选择cglib方式,对接口的依赖被克服了。
cglib动态代理采取的是创建目标类的子类的方式,因为是子类化,可以达到近似使用被调用者本身的效果,在Spring编程中,框架通常会处理这种情况,当然也可以显示指定。关于类似方案的实现细节,在这里不做过多讨论。
JDK Proxy优势:
最小依赖关系,减少依赖意味着简化开发和维护,JDK本身的支持,可能比cglib更加可靠平滑进行jdk版本升级,而字节码类库通常需要进行更新以保证在新版Java上能够使用代码实现简单基于类似cglib框架的优势:
有的时候调用目标可能不便实现额外接口,从某种角度看,限定调用者实现接口是有些侵入性的实践,类似cglib动态代理就没有这种限制只操作关心的类,而不必为其他相关类增加工作量高性能另外,从性能角度看,记得有人曾经得出结论说JDK Proxy比cglib或者Javassist慢几十倍。坦白说,不去争论具体的benchmark细节,在主流JDK版本中,JDK Proxy在典型场景可以提供对等的性能水平,数量级的差距基本上不是广泛存在的。而且反射机制性能在现代JDK中,自身已经得到了极大的改进和优化,同时JDK很多功能也不完全是反射,同样使用了ASM进行字节码操作。
在选型中,性能未必是唯一的考量,可靠性、可维护性、编程工作量等往往是更主要的考虑因素,毕竟标准类库和反射编程的门槛要低的多,代码量也是更加可控的,如果比较下不同开源项目在动态代理开发上的投入也能看到这一点。
动态代理应用非常广泛,虽然最初多是因为RPC等使用进入大家视线,但是动态代理的使用场景远远不仅如此,它完美符合Spring AOP等切面编程。之后会详细分析AOP目的和能力。简单说可以看做是对oop的一个补充,因为OOP对于跨越不同对象或类的分散、纠缠逻辑表现力不够,比如在不同模块的特定阶段做一些事情,类似日志、用户鉴权、全局性异常处理、性能监控,甚至事务处理等,参考如下图: AOP通过动态代理机制可以让开发者从这些繁琐事项中抽身出来,大幅度提高了代码的抽象程度和复用度。从逻辑上来说,在软件设计和实现中的类似处理,如Facade、 Observer等很多设计目的,都可以通过动态代理优雅地实现。