字节码调试的入口 —— JVM 的寄生插件 javaAgent 那些事 ...

莱莱  金牌会员 | 2023-8-28 12:09:25 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 908|帖子 908|积分 2724


Java Instrumentation 包

Java Instrumentation 概述

Java Instrumentation 这个技术看起来非常神秘,很少有书会详细介绍。但是有很多工具是基于 Instrumentation 来实现的:

  • APM 产品: pinpoint、skywalking、newrelic、听云的 APM 产品等都基于 Instrumentation 实现
  • 热部署工具:Intellij idea 的 HotSwap、Jrebel 等
  • Java 诊断工具:Arthas、Btrace 等
由于对字节码修改功能的巨大需求,JDK 从 JDK5 版本开始引入了java.lang.instrument 包。它可以通过 addTransformer 方法设置一个 ClassFileTransformer,可以在这个 ClassFileTransformer 实现类的转换。
JDK 1.5 支持静态 Instrumentation,基本的思路是在 JVM 启动的时候添加一个代理(javaagent),每个代理是一个 jar 包,其 MANIFEST.MF 文件里指定了代理类,这个代理类包含一个 premain 方法。JVM 在类加载时候会先执行代理类的 premain 方法,再执行 Java 程序本身的 main 方法,这就是 premain 名字的来源。在 premain 方法中可以对加载前的 class 文件进行修改。这种机制可以认为是虚拟机级别的 AOP,无需对原有应用做任何修改,就可以实现类的动态修改和增强。
从 JDK 1.6 开始支持更加强大的动态 Instrument,在JVM 启动后通过 Attach API 远程加载,后面会详细介绍。
本文会分为 javaagent 和动态 Attach 两个部分来介绍
Java Instrumentation 核心方法

Instrumentation 是 java.lang.instrument 包下的一个接口,这个接口的方法提供了注册类文件转换器、获取所有已加载的类等功能,允许我们在对已加载和未加载的类进行修改,实现 AOP、性能监控等功能。
常用的方法如下:
  1. /**
  2. * 为 Instrumentation 注册一个类文件转换器,可以修改读取类文件字节码
  3. */
  4. void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
  5. /**
  6. * 对JVM已经加载的类重新触发类加载
  7. */
  8. void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
  9. /**
  10. * 获取当前 JVM 加载的所有类对象
  11. */
  12. Class[] getAllLoadedClasses()
复制代码
它的 addTransformer 给 Instrumentation 注册一个 transformer,transformer 是 ClassFileTransformer 接口的实例,这个接口就只有一个 transform 方法,调用 addTransformer 设置 transformer 以后,后续JVM 加载所有类之前都会被这个 transform 方法拦截,这个方法接收原类文件的字节数组,返回转换过的字节数组,在这个方法中可以做任意的类文件改写。
下面是一个空的 ClassFileTransformer 的实现:
  1. public class MyClassTransformer implements ClassFileTransformer {
  2.     @Override
  3.     public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classBytes) throws IllegalClassFormatException {
  4.         // 在这里读取、转换类文件
  5.         return classBytes;
  6.     }
  7. }
复制代码
接下来我们来介绍本文的主角之一 javaagent。
Javaagent 介绍

Javaagent 是一个特殊的 jar 包,它并不能单独启动的,而必须依附于一个 JVM 进程,可以看作是 JVM 的一个寄生插件,使用 Instrumentation 的 API 用来读取和改写当前 JVM 的类文件。
Agent 的两种使用方式

它有两种使用方式:

  • 在 JVM 启动的时候加载,通过 javaagent 启动参数 java -javaagent:myagent.jar MyMain,这种方式在程序 main 方法执行之前执行 agent 中的 premain 方法
  • 在 JVM 启动后 Attach,通过 Attach API 进行加载,这种方式会在 agent 加载以后执行 agentmain 方法 premain 和 agentmain 方法签名如下:
  1. public static void premain(String agentArgument, Instrumentation instrumentation) throws Exception
  2. public static void agentmain(String agentArgument, Instrumentation instrumentation) throws Exception
复制代码
这两个方法都有两个参数

  • 第一个 agentArgument 是 agent 的启动参数,可以在 JVM 启动命令行中设置,比如java -javaagent:=appId:agent-demo,agentType:singleJar test.jar的情况下 agentArgument 的值为 "appId:agent-demo,agentType:singleJar"。
  • 第二个 instrumentation 是 java.lang.instrument.Instrumentation 的实例,可以通过 addTransformer 方法设置一个 ClassFileTransformer。
第一种 premain 方式的加载时序如下:
Agent 打包

为了能够以 javaagent 的方式运行 premain 和 agentmain 方法,我们需要将其打包成 jar 包,并在其中的 MANIFEST.MF 配置文件中,指定 Premain-class 等信息,一个典型的生成好的 MANIFEST.MF 内容如下
  1. 为了能够以 javaagent 的方式运行 premain 和 agentmain 方法,我们需要将其打包成 jar 包,并在其中的 MANIFEST.MF 配置文件中,指定 Premain-class 等信息,一个典型的生成好的 MANIFEST.MF 内容如下
复制代码
下面是一个可以帮助生成上面 MANIFEST.MF 的 maven 配置
  1. <build>
  2.   <finalName>my-javaagent</finalName>
  3.   <plugins>
  4.     <plugin>
  5.       <groupId>org.apache.maven.plugins</groupId>
  6.       <artifactId>maven-jar-plugin</artifactId>
  7.       <configuration>
  8.         <archive>
  9.           <manifestEntries>
  10.             <Agent-Class>me.geek01.javaagent.AgentMain</Agent-Class>
  11.             <Premain-Class>me.geek01.javaagent.AgentMain</Premain-Class>
  12.             <Can-Redefine-Classes>true</Can-Redefine-Classes>
  13.             <Can-Retransform-Classes>true</Can-Retransform-Classes>
  14.           </manifestEntries>
  15.         </archive>
  16.       </configuration>
  17.     </plugin>
  18.   </plugins>
  19. </build>
复制代码
Agent 使用方式一:JVM 启动参数

下面使用 javaagent 实现简单的函数调用栈跟踪,以下面的代码为例:
  1. public class MyTest {
  2.     public static void main(String[] args) {
  3.         new MyTest().foo();
  4.     }
  5.     public void foo() {
  6.         bar1();
  7.         bar2();
  8.     }
  9.     public void bar1() {
  10.     }
  11.     public void bar2() {
  12.     }
  13. }
复制代码
通过 javaagent 启动参数的方式在每个函数进入和结束时都打印一行日志,实现调用过程的追踪的效果。
核心的方法 instrument 的逻辑如下:
[code]public static class MyMethodVisitor extends AdviceAdapter {    @Override    protected void onMethodEnter() {        // 在方法开始处插入

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

莱莱

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表