Instrumentation 是 Java SE 5 引入的一套 API,它允许开发者在运行时修改类的字节码。Java Instrumentation 可以实现在方法插入额外的字节码从而达到收集使用中的数据到指定工具的目的。Java.lang.instrument包的最大功能就是可以在已有的类上附加(修改)字节码来实现增强的逻辑,它最常见的用途包括:
Instrumentation 通常与 Java Agent 一起使用。Java Agent 是一种特殊的 Java 应用,它会在目标应用启动时被 JVM 加载。Agent 通过实现 java.lang.instrument.Instrumentation 接口,来对字节码进行修改或增强。
Instrumentation 的底层核心在于 JVM Tool Interface (JVMTI)。JVMTI 是 JVM 提供的一组 native 方法,算是JVM暴露出来的一些供用户扩展的接口集合,它允许外部工具与 JVM 进行交互(基于事件驱动,JVM指定到每一层逻辑层都会调用事件的回调接口)。通过 JVMTI,我们可以实现对 JVM 的监控、调试和修改。Instrumentation 就是利用 JVMTI 来实现对字节码的动态修改。
在Java 1.5开始引入Instrument增加技术时,最常用的一种使用方式是通过JVM启动参数:-javaagent来启动,这实际上是一种静态的代理。这种静态的agent只能在jar包启动时候进行代理,存在较大的局限性。Java 1.6开始引入了动态的Attach方式,可以在JVM启动之后的任意时刻通过Attach API远程加载Agent的jar,比如阿里开源的arthas工具就是基于Attach API实现的。
Instrumentation 的实现依赖以下 JVM 的特性和机制:
Instrumentation 的工作流程大致如下:
我这里使用的是JDK 21版本,Instrumentation类包含在java.instrument模块中。
package java.lang.instrument;
import java.security.ProtectionDomain;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarFile;
public interface Instrumentation {
/**
* JDK1.6引入的方法,注册一个ClassFileTransformer,用于在类加载时对字节码进行转换。
* 当类加载器加载某个类时,JVM 会调用注册的 ClassFileTransformer 的 transform 方法,允许我们对字节码进行修改。
*/
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void addTransformer(ClassFileTransformer transformer);
boolean removeTransformer(ClassFileTransformer transformer);
boolean isRetransformClassesSupported();
/**
* 重新转换已经加载的类,允许我们对已经加载到 JVM 中的类进行再次转换,实现热部署等功能。
* 这个方法是JDK 1.6引入的,这个方法很经常会被使用到。
*/
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
boolean isRedefineClassesSupported();
/**
* 使用新的字节码重新定义一组类。经常使用在动态加载类,或类替换
*/
void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;
boolean isModifiableClass(Class<?> theClass);
/**
* 获取所有已加载的类。
*/
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
/**
* 获取由当前类加载器初始化的类。
*/
@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);
long getObjectSize(Object objectToSize);
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
void appendToSystemClassLoaderSearch(JarFile jarfile);
boolean isNativeMethodPrefixSupported();
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
/**
* 在 Java 9 中,为了更好地支持模块化系统,Instrumentation 接口新增了一个重要方法:
* redefineModule。这个方法允许我们在运行时对已加载的模块进行重新定义。
*/
void redefineModule(Module module,
Set<Module> extraReads,
Map<String, Set<Module>> extraExports,
Map<String, Set<Module>> extraOpens,
Set<Class<?>> extraUses,
Map<Class<?>, List<Class<?>>> extraProvides);
boolean isModifiableModule(Module module);
}
大致了解了其实现原理后,我们来初步使用下。Java Instrumentation通常有2种方式可以加载Java Agent:
package org.example.instrument.static_load;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class MyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("---- begin load agent ----:Static Agent loaded with args: " + agentArgs);
// 添加 Transformer
inst.addTransformer(new ExampleTransformer());
}
// 定义一个字节码转换器
static class ExampleTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className.equals("org/example/instrument/static_load/MyDemo")) {
System.out.println("---- Transforming class: " + className);
// 这里可以修改字节码,示例中直接返回原字节码
return classfileBuffer;
}
return classfileBuffer;
}
}
}
集成maven,我这里使用的是maven3.9.9版本。其中maven-jar-plugin使用的是3.4.2版本,注意maven插件不同,配置属性项也不同,需要根据版本按需修改。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>org.example.instrument.static_load.MyAgent</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build><build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>org.example.instrument.static_load.MyAgent</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
public static void main(String[] args) {
System.out.println("hello static agent");
}
-javaagent:E:\idea_projects\java-agent-demo\target\java-agent-demo-1.0-SNAPSHOT.jar=agentArgs1
这里的java-agent-demo-1.0-SNAPSHOT.jar是上面maven package出来的包路径和名称,这里没有做修改直接拿来测试使用,agentArgs1为传入agent的参数。
运行效果如下:
和静态agent大同小异,只是入口方法premain改为了agentmain。
package org.example.instrument.dynamic_load;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class MyDynamicAgent {
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("---- begin load agent ----:Static Agent loaded with args: " + agentArgs);
// 添加 Transformer
inst.addTransformer(new ExampleTransformer(), true);
}
// 定义一个字节码转换器
static class ExampleTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className.equals("org/example/instrument/dynamic_load/MyDynamicDemo")) {
System.out.println("---- Transforming class: " + className);
// 这里可以修改字节码,示例中直接返回原字节码
return classfileBuffer;
}
return classfileBuffer;
}
}
}
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Agent-Class>org.example.instrument.dynamic_load.MyDynamicAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin><plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Agent-Class>org.example.instrument.dynamic_load.MyDynamicAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
package org.example.instrument.dynamic_load;
import com.sun.tools.attach.VirtualMachine;
public class MyDynamicDemo {
public static void main(String[] args) {
try {
// 目标 JVM 的进程 ID(使用 jps 命令获取)。这里812进程号,是我另起了一个java进程的id
String targetJvmPid = "812";
// Attach 到目标 JVM
VirtualMachine vm = VirtualMachine.attach(targetJvmPid);
// 加载动态 Agent
vm.loadAgent("E:\\idea_projects\\java-agent-demo\\target\\java-agent-demo-1.0-SNAPSHOT.jar", "dynamicAgentArgs1231");
// Detach
vm.detach();
System.out.println("--- Agent attached successfully.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
直接运行MyDynamicDemo类,由于这里使用API,动态的将agent附加到另一个JVM进程上,因此无需像静态agent一样指定-javaagent参数。
agent附加成功:
目标JVM上打印:
由于我这里使用的是JDK21版本,21版本已经将jdk.attach做为单独模块,因此开始很容易遇到找不到jdk.attach包。需要绑定jdk.attach模块。
至此,我们已经初步认识了 Instrumentation,他是 JVM 提供的一个强大工具,通过它可以实现动态字节码修改、性能监控、热部署等功能。通过合理使用 Instrumentation,开发者可以大幅提升系统的动态性与可维护性。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。