初学 Java 时,我对 IDEA 的 Debug 很是好奇,不止是它能查看断点的上下文环境,更神奇的是我能够在断点处使用它的 Evaluate 功能直接执行某些命令,进行一些计算或改变当前变量。html
刚开始语法不熟常常写错代码,从新打包部署一次代码耗时很长,我就直接面向 Debug 开发。在要编写的方法开始处打一个断点,在 Evaluate 框内一次次地执行方法函数不停地调整代码,没问题后再将代码复制出来放到 IDEA 里,再进行下一个方法的编写,这样就跟写 PHP 相似的解释性语言同样,写完即执行,很是方便。java
但 Java 是静态语言,运行以前是要先进行编译的,难道我写的这些代码是被实时编译又”注入”到我正在 Debug 的服务里了吗?linux
随着对 Java 的越发熟悉,我也了解了反射、字节码等技术,直到前些天的周会分享,有位同事分享了 Btrace 的使用和实现,提到了 Java 的 ASM 框架和 JVM TI 接口。 Btrace 修改代码能力的实现与 Debug 的 Evaluate 有不少类似之处,这大大吸引了我。分享就像一个引子,从中学到的东西只是皮毛,要了解它仍是要本身研究。因而本身查看资料并写代码学习了下其具体实现。git
转载随意,文章会持续修订,请注明来源地址:https://zhenbianshu.github.io 。github
实现 Evaluate 要解决的第一个问题就是怎么改变原有代码的行为,它的实如今 Java 里被称为动态字节码技术。apache
咱们知道,咱们编写的 Java 代码都是要被编译成字节码后才能放到 JVM 里执行的,而字节码一旦被加载到虚拟机中,就能够被解释执行。api
字节码文件(.class)就是普通的二进制文件,它是经过 Java 编译器生成的。而只要是文件就能够被改变,若是咱们用特定的规则解析了原有的字节码文件,对它进行修改或者干脆从新定义,这不就能够改变代码行为了么。数组
Java 生态里有不少能够动态生成字节码的技术,像 BCEL、Javassist、ASM、CGLib 等,它们各有本身的优点。有的使用复杂却功能强大、有的简单确也性能些差。oracle
ASM 是它们中最强大的一个,使用它能够动态修改类、方法,甚至能够从新定义类,连 CGLib 底层都是用 ASM 实现的。框架
固然,它的使用门槛也很高,使用它须要对 Java 的字节码文件有所了解,熟悉 JVM 的编译指令。虽然我对 JVM 的字节码语法不熟,但有大神开发了能够在 IDEA 里查看字节码的插件:ASM Bytecode Outline
,在要查看的类文件里右键选择 Show bytecode Outline
便可以右侧的工具栏查看咱们要生成的字节码。对照着示例,咱们就能够很轻松地写出操做字节码的 Java 代码了。
而切到 ASMified
标签栏,咱们甚至能够直接获取到 ASM 的使用代码。
在 ASM 的代码实现里,最明显的就是访问者模式,ASM 将对代码的读取和操做都包装成一个访问者,在解析 JVM 加载到的字节码时调用。
ClassReader 是 ASM 代码的入口,经过它解析二进制字节码,实例化时它时,咱们须要传入一个 ClassVisitor,在这个 Visitor 里,咱们能够实现 visitMethod()/visitAnnotation()
等方法,用以定义对类结构(如方法、字段、注解)的访问方法。
而 ClassWriter 接口继承了 ClassVisitor 接口,咱们在实例化类访问器时,将 ClassWriter “注入” 到里面,以实现对类写入的声明。
字节码是修改完了,但是 JVM 在执行时会使用本身的类加载器加载字节码文件,加载后并不会理会咱们作出的修改,要想实现对现有类的修改,咱们还须要搭配 Java 的另外一个库 instrument
。
instrument 是 JVM 提供的一个能够修改已加载类文件的类库。1.6之前,instrument 只能在 JVM 刚启动开始加载类时生效,以后,instrument 更是支持了在运行时对类定义的修改。
要使用 instrument 的类修改功能,咱们须要实现它的 ClassFileTransformer
接口定义一个类文件转换器。它惟一的一个 transform()
方法会在类文件被加载时调用,在 transform 方法里,咱们能够对传入的二进制字节码进行改写或替换,生成新的字节码数组后返回,JVM 会使用 transform 方法返回的字节码数据进行类的加载。
定义完了字节码的修改和重定义方法,但咱们怎么才能让 JVM 可以调用咱们提供的类转换器呢?这里又要介绍到 JVM TI 了。
JVM TI(JVM Tool Interface)JVM 工具接口是 JVM 提供的一个很是强大的对 JVM 操做的工具接口,经过这个接口,咱们能够实现对 JVM 多种组件的操做,从JVMTM Tool Interface 这里咱们认识到 JVM TI 的强大,它包括了对虚拟机堆内存、类、线程等各个方面的管理接口。
JVM TI 经过事件机制,经过接口注册各类事件勾子,在 JVM 事件触发时同时触发预约义的勾子,以实现对各个 JVM 事件的感知和反应。
Agent 是 JVM TI 实现的一种方式。咱们在编译 C 项目里连接静态库,将静态库的功能注入到项目里,从而才能够在项目里引用库里的函数。咱们能够将 agent 类比为 C 里的静态库,咱们也能够用 C 或 C++ 来实现,将其编译为 dll 或 so 文件,在启动 JVM 时启动。
这时再来思考 Debug 的实现,咱们在启动被 Debug 的 JVM 时,必须添加参数 -agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:3333
,而 -agentlib 选项就指定了咱们要加载的 Java Agent,jdwp 是 agent 的名字,在 linux 系统中,咱们能够在 jre 目录下找到 jdwp.so 库文件。
Java 的调试体系 jdpa 组成,从高到低分别为 jdi->jdwp->jvmti
,咱们经过 JDI 接口发送调试指令,而 jdwp 就至关于一个通道,帮咱们翻译 JDI 指令到 JVM TI,最底层的 JVM TI 最终实现对 JVM 的操做。
JVM TI 的 agent 使用很简单,在启动 agent 时添加 -agent 参数指定咱们要加载的 agent jar包便可。
而要实现代码的修改,咱们须要实现一个 instrument agent,它能够经过在一个类里添加 premain()
或 agentmain()
方法来实现。而要实现 1.6 以上的动态 instrument 功能,实现 agentmain 方法便可。
在 agentmain 方法里,咱们调用 Instrumentation.retransformClasses()
方法实现对目标类的重定义。
另外往一个正在运行的 JVM 里动态添加 agent,还须要用到 JVM 的 attach 功能,Sun 公司的 tools.jar 包里包含的 VirtualMachine
类提供了 attach 一个本地 JVM 的功能,它须要咱们传入一个本地 JVM 的 pid, tools.jar 能够在 jre 目录下找到。
另外,咱们还须要注意 agent 的打包,它须要指定一个 Agent-Class 参数指定咱们的包括 agentmain 方法的类,能够算是指定入口类吧。
此外,还须要配置 MANIFEST.MF
文件的一些参数,容许咱们从新定义类。若是你的 agent 实现还须要引用一些其余类库时,还须要将这些类库都打包到此 jar 包中,下面是个人 pom 文件配置。
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <configuration> <archive> <manifestEntries> <Agent-Class>asm.TestAgent</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> <Manifest-Version>1.0</Manifest-Version> <Permissions>all-permissions</Permissions> </manifestEntries> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> </plugin> </plugins> </build>
另外在打包时须要使用 mvn assembly:assembl
命令生成 jar-with-dependencies 做为 agent。
我在测试时写了一个用以上技术实现了一个简单的字节码动态修改的 Demo。
TransformTarget 是要被修改的目标类,正常执行时,它会三秒输出一次 “hello”。
public class TransformTarget { public static void main(String[] args) { while (true) { try { Thread.sleep(3000L); } catch (Exception e) { break; } printSomething(); } } public static void printSomething() { System.out.println("hello"); } }
Agent 是执行修改类的主体,它使用 ASM 修改 TransformTarget 类的方法,并使用 instrument 包将修改提交给 JVM。
入口类,也是代理的 Agent-Class。
public class TestAgent { public static void agentmain(String args, Instrumentation inst) { inst.addTransformer(new TestTransformer(), true); try { inst.retransformClasses(TransformTarget.class); System.out.println("Agent Load Done."); } catch (Exception e) { System.out.println("agent load failed!"); } } }
执行字节码修改和转换的类。
public class TestTransformer implements ClassFileTransformer { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println("Transforming " + className); ClassReader reader = new ClassReader(classfileBuffer); ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES); ClassVisitor classVisitor = new TestClassVisitor(Opcodes.ASM5, classWriter); reader.accept(classVisitor, ClassReader.SKIP_DEBUG); return classWriter.toByteArray(); } class TestClassVisitor extends ClassVisitor implements Opcodes { TestClassVisitor(int api, ClassVisitor classVisitor) { super(api, classVisitor); } @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("printSomething")) { mv.visitCode(); Label l0 = new Label(); mv.visitLabel(l0); mv.visitLineNumber(19, l0); mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("bytecode replaced!"); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); Label l1 = new Label(); mv.visitLabel(l1); mv.visitLineNumber(20, l1); mv.visitInsn(Opcodes.RETURN); mv