Java Agent
- 参考文章 JVM Tool Interface (JVM TI)
- Oracle
- java.lang.instrument API
- Attach API
- OpenJDK
Java Agent = bytecode instrumentation
Java Agent 一个可以修改字节码的机会(需要熟悉 `java.lang.instrument` 相关的 API)
------------------
Java ASM 操作字节码的类库(常用的字节码的类库有:ASM、ByteBuddy 和 Javassist)
------------------
ClassFile 理论基础
Instrumentation can be inserted in one of three ways :
java.lang.instrument
- instrumentation 可以加载修改类中字节码的方法
- agent jar 的功能实际上是借助于 Instrumentation 对象完成的
规范定义
-
manifest 中 定义 Premain-Class Agent-Class 属性
-
根据 定义的 class 执行对应的
premain和agentmain方法 -
执行时机
premain是在 JVM 启动时执行的(主要在启动时的代理)。agentmain是在动态附加时执行的(即运行时通过 attach 动态附加的代理)。
-
Manifest
=指定=>Agent Class=获取=>Instrumentation=修改字节码=>ClassFileTransformer
Agent Jar
Java Agent 对应的 .jar 文件里,有三个主要组成部分:
- Manifest
- META-INF/MANIFEST.MF
- Premain-Class
- JVM 加载时,调用
Premain-Class属性中指定的 类 的premain方法
- JVM 加载时,调用
- Agent-Class
- JVM 启动之后,通过 Attach API 动态附加 Java Agent 时执行的。这个方法通常用于代理在 JVM 启动后动态附加到运行中的 JVM 实例
Can-Redefine-Classes- class 是否可被 agent 重新定义;可选,默认为 false
Can-Retransform-Classes:- class 是否可被 agent 重新转换;可选,默认为 false
Can-Set-Native-Method-Prefix- 是否可以设置本地方法前缀;可选,默认为 false
- Boot-Class-Path
- 指定由引导类
bootstrap class loader加载器加载的类路径
- 指定由引导类
- Launcher-Agent-Class
- 在应用程序的
main方法执行之前插入代理代码,通常用于做一些应用级的初始化工作。
- 在应用程序的
- Premain-Class
- META-INF/MANIFEST.MF
- Agent Class
- LoadTimeAgent.class: premain
- DynamicAgent.class: agentmain
- ClassFileTransformer
- ASMTransformer.class
LoadTimeAgent VS DynamicAgent
-
涉及虚拟机数量
- Load-Time Instrumentation 只涉及到一个虚拟机
- Dynamic Instrumentation 涉及到两个虚拟机
-
执行时机
-
Load-Time Instrumentation,会执行 Agent Jar 当中的
premain()方法;premain()方法是先于main()方法执行,此时 Application 当中使用的大多数类还没有被加载。 -
Dynamic Instrumentation,会执行 Agent Jar 当中的
agentmain()方法;而agentmain()方法是往往是在main()方法之后执行,此时 Application 当中使用的大多数类已经被加载。
-
-
能力
- Load-Time Instrumentation 可以做很多事情:添加和删除字段、添加和删除方法等。
- Dynamic Instrumentation 做的事情比较有限,大多集中在对于方法体的修改。
-
线程不同
- Load-Time Instrumentation 是运行在
main线程 - Dynamic Instrumentation 是运行在
Attach Listener线程
- Load-Time Instrumentation 是运行在
-
Exception 处理
- Load-Time Instrumentation 时,出现异常,会报告错误信息,并且停止执行,退出虚拟机。
- Dynamic Instrumentation 时,出现异常,会报告错误信息,但是不会停止虚拟机,而是继续执行。
命令行写法
- 定义
option-text格式,并解析为key-valueusername:tomcat,password:123456 username=tomcat,password=123456
类介绍
ClassDefinition
- 封装了
Class和byte[]两个字段 - mClass 位于堆区,关联了 方法区的 mClassFile 字节数组
- 当想要 重新 定义某个类时,可以根据 ClassDefinition 找到方法区的字节数组
package java.lang.instrument;
public final class ClassDefinition {
private final Class<?> mClass;
private final byte[] mClassFile;
}
ClassFileTransformer
An agent provides an implementation of this interface in order to transform class files. The transformation occurs before the class is defined by the JVM.
public interface ClassFileTransformer {
byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
}
- ClassLoader loader:加载当前类的类加载器, 如果值为
null,则表示 bootstrap loader - String className:internal class name(java/lang/String)当前类的名字,也是你要修改的类名称。
Class<?> classBeingRedefined:如果该类正在被重新定义(例如在热部署中),这个参数会包含正在被重新定义的类的信息。如果是新加载的类,这个参数为null。- ProtectionDomain protectionDomain:类的保护域,表示该类的安全限制,比如权限控制等。
- byte[] classfileBuffer:表示原始的字节码数据,也是你需要修改的字节数组。
- byte[] :返回修改后的字节码。如果不进行任何修改,可以直接返回原始的字节码 或者 null。如果进行了修改,返回一个新的字节码数组,这将作为最终加载到JVM中的字节码。
Instrumentation
public interface Instrumentation {
// 读取 `META-INF/MANIFEST.MF` 文件中的属性信息
boolean isRedefineClassesSupported();
boolean isRetransformClassesSupported();
boolean isNativeMethodPrefixSupported();
// 添加和删除 `ClassFileTransformer`:
void addTransformer(ClassFileTransformer transformer);
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
boolean removeTransformer(ClassFileTransformer transformer);
// 重新定义Class字节码
void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException, UnmodifiableClassException;
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
// 获取内存中已经加载的类,已经初始化的类
Class[] getAllLoadedClasses();
Class[] getInitiatedClasses(ClassLoader loader);
boolean isModifiableClass(Class<?> theClass);
// 获取对象大小
long getObjectSize(Object objectToSize);
// native 也可通过 Instrumentation 修改native方法
boolean isNativeMethodPrefixSupported();
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
// java 9 引入的
boolean isModifiableModule(Module module);
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);
}
redefine and retransform
There are two kinds of transformers, determined by the canRetransform parameter of Instrumentation.addTransformer(ClassFileTransformer,boolean):
- retransformation capable transformers that were added with
canRetransformastrue - retransformation incapable transformers that were added with
canRetransformasfalseor where added withInstrumentation.addTransformer(ClassFileTransformer)
- 两者都是对已经加载的类(already loaded classes)进行修改。
- 处理方式不同:替换(redefine)和修改(retransform)
- 方法的调用时机有 3 种
- 类加载的时候
- 调用
Instrumentation.redefineClasses方法的时候 - 调用
Instrumentation.retransformClasses方法的时候
- 对于正在加载的类进行修改,属于 define。
- 对于已经加载的类进行修改,它属于 redefine 和 retransform 的范围。
对于已经加载的类(loaded class),redefine 侧重于以“新”换“旧”,而 retransform 侧重于对“旧”的事物进行“修补”。
| define | redefine | retransform | |
|---|---|---|---|
| Interface Add | OK | NO | NO |
| Field Add | OK | NO | NO |
| Method Add | OK | NO | NO |
| Method Remove | OK | NO | NO |
| Method Body Modify | OK | OK | OK |
- load,是类在加载的过程当中,JVM 内部机制来自动触发。
- redefine 和 retransform,是我们自己写代码触发。
手动打包
LoadTimeAgent
-
编译
javac -d out -sourcepath src $(find src -name "*.java")
-
将 manifest 文件添加到编译路径
cp src/main/manifest.txt out
-
打包
jar -cvfm TestAgent.jar out/manifest.txt -C out com/dawn/agentjar cvfm TestAgent.jar:创建一个 JAR 文件并指定输出文件名 TestAgent.jar。c 是创建 JAR 文件,f 是指定文件名, m 是 mainfest 文件 -C out:表示切换到 out 目录,在该目录下执行后续的打包操作。-C 选项是 jar 命令的一个选项,用来改变工作目录。 com/dawn/agent:表示只打包 com.dawn.agent 包以及其子包中的 .class 文件
打印加载的类信息
Dynamic Agent
借助 JDK 内置的 ASM 打印出方法接收的参数,使用 Dynamic Instrumentation 的方式实现。
manifest.txt
Agent-Class: com.dawn.agent.DynamicAgent
Can-Retransform-Classes: true
VMAttach.java
VMAttach 获取 VM 列表,根据条件 attach 到对应进程
package com.dawn.agent.attach;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class VMAttach {
/**
* 需要 tools.jar
*/
public static void main(String[] args) throws Exception {
String agent = "TestAgent.jar";
System.out.println("Agent Path: " + agent);
List<VirtualMachineDescriptor> vmds = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : vmds) {
if (vmd.displayName().equals("com.dawn.application.Program")) {
VirtualMachine vm = VirtualMachine.attach(vmd.id());
System.out.println("Load Agent");
vm.loadAgent(agent);
System.out.println("Detach");
vm.detach();
}
}
}
}
DynamicAgent.java
package com.dawn.agent;
import com.dawn.agent.instrument.ASMTransformer;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
public class DynamicAgent {
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("Agent-Class: " + DynamicAgent.class.getName());
ClassFileTransformer transformer = new ASMTransformer();
try {
inst.addTransformer(transformer, true);
Class<?> targetClass = Class.forName("com.dawn.application.HelloWorld");
inst.retransformClasses(targetClass);
} catch (Exception ex) {
ex.printStackTrace();
}
finally {
inst.removeTransformer(transformer);
}
}
}
编译:
-
VMAttach:编译需要tools.jar
javac -XDignore.symbol.file -encoding UTF-8 -cp "${JAVA_HOME}\lib\tools.jar;." -d out -sourcepath src $(find src -name "*.java")
-
将 manifest 文件添加到编译路径
cp src/main/manifest.txt out
打包:
$ jar -cvfm TestAgent.jar out/manifest.txt -C out com/dawn/agent
运行:
-
运行Program
$ java -cp out com.dawn.application.Program -
运行VMAttach
$ java -cp "C:\soft\Programs\java\jdk1.8.0_441/lib/tools.jar;out" com.dawn.agent.attach.VMAttach -
运行结果
Maven 打包
使用插件
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<configuration>
<minimizeJar>true</minimizeJar>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>application/*</exclude>
<exclude>run/*</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>com.dawn.application.HelloProgram</Main-Class>
<Premain-Class>com.dawn.agent.LoadTimeAgent</Premain-Class>
<Agent-Class>com.dawn.agent.DynamicAgent</Agent-Class>
<Launcher-Agent-Class>com.dawn.agent.LauncherAgent</Launcher-Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
</manifestEntries>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>