写点什么

Java Core 「8」字节码增强技术

作者:Samson
  • 2022 年 6 月 15 日
  • 本文字数:2567 字

    阅读完需:约 8 分钟

01-动态代理技术

我们都知道,JVM 中加载并运行的是 Java 源文件编译后生成的字节码文件(*.class)。关于字节码文件格式及相关操作的介绍,可以参考美团技术团队的文章[1]。

字节码增强技术是一类对现有字节码文件进行修改或者动态生成新的字节码文件的技术。最常见的基于字节码增强技术的应用是动态代理,以及基于动态代理实现的面向切片编程技术。

padi.tech 中的一张图片很好地描述了字节码增强技术之间的关系:

图 1. 字节码增强相关技术及它们的关系

[1] 字节码增强技术探索

01.1- JDK 动态代理

JDK 中与动态代理相关的类在 java.lang.reflect 包中,主要有以下几个类或接口:

  • Proxy,提供了接口来创建代理类,或者代理类实例

  • InvocationHandler,定义了 invoke 方法,在代理类上调用方法时,会回调到该方法。

基于 Proxy 实现动态代理,被代理的类必须要实现某个接口。这是因为,动态代理类是通过继承 Proxy 类并实现与被代理类同样的接口,来保证与被代理类保持同样的接口。

public final class $Proxy0 extends Proxy implements SomeInterface { ... }复制代码
复制代码


图 2. 基于 JDK Proxy 的动态代理实现基本流程

如上图所示,当使用 JDK Proxy 实现动态代理时,需要将待代理的类(用来获得目标类的类加载器(1)、目标类实现的所有接口列表(2))和 InvocationHandler 接口实现类的实例(3),这三个参数传入到Proxy::newProxyInstance方法中,便会生成动态代理类 $Proxy0 (也可能不是这个名字),该代理类与被代理类均实现同一个接口。因此对客户端来说,具体实现是透明的,接口是一致的。

当客户端调用代理类的方法时,Proxy 中定义的方法会回调 InvocationHandler 接口实现类实例的 invoke 方法。

一个完整的基于 JDK Proxy 的动态代理示例可以在我的 gitee 中找到,有兴趣的读者可以参考一下。

[1] JDK动态代理为什么必须要基于接口?

01.2-CGLIB 动态代理

基于 ASM 的 CGLIB 也是常用来实现动态代理的技术。例如 Spring AOP 即通过 CGLIB 实现动态代理功能。与 JDK Proxy 相比,CGLIB 不要求被代理类必须实现某个接口。

如果要查看 CGLIB 生成的动态代理类的字节码文件,可在程序中追加如下代码:

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\cglib");复制代码
复制代码

运行程序后,可在”D:\cglib”中看到生成的字节码文件。

通过jad -s java *.class分析生成的字节码文件发现,代理类的实现如下:

public class Service$EnhancerByCGLIB$a1718795 extends Service implements Factory {...}复制代码
复制代码

其中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 都比较底层,例如:

mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);复制代码
复制代码

相对来说,使用不太方便,比较麻烦、隐晦。

02.2-Javassist 实现插桩

Javassist 中实现字节码操作主要依赖以下几个类:

  • ClassPool,工具类,提供获取、新建 CtClass 的接口,

  • CtClass,compile-time class,编译时类结构信息,字节码的抽象表示。

  • CtMethod / CtFiled,分别代表方法、域的抽象表示。

我的 gitee 中有完整的使用 Javassist 创建 HelloWorld 类字节码的实例,感兴趣的读者可以参考。

Javassist 强调源代码层次操纵字节码。例如,在方法前后插入代码:

m.insertBefore("{ System.out.println(\"start\"); }");m.insertAfter("{ System.out.println(\"end\"); }");复制代码
复制代码

可以看到,相对于 ASM 来说,对开发者已经相对友好。

02.3-Instrument 替换已加载的字节码

前两节提到的方法,可以对字节码进行修改、增强,但是如果将修改后的代码载入 JVM,则会抛异常。说明单纯的使用 ASM / Javassist 并不能对已加载的类进行修改、增强。

要解决上面的问题,需要用到 JVM 提供的 Instrument 机制。


图 4. Instrument j 机制替换 JVM 中已加载字节码基本流程图

使用 Instrument 替换 JVM 中已加载的类时,需要:

  1. 创建 agent.jar,其中包含 Agent 入口 agentmain 方法和 MANIFEST.MF 文件,指定了 Agent 路径。

    Agent-Class: self.samson.example.aop.ByteEnhancementExamples // Agent 的路径 Can-Redefine-Classes: true Can-Retransform-Classes: true 复制代码

  2. agentmain 方法中,添加 ClassFileTransformer 接口实现类的实例到 Instrumentation 中;并且指定要重载的类,例如图中的 SomeClass。

  3. ClassFileTransformer 接口实现类中的 transform 方法中,使用 ASM / Javassist 框架对字节码进行修改、增强

  4. 使用 $JAVA_HOME/lib/tools.jar 中的 VirtualMachine 类 attach 到运行的 JVM 上,然后通过 VirtualMachine 加载 Agent.jar 替换指定的类。

我的 gitee 中有完整的实现代码,感兴趣的读者可以参考。

[1] 调研字节码插桩技术,用于系统监控设计和实现[2] MonitorDesign


历史文章推荐

Java Core 「7」各种不同类型的锁

Java Core「6」反射与 SPI 机制

Java Core「5」自定义注解编程

Java Core「4」java.util.concurrent 包简介

Java Core「3」volatile 关键字

Java Core「2」synchronized 关键字

Java Core「1」JUC- 线程基础

发布于: 刚刚阅读数: 5
用户头像

Samson

关注

还未添加个人签名 2019.07.22 加入

还未添加个人简介

评论

发布
暂无评论
Java Core 「8」字节码增强技术_学习笔记_Samson_InfoQ写作社区