从代理聊到 Lambda 表达式
从代理概念到代理模式
代理可谓无处不在。
它是法律术语,是组织术语,也是商业行为。它是一种服务器架构,也是一种设计模式。
从其概念而言,代理与委托似乎密不可分,在被代理方委托下,代理方进行一定权责的行为。但在技术领域的定义中,对齐这个解释却颇为牵强,代理往往表达为对被代理对象的访问权限控制。
然而在实现代理模式时,被代理方又被进一步淡化,开发人员只关注在代理层插入自定义功能,全然没有委托的含意,也不再局限于访问的控制,只有无感知的侵入还算比较得体。
在 Java 的编程框架里,依赖有其静态的一面,调用关系在编译期间业已确定,也有其动态的一面,调用对象能够进行指派增强。代理模式是对动态依赖的具体实现,本质是面向接口的契约和面向继承的指派,一般可分为静态代理和动态代理,这些都是老生常谈。
静态代理无非就是如何设计类和实现类,左右不过是面向对象的编码,机制上着实乏善可陈。相反,动态代理花样繁多,耳熟能详的就有 JavaProxy、CGLIB、ASM、Javassist、AspectJ 和之上的 SpringAOP 等等。支撑这广泛框架的,是 Java 的类加载机制,实现最终代理接管,必然要经过代理类生成、代理类加载、代理对象替换三个阶段。
从类加载机制到代理对象
某种意义上,编程语言是英语的特殊子集,“.java”文件是 Java 的人工书写形式,而“.class”文件才是 JVM 世界中的通行文本。
代理类生成主要是指代理类字节码的生成,而字节码也算是一种特殊的编程语言,只要按照既定规范,能够“入乡随俗讲当地话”,工具只是节省人力,最终跳不出“javac”结果的条条框框。
在虚拟机规范中,一个“.class”的生命周期包括如下七个阶段:
加载(Loading):获取类的二进制字节流,生成代表该类的 java.lang.Class 对象
验证(Verification):校验字节流是否符合虚拟机要求,包括格式、元数据、语义和引用等校验
准备(Preparation):类变量分配内存和初始化
解析(Resolution):常量池符号引用替换成直接引用
初始化(Initialization):执行类构造器方法(<clinit>),从这开始执行类中定义的程序代码
加载是一切的起点,类加载器(ClassLoader)则是需要叩开的大门。曾经看过一家肉制品加工的老板吹嘘自己的机器和生产线有多先进——“猪从这边赶进去,香肠就从另一边出来了”,这么看类加载器也差不多——“字节码从这边赶进去,Class 对象就从另一边出来了”。
Java 类加载的“双亲委派模型”是另一个老生常谈的话题,实践中却不必拘泥于此,只消抓住“类是由类和加载器共同确定”这一要旨,剩下的就是因势利导了:
双亲委派的层级模型保障基本的稳定性,核心类(如 rt.jar)最终都由 Bootstrap ClassLoader 加载,既防止恶意篡改,又能在任何加载器环境中保持绝对统一
由于类加载的可见性是单向的(即子加载器对父加载器加载的类可见,反之不然),必要时也需要破坏双亲委派模型,例如基础类需要调用用户代码的时候(如 SPI 机制),由此引入上下文加载器(Context ClassLoader),通过线程传递来达到父加载器请求子加载器去完成类加载动作的效果
用户对程序隔离和动态性的追求,往往只将层级模型作为加载顺序的约束,通过自定义的类加载器和选择策略,来达到设计目的(如 SpringBoot/Tomcat/OSGI 的类加载架构)
前面长篇累牍也只讲到与代理类加载相关的事情,因为如何生成代理对象并不是语言机制的问题,更多是上层业务的选择。至于是使用 JavaProxy 实现基于接口的代理,还是使用 CGLIB 实现基于对象的代理,都没有本质差别,只是框架不同罢了。
如果将静态代理比作逐帧手绘的老式动画,那么动态代理就像包含矢量计算的游戏 CG。当它们灌注成盘准备播放的时候,所谓动态也只是少画几张手稿而已,毕竟即使观影途中觉得颜色不够鲜艳,也不能掀开机器再往胶卷上涂抹改进。
考虑两个真正称得上是“动态”的场景——从空间上,被代理对象生命周期不由编程控制;从时间上,在程序运行中织入代理逻辑。SpringAOP 是处理第一种场景的特殊方案,因为它管理所有 Bean 的生命周期,对于更一般性的场景,还需要从 JVM 本身的支持入手。
从 Java agent 到动态代理
JVM TI(JVM Tool Interface)是一组用于开发和监控工具的官方编程接口,可以将它理解成在 JVM 中预埋的事件回调或钩子方法(Hook),能够实现各种 profile、debug、监控、线程分析、堆栈分析、覆盖分析等工具。顺着代理的场景,针对类加载事件(ClassFileLoadHook),开发者可以使用 JVM TI 动态地加载、卸载或重新定义类,一般是通过 Java agent 实现 Instrumentation 接口来处理的。
简单浏览 Instrumentation 的接口定义,通过注册的 ClassFileTransformer 拦截类加载事件,可以对类字节码进行重写,从而实现类的重定义。
Java agent 有两种加载方式,可与前面提到的动态场景相对应:
JVM 启动时加载,通过启动参数加载 agent,可以拦截编程时不直接控制生命周期的对象
JVM 运行时加载,通过 Attach API 加载 agent,可以重定义类实现代理逻辑织入
到这里似乎有时机有手段可以无侵入地实现任意对象的代理了,毕竟猪是自己亲手赶进去的,出来什么口味的香肠还不是手到擒来。可是偏偏就有漏网之鱼,因为有些香肠,它居然不需要猪。
从 Lambda 表达式到放弃代理
上边这个简单的例子尝试查看 Transformer 拦截类的情况(忽略 add 之前加载的类,premain 在 main 之前,不影响分析),出现了一个奇怪的类(test.TestAgent$Lambda$14/0x0000000800099440),它没有经过 transform 方法,也与匿名内部类(test/TestAgent$1)不同,没有生成任何.class 文件。
这就是 Lambda 表达式生成的“隐藏类”,可以添加调试参数(-Djdk.internal.lambda.dumpProxyClasses)将其保留下来,反编译结果可以看出它是个合成类,从内容可以推断几点有意思的地方:
final 类加 private 构造函数,说明要通过反射来间接使用
接口实现引用了主类“不存在”的静态方法,说明 Lambda 表达式生成类与匿名内部类的差别
生成的类名与 class.getName()不同,没有后缀(TestAgent$Lambda$14/0x0000000800099440),说明“隐藏类”规则的特殊性
使用 javap 指令解析主类的字节码,查看 Lambda 表达式生成的关键信息(节选):
javap -verbose -private TestAgent.class
可以看到确是生成了一个静态方法(lambda$main$0),最终委托给 LambdaMetafactory.metafactory()生成调用点:
层层追查下去,找到生成 Class 的地方(省略其他):
所以 Lambda 表达式生成类的特殊性在于它是由 Unsafe.defineAnonymousClass 方法生成的“VM anonymous class”,这里不展开它的具体细节,暂且简单理解它是一个不被类加载器系统或者系统字典感知的类型:
它“没有名字”,即便使用 class.getName()结果进行 Class.forName()操作也会抛出 ClassNotFoundException 异常,构造出来后只能通过 Unsafe.defineAnonymousClass()返回的 Class 对象进行反射操作
它“不显式挂在 ClassLoader 下面”,与 retransform class 不相容,不能被重定义
那么,使用 Java agent 是不是就完全没法拦截和生成 Lambda 表达式的代理对象了?其实也不是,虽然这种香肠不需要猪,但我们可以代理香肠机——也就是拦截 InnerClassLambdaMetafactory 类,并对 spinInnerClass()方法做手脚。
但这样做过于复杂,也侵入了本不对开发人员可见的机制,稍有不慎影响整个系统。所以,对于解决 Lambda 表达式生成对象动态代理问题的最好方法或许就是不要有这种想法。
条条大路通罗马,没必要钻这死胡同。
版权声明: 本文为 InfoQ 作者【陈一之】的原创文章。
原文链接:【http://xie.infoq.cn/article/7560243be36c6047c8ce92517】。文章转载请联系作者。
评论