在 Java SE 5 以后,使用 Instrumentation,使得开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。
在 Java SE 5 中,利用 java.lang.instrument 做静态 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能从本地代码中解放出来,使之可以用 Java 代码的方式解决问题。
在 Java SE 6 里面,instrumentation 包被赋予了更强大的功能:启动后的 instrument、本地代码(native code)instrument,以及动态改变 classpath 等等。这些改变,意味着 Java 具有了更强的动态控制、解释能力,它使得 Java 语言变得更加灵活多变。
在 Java SE 5 中,Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类。但在实际的运行之中,虚拟机在初始化之时(在绝大多数的 Java 类库被载入之前),instrumentation 的设置已经启动,并在虚拟机中设置了回调函数,检测特定类的加载情况,并完成实际工作。但是在实际的很多的情况下,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了 instrument 的应用。而 Java SE 6 改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的。
Instrumentation 的最大作用,就是类定义动态改变和操作。java.lang.instrument 包的实现,是基于JVMTI机制的:在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成Java 类的动态操作。除开 Instrumentation 功能外,JVMTI 还在虚拟机内存管理,线程控制,方法和变量操作等方面提供了大量有价值的函数。
JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的本地编程接口集合。JVMTI 是从 Java SE 5 开始引入,整合和取代了以前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已经消失了。JVMTI 提供了一套“代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。
1. 使用
Agent分为两种,一种是在主程序之前运行的Agent,一种是在主程序之后运行的Agent(前者的升级版,1.6以后提供)。
- 在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 -javaagent 参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。
- 在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 Java Tool API 中的 attach 方式指定进程id和特定jar包地址,启动 Instrumentation 的代理程序。
这里测试使用 javaagent 替换类。
1.1 JVM启动前静态 Instrument
定义一个User类,getName()返回admin。
public class User {
public String getName() {
return "admin";
}
}
主程序,这里的程序就是我们要代理的程序。创建User对象,调用getName()方法。
public class Main {
public static void main(String[] args) {
System.out.println("main start");
System.out.println("main args :" + Arrays.toString(args));
System.out.println("new User().getName() :" + new User().getName());
System.out.println("main end");
}
}
将User类的getName()方法返回值改为user,使用javac编译后,重命名编译文件User.class为User.class.2,再将User类中的getName()方法返回值改回admin。
实现 ClassFileTransformer 接口,transform 方法则完成了类定义的替换。
public class UserTransformer implements ClassFileTransformer {
// 待替换的类文件名
private static final String USER_CLASS_2 = "User.class.2";
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("className :" + className);
if (!className.contains("User")) {
return null;
}
return getBytesFromFile(USER_CLASS_2);
}
/**
* 根据文件名读入二进制字符流
* @param fileName
* @return
*/
private byte[] getBytesFromFile(String fileName) {
File file = new File(fileName);
try (
InputStream inputStream = new FileInputStream(file);
){
long length = file.length();
byte[] bytes = new byte[(int) length];
int offset = 0;
int numRead = 0;
while (offset < bytes.length &&
(numRead = inputStream.read(bytes, offset, bytes.length - offset)) >= 0) {
offset += numRead;
}
if (offset < bytes.length) {
throw new IOException("无法完全读取文件 " + file.getName());
}
return bytes;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
premain方法,代表着他将在主程序的main方法之前运行,agentArgs代表传递过来的参数,inst则是agent技术主要使用的API。
agentArgs 是 premain 函数得到的程序参数,随同 -javaagent 一起传入。与 main 函数不同的是,这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。
Inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
public class Premain {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("premain start");
System.out.println("premain args :" + agentArgs);
// 在类加载之前,重新定义 Class 文件
inst.addTransformer(new UserTransformer());
System.out.println("premain end");
}
}
META-INF/MAINIFEST.MF 文件用于描述Jar包的信息,例如指定入口函数等。
需要添加Premain-Class属性,指定带有 premain 方法类的全路径,然后将agent类打成Jar包。
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Main-Class>
com.shpun.Main
</Main-Class>
<!--main之前-->
<Premain-Class>
com.shpun.Premain
</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
测试
未代理,打印admin。
将 User.class.2 和 Jar 包放在同个目录下测试。
代理后,先执行 premain 方法,然后加载类,每次都经过 transform() 方法。加载完后执行 main 方法,需要加载 User 类,经过transform() 方法,替换User,打印user,代理成功。
1.2 JVM启动后动态 Instrument
User.class.2,User 类,UserTransformer 类和上面一样。
修改 Main 方法,这里判断 getName() 的值,如果是user才退出。
public class Main {
public static void main(String[] args) throws InterruptedException {
System.out.println("main start");
System.out.println("main args :" + Arrays.toString(args));
while (true) {
Thread.sleep(1000);
String name = new User().getName();
System.out.println("new User().getName() :" + new User().getName());
if ("user".equals(name)) {
break;
}
}
System.out.println("main end");
}
}
agentmain 方法在 main 函数开始运行之后再运行。
public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation inst) throws Exception {
System.out.println("agentmain start");
System.out.println("agentmain args :" + agentArgs);
// 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,
// 参数 canRetransform 设置是否允许重新转换,为true才能在运行时替换。
inst.addTransformer(new UserTransformer(), true);
// 在类加载之后,重新定义 Class。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。
inst.retransformClasses(User.class);
System.out.println("agentmain end");
}
}
META-INF/MAINIFEST.MF 需要添加 Agent-Class 和 Can-Retransform-Classes 属性。
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Main-Class>
com.shpun.Main
</Main-Class>
<!--main之后-->
<Agent-Class>
com.shpun.AgentMain
</Agent-Class>
<Can-Retransform-Classes>
true
</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
测试
使用 agentmain,需要通过 Attach API 。Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM ”附着”(Attach)代理工具程序的。有了它,开发者可以方便的监控一个 JVM,运行一个外加的代理程序。Jar 包在JAVA_HOME的lib目录下。
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>G:\Java\jdk1.8.0_181\lib\tools.jar</systemPath>
</dependency>
VirtualMachine 代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作(Attach 动作的相反行为,从 JVM 上面解除一个代理)等等 。该类允许我们通过给 attach() 方法传入一个jvm的pid(进程id),远程连接到jvm上 。然后我们可以通过 loadAgent() 方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以在 Class 加载前改变 Class 的字节码,也可以在 Class 加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer 接口中提供的方法进行处理。
public class AttachAgent {
public static void main (String[] args) throws Exception {
// 通过jps命令,获取进程号
VirtualMachine virtualMachine = VirtualMachine.attach("19056");
virtualMachine.loadAgent("E:\\IDEA_workspace\\java-agent-test\\java-agent-agentmain\\target\\java-agent-agentmain-1.0-SNAPSHOT.jar", "attach-agent");
virtualMachine.detach();
}
}
将 User.class.2 和 Jar 包放在同个目录下测试。
运行 Jar 包,先打印的是admin。然后执行AttachAgent,发现agentmain方法被执行了,并且在替换了类,打印user。这个表示 agentmain 已经被 Attach API 成功附着到 JVM 上,代理程序生效了。
参考:
Java Agent简介
javaagent使用指南
JavaAgent技术
☆基于Java Instrument的Agent实现
初探 Java agent
☆浅谈JPDA中JVMTI模块
☆JVMTI Agent 工作原理及核心源码分析
☆JVMTI Attach机制与核心源码分析
"程序包com.sun.tools.attach不存在"最简单粗暴的解决方案
agentmain 使用过程中的坑,看看你有没有遇到