Java 自从 JDK 1.5 开始提供了 Instrument 机制,允许使用单独的 agent 获取 JVM 信息、动态修改 class 字节码,可以实现无侵入的运行时 AOP。使用 Java agent 可以在 JVM 启动前(JDK 1.5+)或启动后(JDK 1.6+)修改字节码,实现运行时数据的采集和回放(如 doom),也可以用于实时查看线上运行情况(如 arthas)。
启动前加载 agent 可以在 Java 启动命令中,加入 javaagent 选项来启动代理。命令格式是:
-javaagent:jarpath [=options]
其中,jarpath 是代理 jar 文件的路径,options是传给代理的入参数据。javaagent 命令可以使用多次从而创建多个代理,且多个代理可以使用相同的 jarpath。
用于代理的 jar 文件需要满足如下要求:
premain 方法有两个可用的定义, JVM 首先尝试调用:
public static void premain(String agentArgs, Instrumentation inst);如果该方法未实现,JVM 再次尝试调用:
public static void premain(String agentArgs);
在 options 配置的入参,会通过 agentArgs 参数传入,由开发者自行解析字符串的内容。如果使用 javaagent 命令创建了多个代理,JVM 会依次调用每个代理的 premain 方法,然后才会调用 main 方法。所以 premain 方法必须返回,否则将无法启动。
代理类会使用系统类加载器( ClassLoader.getSystemClassLoader)来加载,因此 premain 会和 main 方法拥有相同的安全和加载器规则。JVM 不限制 premain 的实现,main 方法能做的事情,premain 都可以做。
除了 premain 方法,JVM(1.6 及以后)还支持在启动之后启动代理。用于代理的 jar 文件需要满足如下要求:
agentmain 方法有两个可用的定义, JVM 首先尝试调用:
public static void agentmain(String agentArgs, Instrumentation inst);
如果该方法未实现,JVM 再次尝试调用:
public static void agentmain(String agentArgs);
入参会通过 agentArgs 参数传入,由开发者自行解析字符串的内容。
JVM 启动后启动代理,是通过 VirtualMachine 类来实现,该类位于 com.sun.tools.attach 包中,提供了 JVM 相关的操作方法,代理相关的主要是以下方法:
demo 代码如下,首先获取运行中的 JVM 列表,然后 attach 到 JVM,调用 loadAgent 方法让 JVM 启动指定的代理类。
Listlist = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list) { VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("/home/admin/agent.jar","agentArgs"); }
manifest 文件中可以同时存在 Premain-Class 属性和 Agent-Class 属性。使用 -javaagent 选项在命令行上启动代理后,Premain-Class 属性生效,Agent-Class 属性会被忽略。如果在启动 JVM 后启动代理,则 Agent-Class 属性生效,Premain-Class 属性会被忽略。
manifest 文件除指定 agent 类外,还有其他可选选项可以配置,如下:
无论是 premain 还是 agentmain,JVM 都可以通过入参传入 Instrumentation 实例。Instrumentation 接口定义在 java.lang.instrument 包中,该包中还有 ClassFileTransformer 接口。ClassFileTransformer 接口只有一个方法 transform,用于转换 class 字节码。Instrumentation 接口提供了以下方法:
借助 Instrument 机制,我们可以获取 JVM 加载的所有类,对目标类进行修改并重新生成字节码文件,实现对目标的增强。
inst.addTransformer(new ClassFileTransformer(){ @Override public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { // 转换 class 返回 byte[] 字节码内容 return new byte[0]; } }, true); try { inst.retransformClasses(inst.getAllLoadedClasses()); } catch (Exception e) { e.printStackTrace(); }
修改字节码的技术比较多,比如 ASM、Javassist、BCEL、CGLib 等。创建 Demo 类,预期在代码前后插入增强代码。
package com.chengxuzhixin; public class Demo { public void test(){ // 预期前面插入 打印 start System.out.println("test"); // 预期后面插入 打印 end } }
ASM 是在指令层次上操作字节码。字节码结构比较稳定,很适合使用 ASM 访问者模式进行修改。ASM 提供了 ClassReader 可以读取已经编译好的 class 文件,通过 ClassVisitor、MethodVisitor、FieldVisitor 等各种类型的 Visitor 修改字节码,然后用 ClassWriter 重新生成字节码。这样的重写方式需要开发者对字节码非常了解,要熟悉一系列 visitXXXInsn 方法,可以使用社区工具 ASM ByteCode Outline 来帮助生成 visitXXXInsn 方法。
// 创建 ClassReader ClassReader reader = new ClassReader("com.chengxuzhixin.Demo"); // 创建 ClassWriter ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); // 创建 ClassVisitor 并传入 reader.accept(new ClassAdapter(classWriter){ @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); if (name.equals("test")) { // 只处理 test 方法 mv = new MethodAdapter(mv){ @Override public void visitCode() { super.visitCode(); // 前面插入代码 mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("asm start"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V"); } @Override public void visitInsn(int opcode) { if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) { // 后面插入代码 mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("asm end"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V"); } super.visitInsn(opcode); } }; } return mv; } }, ClassReader.SKIP_DEBUG); // Java agent 获取字节码数据 return classWriter.toByteArray();
Javassist 可以直接用 Java 编码来实现增强,无需关注字节码结构,比 ASM 更简单。Javassist 中核心的类主要有四个:
基于这四个类,可以方便地实现增强,比如要在指定方法前后增加代码,如下所示:
// 获取默认 ClassPool ClassPool cp = ClassPool.getDefault(); // 找到 CtClass,重写 com.chengxuzhixin.Demo CtClass cc = cp.get("com.chengxuzhixin.Demo"); // 增强方法 test CtMethod m = cc.getDeclaredMethod("test"); // 前面插入代码 m.insertBefore("{ System.out.println(\"javassist start\"); }"); // 后面插入代码 m.insertAfter("{ System.out.println(\"javassist end\"); }"); // Java agent 获取字节码数据 return cc.toBytecode();
本文来源:程序之心,转载请注明出处!
最新内容
© 2016 - 2024 chengxuzhixin.com All Rights Reserved.