Java的插装(Instrumentation)是指通过字节码操作对Java类加载过程进行监控和动态修改的技术。简单来说,插装可以在Java应用运行时对已经编译好的字节码进行修改,以实现某些额外的功能,而不需要修改原始源代码。这种技术经常用于性能监控、代码覆盖率分析、日志记录、安全检查等场景。
下面介绍一下一些出名的使用了插装技术概念
应用
性能监控和分析:
Arthas:Arthas是一个强大的Java诊断工具,非常适合在线上问题排查中使用。
日志
Apache SkyWalking 和 Zipkin:这些是分布式追踪系统,通过插装技术在方法调用前后插入代码,以收集分布式系统中的调用链信息。
JRebel:通过插装技术实现类和资源的热部署,允许开发者在运行时修改代码并立即查看效果,而不需要重新启动应用。
这些工具和库利用插装技术,极大地扩展了Java应用的能力和灵活性,使得开发者能够在不修改源代码的情况下实现各种高级功能。
区别
动态代理:主要用于方法级别的拦截和增强,特别适合接口代理。它更容易实现和使用,限制较少,但其功能相对简单。
插装技术:可以对整个类的字节码进行复杂的修改,适用于需要低层次控制和广泛应用场景(如性能监控、代码覆盖率、安全性等)。插装技术更为复杂,但也更为强大。
使用
Java提供了一个名为java.lang.instrument
的包,它包含了Java插装的API。要使用插装功能,通常需要创建一个Java代理(agent),它能够在JVM启动时或运行时被加载。Java代理主要分为两类:
启动代理(Startup Agent):在JVM启动时通过命令行参数指定,通过
-javaagent:your_agent_jar_path
来启动。这种类型的代理可以在类加载前修改字节码。运行时代理(Runtime Agent):可以在JVM运行时动态附加到进程上。这种类型的代理主要通过
Attach API
实现。
具体案例
1. 编写Java代理类
首先编写一个Java代理类(Agent),它包含一个premain
方法,用于在JVM启动时进行字节码插装。
import java.lang.instrument.Instrumentation;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import org.objectweb.asm.*;
public class PerformanceMonitorAgent {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className.equals("mypackage/MyService")) {
return modifyClass(classfileBuffer);
}
return classfileBuffer;
}
});
}
private static byte[] modifyClass(byte[] classfileBuffer) {
ClassReader classReader = new ClassReader(classfileBuffer);
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);
classReader.accept(new ClassVisitor(Opcodes.ASM9, classWriter) {
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MethodVisitor(Opcodes.ASM9, methodVisitor) {
public void visitCode() {
methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "currentTimeMillis", "()J");
methodVisitor.visitVarInsn(Opcodes.LSTORE, 1);
super.visitCode();
}
public void visitInsn(int opcode) {
if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "currentTimeMillis", "()J");
methodVisitor.visitVarInsn(Opcodes.LSTORE, 3);
methodVisitor.visitVarInsn(Opcodes.LLOAD, 3);
methodVisitor.visitVarInsn(Opcodes.LLOAD, 1);
methodVisitor.visitInsn(Opcodes.LSUB);
methodVisitor.visitVarInsn(Opcodes.LSTORE, 5);
methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitVarInsn(Opcodes.LLOAD, 5);
methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);
}
super.visitInsn(opcode);
}
};
}
}, 0);
return classWriter.toByteArray();
}
}
2. 目标类
这是一个示例目标类,会被我们的代理进行字节码修改。
package mypackage;
public class MyService {
public void myMethod() {
System.out.println("Executing myMethod...");
try {
Thread.sleep(100); // Simulating some work
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3. 创建代理JAR包
接下来,我们需要创建一个包含代理类的JAR包,并在MANIFEST.MF文件中指定代理类。
MANIFEST.MF:
Manifest-Version: 1.0
Premain-Class: PerformanceMonitorAgent
生成JAR文件:
jar cvfm agent.jar MANIFEST.MF PerformanceMonitorAgent.class
4. 使用代理执行程序
最后,我们在运行时指定使用这个代理JAR包。
java -javaagent:agent.jar -cp your-classpath mypackage.MyService