Java Core 「8」字节码增强技术
01-动态代理技术
我们都知道,JVM 中加载并运行的是 Java 源文件编译后生成的字节码文件(*.class)。关于字节码文件格式及相关操作的介绍,可以参考美团技术团队的文章[1]。
字节码增强技术是一类对现有字节码文件进行修改或者动态生成新的字节码文件的技术。最常见的基于字节码增强技术的应用是动态代理,以及基于动态代理实现的面向切片编程技术。
padi.tech 中的一张图片很好地描述了字节码增强技术之间的关系:
图 1. 字节码增强相关技术及它们的关系
[1] 字节码增强技术探索
01.1- JDK 动态代理
JDK 中与动态代理相关的类在 java.lang.reflect 包中,主要有以下几个类或接口:
Proxy,提供了接口来创建代理类,或者代理类实例
InvocationHandler,定义了 invoke 方法,在代理类上调用方法时,会回调到该方法。
基于 Proxy 实现动态代理,被代理的类必须要实现某个接口。这是因为,动态代理类是通过继承 Proxy 类并实现与被代理类同样的接口,来保证与被代理类保持同样的接口。
图 2. 基于 JDK Proxy 的动态代理实现基本流程
如上图所示,当使用 JDK Proxy 实现动态代理时,需要将待代理的类(用来获得目标类的类加载器(1)、目标类实现的所有接口列表(2))和 InvocationHandler 接口实现类的实例(3),这三个参数传入到Proxy::newProxyInstance
方法中,便会生成动态代理类 $Proxy0 (也可能不是这个名字),该代理类与被代理类均实现同一个接口。因此对客户端来说,具体实现是透明的,接口是一致的。
当客户端调用代理类的方法时,Proxy 中定义的方法会回调 InvocationHandler 接口实现类实例的 invoke 方法。
一个完整的基于 JDK Proxy 的动态代理示例可以在我的 gitee 中找到,有兴趣的读者可以参考一下。
01.2-CGLIB 动态代理
基于 ASM 的 CGLIB 也是常用来实现动态代理的技术。例如 Spring AOP 即通过 CGLIB 实现动态代理功能。与 JDK Proxy 相比,CGLIB 不要求被代理类必须实现某个接口。
如果要查看 CGLIB 生成的动态代理类的字节码文件,可在程序中追加如下代码:
运行程序后,可在”D:\cglib”中看到生成的字节码文件。
通过jad -s java *.class
分析生成的字节码文件发现,代理类的实现如下:
其中Service
是被代理类。可以看到,CGLIB 与 JDK Proxy 采用不同的方式来实现代理类。
图 3. 基于 CGLIB 的动态代理基本流程
如上图所示,当使用 CGLIB 实现动态代理时,需要将被代理类(1)和 MethodInterceptor 接口实现类的实例(2)传入 Enhancer 中,便会生成动态代理类Service$EnhancerByCGLIB$a1718795
(也可能不是这个名字),该类为被代理的子类,所以拥有同样的的接口,对客户端来说其实现是透明的。
当客户端调用代理类中的方法时,会回调到 MethodInterceptor 接口中的 intercept 方法。
一个完整的基于 CGLIB 的动态代理示例可以在我的 gitee 中找到,有兴趣的读者可以参考一下。
[1] Spring进阶 - Spring AOP实现原理详解之Cglib代理实现
02-字节码插桩
对于需要手动操纵字节码的场景,可以使用 ASM / Javassist / Instrument / byte-buddy 等框架。
02.1-ASM 实现插桩
ASM 中实现字节码操作主要依赖一下几个类:
ClassReader,读取要增强的类字节码
ClassWriter,生成新的字节码、写入文件。
ClassVisitor / MethodVisitor,自定义实现对读取的字节码进行处理,插桩逻辑一般实现在 MethodVisitor 子类中。
我的 gitee 中有完整的使用 ASM 创建 HelloWorld 类字节码的实例,感兴趣的读者可以参考。[1] 中也有对方法进行增强的示例代码,也可以参考下。
ASM 提供的字节码操作 API 都比较底层,例如:
相对来说,使用不太方便,比较麻烦、隐晦。
02.2-Javassist 实现插桩
Javassist 中实现字节码操作主要依赖以下几个类:
ClassPool,工具类,提供获取、新建 CtClass 的接口,
CtClass,compile-time class,编译时类结构信息,字节码的抽象表示。
CtMethod / CtFiled,分别代表方法、域的抽象表示。
我的 gitee 中有完整的使用 Javassist 创建 HelloWorld 类字节码的实例,感兴趣的读者可以参考。
Javassist 强调源代码层次操纵字节码。例如,在方法前后插入代码:
可以看到,相对于 ASM 来说,对开发者已经相对友好。
02.3-Instrument 替换已加载的字节码
前两节提到的方法,可以对字节码进行修改、增强,但是如果将修改后的代码载入 JVM,则会抛异常。说明单纯的使用 ASM / Javassist 并不能对已加载的类进行修改、增强。
要解决上面的问题,需要用到 JVM 提供的 Instrument 机制。
图 4. Instrument j 机制替换 JVM 中已加载字节码基本流程图
使用 Instrument 替换 JVM 中已加载的类时,需要:
创建 agent.jar,其中包含 Agent 入口 agentmain 方法和 MANIFEST.MF 文件,指定了 Agent 路径。
Agent-Class: self.samson.example.aop.ByteEnhancementExamples // Agent 的路径 Can-Redefine-Classes: true Can-Retransform-Classes: true 复制代码
agentmain 方法中,添加 ClassFileTransformer 接口实现类的实例到 Instrumentation 中;并且指定要重载的类,例如图中的 SomeClass。
ClassFileTransformer 接口实现类中的 transform 方法中,使用 ASM / Javassist 框架对字节码进行修改、增强
使用 $JAVA_HOME/lib/tools.jar 中的 VirtualMachine 类 attach 到运行的 JVM 上,然后通过 VirtualMachine 加载 Agent.jar 替换指定的类。
我的 gitee 中有完整的实现代码,感兴趣的读者可以参考。
[1] 调研字节码插桩技术,用于系统监控设计和实现[2] MonitorDesign
历史文章推荐
版权声明: 本文为 InfoQ 作者【Samson】的原创文章。
原文链接:【http://xie.infoq.cn/article/dde7d50da8d07833fd324ee51】。文章转载请联系作者。
评论