Java Agent是一种Java方式用于修改现有Java应用程序类的机制。Java Agent通过Java虚拟机(JVM)启动时运行的预定义类的帮助,可以动态注入代码到应用程序的ClassLoader中,从而以运行时方式改变应用程序的行为,例如:收集应用程序的性能数据、记录调试日志等。
以下是使用Java Agent的步骤:
步骤一:创建Java Agent
- 创建一个Maven项目,添加
javaagent
依赖:
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
</dependency>
- 创建一个代理类,并实现
premain
方法:
public class Agent {
public static void premain(String agentArgs, Instrumentation instrumentation) {
// 在这里编写代理类的逻辑代码
}
}
- 导出可执行的Jar包:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>Agent</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
- 打包可执行的Jar包
在命令行窗口中,使用以下命令进行打包:
$ mvn clean package
步骤二:使用Java Agent
使用Java Agent需要在应用程序启动时增加一个参数-javaagent:<agent-jar-path> [args]
。其中,agent-jar-path
表示Java Agent可执行Jar包的绝对路径,args
为可选的代理程序参数。
例如,运行如下命令:
$ java -javaagent:agent.jar -jar app.jar
这将会在启动应用程序时加载Java Agent中的Agent
类,并执行其中的premain
方法。
示例1:性能数据收集
以下是一个简单的Java Agent,用于收集应用程序中所有方法的执行时间:
import java.lang.instrument.Instrumentation;
public class Agent {
public static void premain(String agentArgs, Instrumentation instrumentation) {
instrumentation.addTransformer(new TimeTransformer());
}
}
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class TimeTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.startsWith("com/example/")) {
return TimeWeaver.weave(className, classfileBuffer);
}
return classfileBuffer;
}
}
import java.io.IOException;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class TimeWeaver {
private static Map<String, Long> methodTimeMap = new ConcurrentHashMap<>();
public static byte[] weave(String className, byte[] classBytes) {
ClassDefinition definition = new ClassDefinition(getClassFromBytes(classBytes),
classBytes);
Instrumentation instrumentation = Agent.getInstrumentation();
try {
instrumentation.redefineClasses(new ClassDefinition[]{definition});
} catch (Throwable throwable) {
timeMethod(className, classBytes);
}
return classBytes;
}
private static Class<?> getClassFromBytes(byte[] classBytes) {
return new ByteClassLoader().defineClass(null, classBytes, 0, classBytes.length);
}
private static void timeMethod(String className, byte[] classBytes) {
long start = System.currentTimeMillis();
getClassFromBytes(classBytes);
long end = System.currentTimeMillis();
long usedTime = end - start;
methodTimeMap.put(className, usedTime);
System.out.println(className + ": " + usedTime + "ms");
}
}
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.security.ProtectionDomain;
public class ByteClassLoader extends ClassLoader {
public Class<?> defineClass(String name, byte[] b) {
return defineClass(name, b, 0, b.length);
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException();
}
}
该代理类使用了Instrumentation.addTransformer
方法,该方法添加了一个转换器TimeTransformer
,用于拦截应用程序中所有的类。
TimeTransformer
类中的transform
方法是必须实现的方法,并用于返回字节数组,这个字节数组表示被修改后的字节码,应用程序就会以修改后的字节码运行。在这个转换器中,我们只修改包名为“com/example”的类。
TimeWeaver
类是一个工具类,主要用于重新定义类。当redefineClasses
方法失败时,timeMethod
方法将被调用,通过计算字节码的加载时间,该方法会输出一个日志来记录每个类的加载时间。
示例2:方法调用轨迹记录
以下是一个Java Agent的示例代码,用于记录应用程序中每个方法的调用堆栈:
import java.lang.instrument.Instrumentation;
public class Agent {
public static void premain(String agentArgs, Instrumentation instrumentation) {
instrumentation.addTransformer(new StackTraceTransformer());
}
}
import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class StackTraceTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classFileBuffer)
throws IllegalClassFormatException {
if (!className.startsWith("java") && !className.startsWith("sun")
&& !className.startsWith("javax") && !className.startsWith("com/sun")
&& !className.startsWith("com/oracle") && !className.startsWith("com/example")) {
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get(classBeingRedefined.getName());
CtMethod[] methods = cc.getDeclaredMethods();
for (CtMethod method : methods) {
method.insertBefore("System.err.println(\"Method " + method.getName() + " called at: \" + new java.util.Date());" +
"Thread.dumpStack();");
}
classFileBuffer = cc.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}
return classFileBuffer;
}
}
该代理类使用了 Instrumentation.addTransformer
来注册一个 ClassFileTransformer
。StackTraceTransformer
类实现了 ClassFileTransformer
接口, 中的一个方法transform
,该方法将会被 Java 虚拟机调用,可以修改类的字节码。
在 transform
方法中,我们使用了 Javassist 库(一个 Java 字节码操作库),在每个方法的开头插入了两行代码,记录了方法的名称和调用栈。该示例仅记录名字不以 java,sun,javax,com/sun 和 com/oracle 开头的方法。
使用这个 Java Agent 去 change 项目中的字节码,然后生成一个文件,我们可以看到每个方法的调用栈:
Method m() called at: Tue Jan 18 17:54:56 UTC 2022
java.lang.Thread.dumpStack(Thread.java:1387)
com.example.demo.change.ChangeImpl.m(ChangeImpl.java:7)
com.example.demo.Main.main(Main.java:20)
Method i() called at: Tue Jan 18 17:54:56 UTC 2022
java.lang.Thread.dumpStack(Thread.java:1387)
com.example.demo.change.ChangeImpl.i(ChangeImpl.java:11)
com.example.demo.Main.main(Main.java:21)
总结
Java Agent是Java应用程序修改现有类的机制,它可以通过JVM将代码注入到应用程序的ClassLoader中,以运行时方式来改变应用程序的行为。 本文讲解了Java Agent的基本使用步骤,并给出了两个简单的示例:收集应用程序中的性能数据和记录每个方法的调用堆栈。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Java Agent的作用是什么? - Python技术站