1.简介
本文将讨论Java Instrumentation API。Instrumentation API由JVM提供用来修改已加载类的工具,可以提供Java语言编写的插桩功能,动态修改运行时代码的能力。
此外本文还会介绍如何开发Java agent,通过Java agent来动态增强系统功能。
2.JPDA介绍
JPDA(Java platform debugger architecture)定义了一整套完整的调试体系,它将调试体系分为三部分,并规定了三者之间的通信接口[1]。三部分由低到高分别是Java 虚拟机工具接口(JVMTI),Java 调试协议(JDWP)以及 Java 调试接口(JDI),三者之间的关系如下图所示:
正式通过这三层结构提供的能力,给我们提供了调试功能。利用JVMTI提供的能力,可以实现对JVM的多种操作,它通过接口注册各种事件勾子,在JVM事件触发时,同时触发预定义的勾子,以实现对各个JVM事件的响应,事件包括类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、GC开始和结束、方法调用进入和退出、临界区竞争与等待、VM启动与退出等等。
关于JPDA和JVMTI的详细介绍可以进一步阅读参考资料1。
3.什么是Java Agent
简单来说,Java agent是一个我们通过使用JVM提供的Instrumentation API来开发出来的jar包,可以用来修改已加载到JVM中的字节码文件。
一个agent要工作,需要定义两个方法:
- premain: 在JVM启动时,通过指定-javaagent参数来静态加载agent
- agentmain: 如果希望在运行时加载一个agent,那么我们就会用到agentmain方法,最后使用Java Attach API动态加载Agent,并执行agentmain方法,实现运行时对字节码文件的修改。
接下来,我们首先将看一下如何使用已有的Java agent。随后,如何从零开始如何动态在创建功能并增加到字节码中。
4. 通过Java agent增强代码
在介绍Java agent增强代码之前首先介绍业务背景,随后对介绍利用agent来增强业务代码。
4.1 业务代码
我们定义一个简单的业务类,打印一段信息。后续我们利用Agent来给代码动态增加功能,实现增强。
public class MyAtm {
public static void withdrawMoney(int amount) throws InterruptedException {
//processing going on here
Thread.sleep(2000L);
System.out.println(String.format("[Application] Successful Withdrawal of [%s] units!",amount));
}
}
4.2 InstrumentationAPI简介
为了开发Agent会用到Instrumentation API,几个核心API如下[2]:
public interface Instrumentation {
//增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
//在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
void addTransformer(ClassFileTransformer transformer);
//删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);
boolean isRetransformClassesSupported();
//在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
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);
}
关于开发Agent规范,可以进一步阅读参考资料2。
4.3 开发Agent
根据4.2中引用的参考,我们开发一个Agent,对4.1中的代码增加。增强的能力为,在方法前后增加开始和结束时间,从而记录这段代码执行的耗时。
首先我们定义一个ClassFileTransformer实现,该实现的主要功能是通过javassist来修改字节码文件。
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class AtmTransformer implements ClassFileTransformer{
private String targetClassName;
private ClassLoader targetClassLoader;
private static final String WITHDRAW_MONEY_METHOD = "withdrawMoney";
public AtmTransformer(String targetClassName,ClassLoader targetClassLoader) {
this.targetClassName = targetClassName;
this.targetClassLoader = targetClassLoader;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
byte[] byteCode = classfileBuffer;
String finalTargetClassName = this.targetClassName.replaceAll("\\.", "/");
if (!className.equals(finalTargetClassName)) {
return byteCode;
}
if (className.equals(finalTargetClassName) && loader.equals(targetClassLoader)) {
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get(targetClassName);
CtMethod m = cc.getDeclaredMethod(WITHDRAW_MONEY_METHOD);
m.addLocalVariable("startTime",CtClass.longType);
m.insertBefore("{startTime=System.currentTimeMillis();}");
StringBuilder endBlock = new StringBuilder();
m.addLocalVariable("endTime",CtClass.longType);
m.addLocalVariable("opTime",CtClass.longType);
endBlock.append("endTime = System.currentTimeMillis();");
endBlock.append("opTime = (endTime-startTime)/1000;");
endBlock.append("System.out.println(\"[Application] Withdrawal operation completed in:\" + opTime + \" seconds!\");");
m.insertAfter(endBlock.toString());
byteCode = cc.toBytecode();
cc.detach();
} catch (Throwable e) {
System.out.println("Exception" + e);
}
}
return byteCode;
}
}
随后我们定义premain和agentmain方法,当通过不同的方法加载agent后会执行这些方法。这些方法在启动后,会添加一个转换器,当业务类被加载后,会注册AtmTransformer类,最终完成对原有功能的增加。
package com.baeldung.instrumentation.agent;
import java.lang.instrument.Instrumentation;
public class MyInstrumentationAgentV {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("[Agent] In premain method");
String className = "com.baeldung.instrumentation.application.MyAtm";
transformClass(className, inst);
}
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("[Agent] In agentmain method");
String className = "com.baeldung.instrumentation.application.MyAtm";
transformClass(className, inst);
}
private static void transformClass(String className, Instrumentation instrumentation) {
Class<?> targetCls = null;
ClassLoader targetClassLoader = null;
try {
targetCls = Class.forName(className);
targetClassLoader = targetCls.getClassLoader();
transform(targetCls, targetClassLoader, instrumentation);
return;
} catch (Exception ex) {
System.err.println("Class [{}] not found with Class.forName");
}
// otherwise iterate all loaded classes and find what we want
for(Class<?> clazz: instrumentation.getAllLoadedClasses()) {
if(clazz.getName().equals(className)) {
targetCls = clazz;
targetClassLoader = targetCls.getClassLoader();
transform(targetCls, targetClassLoader, instrumentation);
return;
}
}
throw new RuntimeException("Failed to find class [" + className + "]");
}
private static void transform(Class<?> clazz, ClassLoader classLoader, Instrumentation instrumentation) {
AtmTransformer dt = new AtmTransformer(clazz.getName(), classLoader);
instrumentation.addTransformer(dt, true);
try {
instrumentation.retransformClasses(clazz);
} catch (Exception ex) {
throw new RuntimeException("Transform failed for class: [" + clazz.getName() + "]", ex);
}
}
}
4.4 加载Agent
agent有两种方法加载,静态和动态。静态在系统启动时候通过指定-javaagent:agent.jar来加载。而动态方法通过attach api在运行时来加载agent。
4.4.1 静态加载
定义一个业务执行的主方法,在启动时,通过命令或idea中的ide中的JVM参数来启动agent。
-javaagent:xx/xx/agent-1.0.0-jar-with-dependencies.jar
public class Launcher {
public static void main(String[] args) throws Exception {
MyAtmApplication.run();
}
}
public class MyAtmApplication {
public static void run() throws Exception {
System.out.println("[Application] Starting ATM application");
MyAtm.withdrawMoney(10);
TimeUnit.SECONDS.sleep(2);
MyAtm.withdrawMoney(10);
}
}
当启动系统后,premain方法会优先执行,随后执行我们的main方法。
接着会看到增加代码的输出执行时间记录,[Application] Withdrawal operation completed in:2 seconds!
[Agent] In premain method
[Application] Starting ATM application
[Application] Successful Withdrawal of [10] units!
[Application] Withdrawal operation completed in:2 seconds!
[Application] Successful Withdrawal of [10] units!
[Application] Withdrawal operation completed in:2 seconds!
4.4.2 动态加载
在运行时,我们找到目标JVM,然后将agent绑定到对应的JVM上,实现对目标系统的增加。这里用到一个AgentLoader类,完成目标JVM的发现和Agent的加载。
import com.sun.tools.attach.VirtualMachine;
import java.io.File;
import java.util.Optional;
public class AgentLoader {
public static void run() {
String agentFilePath = "xxx/xxx/agent-1.0.0-jar-with-dependencies.jar";
String applicationName = "Launcher";
Optional<String> jvmProcessOpt = Optional.ofNullable(VirtualMachine.list()
.stream()
.filter(jvm -> {
System.out.println(String.format("jvm:%s", jvm.displayName()));
return jvm.displayName().endsWith(applicationName);
})
.findFirst().get().id());
if(!jvmProcessOpt.isPresent()) {
System.out.println("Target Application not found");
return;
}
File agentFile = new File(agentFilePath);
try {
String jvmPid = jvmProcessOpt.get();
System.out.println("Attaching to target JVM with PID: " + jvmPid);
VirtualMachine jvm = VirtualMachine.attach(jvmPid);
jvm.loadAgent(agentFile.getAbsolutePath());
jvm.detach();
System.out.println("Attached to target JVM and loaded Java agent successfully");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
随后运行启动方法,同样会输出增强信息:
[Application] Withdrawal operation completed in:2 seconds!
package com.baeldung.instrumentation.application;
public class Launcher {
public static void main(String[] args) throws Exception {
AgentLoader.run();
MyAtmApplication.run();
}
}
5. 遇到的一些问题
- 找不到javassist依赖,NoClassDefFound:这个问题是运行时,找不到工具类依赖,加载后,可以将agent jar工程打为一个fat jar,相关依赖都打好,或者指定好依赖类路径也可以。
- Agent JAR loaded but agent failed to initialize:这个问题是我在通过AgentLoader找目标JVM时,找到了一个错误的JVM导致初始化失败。最开始用的匹配方法是jvm.displayName().contains(applicationName);导致错误。后来改为endsWith(applicationName); 遇到agent初始化错误时,可以简化问题进行排查,先看agent是否正常被加载。这个加日志观察即可。如果都没正常加载肯定是找错JVM了。接着继续缩小问题范围,逐个解决。
6. 总结
通过Agent方法配合字节码增加工具[5]实现运行时对系统代码修改,或者获取JVM信息,实现系统增加,从而可以实现不同的系统能力。如无侵入的在线系统诊断,监控上报。 通过自己完成文章中的demo及处理相关问题,加深对Agent的初步认知。
参考资料
[1]jpda介绍,https://developer.ibm.com/zh/articles/j-lo-jpda1/
[2]java agent学习,https://www.cnblogs.com/rickiyang/p/11368932.html
[3]打包可执行,jarhttps://www.baeldung.com/executable-jar-with-maven
[4]学习demo参考,https://www.baeldung.com/java-instrumentation
[5]字节码增强,https://blog.csdn.net/meituantech/article/details/100570756