在前文中,介绍了Instrumentation中的premain功能, 这次再一起看下它的agentmain功能.
一. agentmain简介
与premain不同的是, agentmain是JVM 利用attach机制,在运行时添加动态代理的方式, 完成字节码的修改.
1.1
agentmain函数
agentmain函数与premain类似, 也有两种写法; 其他参数以及方法优先级也是与premain类似的.
代码语言:javascript复制// 方式1
public static void agentmain(String agentArgs, Instrumentation inst);
// 方式2
public static void agentmain(String agentArgs);
1.2
MANIFEST.MF
与premain类似,打jar包时,也需要一个元文件; 元数据文件目录(maven工程):
代码语言:javascript复制/src/main/resources/META-INF/MANIFEST.MF
文件内容:
代码语言:javascript复制Manifest-Version: 1.0
Agent-Class: com.instrument.agentmain.MyAgentmain
Can-Redefine-Classes: true
Can-Retransform-Classes: true
1.3
jar包生成
打jar包方式以及操作都与premain类似, 这里就不赘述了.
二. Hello World
一起看个示例, 了解下agentmain是如何在运行时, 动态增强的. 我们的目标是在不重启的情况下, 打印出abc()方法的运行时间.
2.1
Agentmain类
agentmain类中的参数会传入类全名和方法名, 并通过TimingTransformer类增加方法功能
代码语言:javascript复制package com.instrument.agentmain;
import java.lang.instrument.Instrumentation;
public class MyAgentmain {
public static void agentmain(String agentArgs, Instrumentation inst)
throws ClassNotFoundException, UnmodifiableClassException {
System.out.println("Hi, I'm agentmain agent! 2:" agentArgs);
Class[] classes = inst.getAllLoadedClasses();
System.out.println("length:" classes.length);
String cname=agentArgs.split(",")[0];
Class<?> targetClass = Thread.currentThread().getContextClassLoader().loadClass(cname);
String methodName=agentArgs.split(",")[1];
inst.addTransformer(new TimingTransformer(cname,methodName),true);
inst.retransformClasses(targetClass);
}
}
2.2
TimingTransformer类
TimingTransformer是解析对应的类和方法, 并利用javassist对原方法进行改造增加, 添加时间统计逻辑.
代码语言:javascript复制package com.instrument.agentmain;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
public class TimingTransformer implements ClassFileTransformer {
private String cname;
private String methodName;
public TimingTransformer(String cname, String methodName) {
this.cname = cname;
this.methodName = methodName;
System.out.println("method in timing:" cname "," methodName);
}
public byte[] transform(ClassLoader loader, String className, Class cBR,
java.security.ProtectionDomain pD, byte[] classfileBuffer) {
try {
System.out.println("className:" className);
if (!className.equals(cname)) {
return null;
}
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(cname);
CtMethod[] methods = cc.getMethods();
for (int i = 0; i < methods.length; i ) {
CtMethod method = methods[i];
if (method.getName().equals(methodName)) {
System.out.println("->className:" className ",method:" method.getName());
// add time
addTimer(method);
}
}
return cc.toBytecode();
} catch (NotFoundException | CannotCompileException | IOException e) {
e.printStackTrace();
System.exit(0);
}
return null; // No transformation required
}
private void addTimer(CtMethod method) throws CannotCompileException {
method.addLocalVariable("startTime", CtClass.longType);
method.insertBefore("long startTime = System.nanoTime();");
method.insertAfter("System.out.println("time:" (System.nanoTime() - startTime));");
// long startTime = System.nanoTime();
// System.out.println("time:" (System.nanoTime() - startTime));
}
}
2.3
pom依赖
引入pom依赖, 再对agentmain打成jar包, 我们的hack类就准备完毕了.
代码语言:javascript复制<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>
2.4
目标类
TestAgentmain是我们要在运行中动态代理的目标类, 需要注意的是程序本身不需要javassist.jar, 但为了配合agentmain使用也是需要引入该pom依赖的.
程序启动运行后, 会打印出进程PID, 后面的VM动态增强时,使用.
代码语言:javascript复制import java.lang.management.ManagementFactory;
public class TestAgentmain {
public static void main(String[] args) {
// 进程PID信息
String name = ManagementFactory.getRuntimeMXBean().getName();
System.out.println(name);
while (true) {
new Thread(new WaitThread()).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class WaitThread implements Runnable {
@Override
public void run() {
abc();
}
public void abc() {
System.out.println("abc in method");
}
}
2.4
TestAgentmain执行结果之一
执行结果, 其中64250为该程序的进程ID. 程序每隔1s会打印日志语句.
代码语言:javascript复制64250@MacBook-Pro.local
abc in method
abc in method
abc in method
abc in method
abc in method
2.5
添加VM attach接口
这里需要用到上面TestAgentmain运行的进程ID:64250;
同时制定agentmin运行的参数"WaitThread,abc", 代码要修改WaitThread类中的abc()方法.
代码语言:javascript复制public class VirtualMachineTest {
public static void main(String[] args) throws AttachNotSupportedException, IOException,
AgentLoadException, AgentInitializationException, InterruptedException {
// attach to target VM PID
VirtualMachine vm = VirtualMachine.attach("64250");
vm.loadAgent("/xx/instrument/agent/target/agent-1.0-SNAPSHOT.jar", "WaitThread,abc");
Thread.sleep(1000);
System.out.println("attach end");
vm.detach();
}
}
2.6
TestAgentmain执行结果之二
再次观察TestAgentmain执行结果, 可以发现能够输出方法的执行时间了. 说明程序已经被动态增强了.
代码语言:javascript复制abc in method
abc in method
Hi, I'm agentmain agent! 2:WaitThread,abc
length:729
method in timing:WaitThread,abc
className:WaitThread
->className:WaitThread,method:abc
abc in method
time:201286
abc in method
time:129545
abc in method
time:162443
2.7
代码结构
小结
premain和agentmain两种方式最终的目的都是为了回调Instrumentation实例并激活sun.instrument.InstrumentationImpl#transform()(InstrumentationImpl是Instrumentation的实现类)从而回调注册到Instrumentation中的ClassFileTransformer实现字节码修改, 本质功能上没有很大区别. 两者的非本质功能的区别如下:
1. premain需要通过命令行使用外部代理jar包, 即-javaagent:代理jar包路径;agentmain则可以通过attach机制直接附着到目标VM中加载代理, 也就是使用agentmain方式下, 操作attach的程序和被代理的程序可以是完全不同的两个程序.
2. premain方式回调到ClassFileTransformer中的类是虚拟机加载的所有类, 这个是由于代理加载的顺序比较靠前决定的, 在开发者逻辑看来就是: 所有类首次加载并且进入程序main()方法之前, premain方法会被激活, 然后所有被加载的类都会执行ClassFileTransformer列表中的回调.
3. agentmain方式由于是采用attach机制, 被代理的目标程序VM有可能很早之前已经启动, 当然其所有类已经被加载完成, 这个时候需要借助Instrumentation#retransformClasses(Class<?>...classes)让对应的类可以重新转换, 从而激活重新转换的类执行ClassFileTransformer列表中的回调.
4. 通过premain方式的代理Jar包进行了更新的话, 需要重启服务器, 而agentmain方式的Jar包如果进行了更新的话, 需要重新attach, 但是agentmain重新attach还会导致重复的字节码插入问题, 不过也有Hotswap和DCE VM方式来避免.