如何實(shí)現(xiàn)IDEA 的 debug?
不點(diǎn)藍(lán)字關(guān)注,我們哪來故事?
初學(xué) Java 時(shí),我對 IDEA 的 Debug 非常好奇,不止是它能查看斷點(diǎn)的上下文環(huán)境,更神奇的是我可以在斷點(diǎn)處使用它的 Evaluate 功能直接執(zhí)行某些命令,進(jìn)行一些計(jì)算或改變當(dāng)前變量。
剛開始語法不熟經(jīng)常寫錯(cuò)代碼,重新打包部署一次代碼耗時(shí)很長,我就直接面向 Debug 開發(fā)。在要編寫的方法開始處打一個(gè)斷點(diǎn),在 Evaluate 框內(nèi)一次次地執(zhí)行方法函數(shù)不停地調(diào)整代碼,沒問題后再將代碼復(fù)制出來放到 IDEA 里,再進(jìn)行下一個(gè)方法的編寫,這樣就跟寫 PHP 類似的解釋性語言一樣,寫完即執(zhí)行,非常方便。

但 Java 是靜態(tài)語言,運(yùn)行之前是要先進(jìn)行編譯的,難道我寫的這些代碼是被實(shí)時(shí)編譯又”注入”到我正在 Debug 的服務(wù)里了嗎?
隨著對 Java 的愈加熟悉,我也了解了反射、字節(jié)碼等技術(shù),直到前些天的周會分享,有位同事分享了 Btrace 的使用和實(shí)現(xiàn),提到了 Java 的 ASM 框架和 JVM TI 接口。Btrace 修改代碼能力的實(shí)現(xiàn)與 Debug 的 Evaluate 有很多相似之處,這大大吸引了我。
分享就像一個(gè)引子,從中學(xué)到的東西只是皮毛,要了解它還是要自己研究。于是自己查看資料并寫代碼學(xué)習(xí)了下其具體實(shí)現(xiàn)。
ASM
實(shí)現(xiàn) Evaluate 要解決的第一個(gè)問題就是怎么改變原有代碼的行為,它的實(shí)現(xiàn)在 Java 里被稱為動態(tài)字節(jié)碼技術(shù)。
動態(tài)生成字節(jié)碼
我們知道,我們編寫的 Java 代碼都是要被編譯成字節(jié)碼后才能放到 JVM 里執(zhí)行的,而字節(jié)碼一旦被加載到虛擬機(jī)中,就可以被解釋執(zhí)行。
字節(jié)碼文件(.class)就是普通的二進(jìn)制文件,它是通過 Java 編譯器生成的。而只要是文件就可以被改變,如果我們用特定的規(guī)則解析了原有的字節(jié)碼文件,對它進(jìn)行修改或者干脆重新定義,這不就可以改變代碼行為了么。
Java 生態(tài)里有很多可以動態(tài)生成字節(jié)碼的技術(shù),像 BCEL、Javassist、ASM、CGLib 等,它們各有自己的優(yōu)勢。有的使用復(fù)雜卻功能強(qiáng)大、有的簡單確也性能些差。
ASM 框架
ASM 是它們中最強(qiáng)大的一個(gè),使用它可以動態(tài)修改類、方法,甚至可以重新定義類,連 CGLib 底層都是用 ASM 實(shí)現(xiàn)的。
當(dāng)然,它的使用門檻也很高,使用它需要對 Java 的字節(jié)碼文件有所了解,熟悉 JVM 的編譯指令。雖然我對 JVM 的字節(jié)碼語法不熟,但有大神開發(fā)了可以在 IDEA 里查看字節(jié)碼的插件:ASM Bytecode Outline?,在要查看的類文件里右鍵選擇?Show bytecode Outline?即可以右側(cè)的工具欄查看我們要生成的字節(jié)碼。對照著示例,我們就可以很輕松地寫出操作字節(jié)碼的 Java 代碼了。
而切到?ASMified?標(biāo)簽欄,我們甚至可以直接獲取到 ASM 的使用代碼。

常用方法
在 ASM 的代碼實(shí)現(xiàn)里,最明顯的就是訪問者模式,ASM 將對代碼的讀取和操作都包裝成一個(gè)訪問者,在解析 JVM 加載到的字節(jié)碼時(shí)調(diào)用。
ClassReader 是 ASM 代碼的入口,通過它解析二進(jìn)制字節(jié)碼,實(shí)例化時(shí)它時(shí),我們需要傳入一個(gè) ClassVisitor,在這個(gè) Visitor 里,我們可以實(shí)現(xiàn)?visitMethod()/visitAnnotation()?等方法,用以定義對類結(jié)構(gòu)(如方法、字段、注解)的訪問方法。
而 ClassWriter 接口繼承了 ClassVisitor 接口,我們在實(shí)例化類訪問器時(shí),將 ClassWriter “注入” 到里面,以實(shí)現(xiàn)對類寫入的聲明。
Instrument
instrument。使用
ClassFileTransformer?接口定義一個(gè)類文件轉(zhuǎn)換器。它唯一的一個(gè)?transform()?方法會在類文件被加載時(shí)調(diào)用,在 transform 方法里,我們可以對傳入的二進(jìn)制字節(jié)碼進(jìn)行改寫或替換,生成新的字節(jié)碼數(shù)組后返回,JVM 會使用 transform 方法返回的字節(jié)碼數(shù)據(jù)進(jìn)行類的加載。JVM TI
介紹
Agent
-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:3333,而 -agentlib 選項(xiàng)就指定了我們要加載的 Java Agent,jdwp 是 agent 的名字,在 linux 系統(tǒng)中,我們可以在 jre 目錄下找到 jdwp.so 庫文件。jdi->jdwp->jvmti,我們通過 JDI 接口發(fā)送調(diào)試指令,而 jdwp 就相當(dāng)于一個(gè)通道,幫我們翻譯 JDI 指令到 JVM TI,最底層的 JVM TI 最終實(shí)現(xiàn)對 JVM 的操作。使用
premain()?或?agentmain()?方法來實(shí)現(xiàn)。而要實(shí)現(xiàn) 1.6 以上的動態(tài) instrument 功能,實(shí)現(xiàn) agentmain 方法即可。Instrumentation.retransformClasses()?方法實(shí)現(xiàn)對目標(biāo)類的重定義。VirtualMachine?類提供了 attach 一個(gè)本地 JVM 的功能,它需要我們傳入一個(gè)本地 JVM 的 pid, tools.jar 可以在 jre 目錄下找到。agent生成
MANIFEST.MF?文件的一些參數(shù),允許我們重新定義類。如果你的 agent 實(shí)現(xiàn)還需要引用一些其他類庫時(shí),還需要將這些類庫都打包到此 jar 包中,下面是我的 pom 文件配置。<build>
????????<plugins>
????????????<plugin>
????????????????<groupId>org.apache.maven.pluginsgroupId>
????????????????<artifactId>maven-assembly-pluginartifactId>
????????????????<configuration>
????????????????????<archive>
????????????????????????<manifestEntries>
????????????????????????????<Agent-Class>asm.TestAgentAgent-Class>
????????????????????????????<Can-Redefine-Classes>trueCan-Redefine-Classes>
????????????????????????????<Can-Retransform-Classes>trueCan-Retransform-Classes>
????????????????????????????<Manifest-Version>1.0Manifest-Version>
????????????????????????????<Permissions>all-permissionsPermissions>
????????????????????????manifestEntries>
????????????????????archive>
????????????????????<descriptorRefs>
????????????????????????<descriptorRef>jar-with-dependenciesdescriptorRef>
????????????????????descriptorRefs>
????????????????configuration>
????????????plugin>
????????plugins>
????build>
mvn assembly:assembl?命令生成 jar-with-dependencies 作為 agent。代碼實(shí)現(xiàn)
被修改的類
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
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.visitMaxs(2, 0);
????????????????mv.visitEnd();
????????????????TransformTarget.printSomething();
????????????}
????????????return?mv;
????????}
????}
}
Attacher
public?class?Attacher?{
????public?static?void?main(String[] args)?throws?AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
?
????????VirtualMachine vm = VirtualMachine.attach("34242"); // 目標(biāo) JVM pid
????????vm.loadAgent("/path/to/agent.jar");
????}
}

小結(jié)

推薦



