Agent 是一个运行在目标 JVM 的特定程序,它的职责是负责从目标 JVM 中获取数据,然后将数据传递给外部进程。加载 Agent 的时机可以是目标 JVM 启动之时,也可以是在目标 JVM 运行时进行加载,而在目标 JVM 运行时进行 Agent 加载具备动态性。
基础概念
- JVMTI(JVM Tool Interface):是 JVM 暴露出来的一些供用户扩展的接口集合,JVMTI 是基于事件驱动的,JVM 每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。
- JVMTIAgent(JVM Tool Interface):是一个动态库,利用 JVMTI 暴露出来的一些接口帮助我们在程序启动时或程序运行时 JVM Attach 机制,将 Agent 加载到目标 JVM 中。
- JPLISAgent(Java Programming Language Instrumentation Services Agent):它的作用是初始化所有通过 Java Instrumentation API 编写的 Agent,并且也承担着通过 JVMTI 实现 Java Instrumentation 中暴露 API 的责任。
- VirtualMachine :提供了Attach 动作和 Detach 动作,允许我们通过 attach 方法,远程连接到 JVM 上,然后通过 loadAgent 方法向 JVM 注册一个代理程序 agent ,在该 agent 的代理程序中会得到一个 Instrumentation 实例,该实例可以在 class 加载前改变 class 的字节码,也可以在 class 加载后重新加载。
- Instrumentation:可以在 class 加载前改变 class 的字节码(premain),也可以在 class 加载后重新加载(agentmain)。
实现方式
当前 Java 提供了两种方式可以将代码注入到 JVM 中:
premain:在启动时通过 javaagent 命令,将代理注入到指定的 JVM 中。
agentmain:运行时通过 attach 工具激活指定代理。
premain 只能在类加载之前修改字节码,类加载之后无能为力,只能通过重新创建ClassLoader 这种方式重新加载。而 agentmain 就是为了弥补这种缺点而诞生的。简而言之,agentmain 可以在类加载之后再次加载一个类,也就是重定义,你就可以通过在重定义的时候进行修改类了,甚至不需要创建新的类加载器,JVM 已经在内部对类进行了重定义(重定义的过程相当复杂)。
其中agentmain 还有一个强大的特点,就是目标程序什么都不需要管,就能够被代理。
具体使用
1.定义一个MANIFEST.MF 文件,文件中必须包含 Agent-Class;
Manifest-Version: 1.0 Can-Redefine-Classes: true Agent-Class: com.zy.agent.AgentMain Can-Retransform-Classes: true
2.创建一个 Agent-Class 指定的类,该类必须包含 agentmain 方法 ;
public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException, ClassNotFoundException {
inst.addTransformer(new InterceptorTransformer(agentArgs), true);
System.out.println("Agent Main called");
System.out.println("agentArgs : " + agentArgs);
inst.retransformClasses(PrintParamTargetTest.class);
}
上面代码主要逻辑是添加自定义的转换器,然后打印一些日志参数,最后执行retransformClasses方法。执行了 inst.retransformClasses(PrintParamTargetTest.class); 这段代码的意思是,重新转换目标类,也就是 PrintParamTargetTest类。也就是说,你需要重新定义哪个类,需要指定,否则 JVM 不可能知道。还有一个类似的方法 redefineClasses ,注意,这个方法是在类加载前使用的。类加载后需要使用 retransformClasses 方法。
InterceptorTransformer 拦截类的加载,通过javassist工具对我们的目标类实现增强:
public class InterceptorTransformer implements ClassFileTransformer {
private String agentArgs;
public InterceptorTransformer(String agentArgs) {
this.agentArgs = agentArgs;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//javassist的包名是用点分割的,需要转换下
if (className != null && className.indexOf("/") != -1) {
className = className.replaceAll("/", ".");
}
if (!className.contains("PrintParamTargetTest")) {
return null;
}
try {
//通过包名获取类文件
CtClass cc = ClassPool.getDefault().get(className);
//获得指定方法名的方法
CtMethod m = cc.getDeclaredMethod(agentArgs.split(",")[1]);
//在方法执行前插入代码
m.insertBefore("{ System.out.println(\"=========开始执行=========\"); }");
m.insertAfter("{ System.out.println(\"=========结束执行=========\"); }");
return cc.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
3.是用maven等工具将MANIFEST.MF 和 Agent 类打成 jar 包;
redis-study-1.0-SNAPSHOT.jar
4.将 jar 包载入目标虚拟机。目标虚拟机将会自动执行 agentmain 方法执行方法逻辑,同时,ClassFileTransformer 也会长期有效,在每一个类加载器加载 Class 的时候都会拦截。
public class AttachMain {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
//1.获取系统中所有虚拟机,类似jps命令;
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list)
{
//2.遍历获取PrintParamTargetTest虚拟机;
if (vmd.displayName().endsWith("PrintParamTargetTest")) {
//3.将当前虚拟机连接到目标虚拟机上;
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//4.并在当前虚拟机上加载目标jar包和启动参数
virtualMachine.loadAgent("D:\\java\\redisstudy\\target\\redis-study-1.0-SNAPSHOT.jar", "PrintParamTargetTest,running");
System.out.println("ok");
//5.卸载虚拟机
virtualMachine.detach();
}
}
}
}
注意:写这段代码的时候 IDE 可能提示找不到 jar 包,这时候将 jdk/lib/tools.jar 添加的项目的 classpath 中。
5.执行测试类,循环打印时间,让目标类一直运行以便观察被agent代理增强的效果:
public class PrintParamTargetTest {
public static void main(String[] args) {
// 打印当前进程ID
System.out.println(ManagementFactory.getRuntimeMXBean().getName());
Random random = new Random();
while (true) {
int sleepTime = 5 + random.nextInt(5);
running(sleepTime);
}
}
private static void running(int sleepTime) {
try {
TimeUnit.SECONDS.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("running sleep time " + sleepTime);
}
}
运行结果如下,可以看到在启动代理虚拟机之后,在我们的目标方法通过字节码增强后,输出了前后置处理。