<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          IDEA 的 debug 怎么實(shí)現(xiàn)?出于這個(gè)好奇心,我越挖越深!

          共 14428字,需瀏覽 29分鐘

           ·

          2021-05-26 17:59

          往期熱門(mén)文章:

          1、還在用 Random生成隨機(jī)數(shù)了?試試 ThreadLocalRandom,好用!

          2、從 0 到 1 手把手教你制作酷炫可視化大屏

          3、為什么不建議你用a.equals(b)判斷對(duì)象相等

          4、為什么不推薦使用BeanUtils屬性轉(zhuǎn)換工具

          5、Intellij IDEA 這樣配置注釋模板,讓你瞬間高出一個(gè)逼格!

          來(lái)源 | https://zhenbianshu.github.io/

          對(duì) Debug 的好奇


          初學(xué) Java 時(shí),我對(duì) IDEA 的 Debug 非常好奇,不止是它能查看斷點(diǎn)的上下文環(huán)境,更神奇的是我可以在斷點(diǎn)處使用它的 Evaluate 功能直接執(zhí)行某些命令,進(jìn)行一些計(jì)算或改變當(dāng)前變量。

          剛開(kāi)始語(yǔ)法不熟經(jīng)常寫(xiě)錯(cuò)代碼,重新打包部署一次代碼耗時(shí)很長(zhǎng),我就直接面向 Debug 開(kāi)發(fā)。在要編寫(xiě)的方法開(kāi)始處打一個(gè)斷點(diǎn),在 Evaluate 框內(nèi)一次次地執(zhí)行方法函數(shù)不停地調(diào)整代碼,沒(méi)問(wèn)題后再將代碼復(fù)制出來(lái)放到 IDEA 里,再進(jìn)行下一個(gè)方法的編寫(xiě),這樣就跟寫(xiě) PHP 類(lèi)似的解釋性語(yǔ)言一樣,寫(xiě)完即執(zhí)行,非常方便。

          但 Java 是靜態(tài)語(yǔ)言,運(yùn)行之前是要先進(jìn)行編譯的,難道我寫(xiě)的這些代碼是被實(shí)時(shí)編譯又”注入”到我正在 Debug 的服務(wù)里了嗎?

          隨著對(duì) Java 的愈加熟悉,我也了解了反射、字節(jié)碼等技術(shù),直到前些天的周會(huì)分享,有位同事分享了 Btrace 的使用和實(shí)現(xiàn),提到了 Java 的 ASM 框架和 JVM TI 接口。Btrace 修改代碼能力的實(shí)現(xiàn)與 Debug 的 Evaluate 有很多相似之處,這大大吸引了我。

          分享就像一個(gè)引子,從中學(xué)到的東西只是皮毛,要了解它還是要自己研究。于是自己查看資料并寫(xiě)代碼學(xué)習(xí)了下其具體實(shí)現(xiàn)。

          ASM


          實(shí)現(xiàn) Evaluate 要解決的第一個(gè)問(wèn)題就是怎么改變?cè)写a的行為,它的實(shí)現(xiàn)在 Java 里被稱(chēng)為動(dòng)態(tài)字節(jié)碼技術(shù)。

          動(dòng)態(tài)生成字節(jié)碼

          我們知道,我們編寫(xiě)的 Java 代碼都是要被編譯成字節(jié)碼后才能放到 JVM 里執(zhí)行的,而字節(jié)碼一旦被加載到虛擬機(jī)中,就可以被解釋執(zhí)行。

          字節(jié)碼文件(.class)就是普通的二進(jìn)制文件,它是通過(guò) Java 編譯器生成的。而只要是文件就可以被改變,如果我們用特定的規(guī)則解析了原有的字節(jié)碼文件,對(duì)它進(jìn)行修改或者干脆重新定義,這不就可以改變代碼行為了么。

          Java 生態(tài)里有很多可以動(dòng)態(tài)生成字節(jié)碼的技術(shù),像 BCEL、Javassist、ASM、CGLib 等,它們各有自己的優(yōu)勢(shì)。有的使用復(fù)雜卻功能強(qiáng)大、有的簡(jiǎn)單確也性能些差。

          ASM 框架

          ASM 是它們中最強(qiáng)大的一個(gè),使用它可以動(dòng)態(tài)修改類(lèi)、方法,甚至可以重新定義類(lèi),連 CGLib 底層都是用 ASM 實(shí)現(xiàn)的。

          當(dāng)然,它的使用門(mén)檻也很高,使用它需要對(duì) Java 的字節(jié)碼文件有所了解,熟悉 JVM 的編譯指令。雖然我對(duì) JVM 的字節(jié)碼語(yǔ)法不熟,但有大神開(kāi)發(fā)了可以在 IDEA 里查看字節(jié)碼的插件:ASM Bytecode Outline ,在要查看的類(lèi)文件里右鍵選擇 Show bytecode Outline 即可以右側(cè)的工具欄查看我們要生成的字節(jié)碼。對(duì)照著示例,我們就可以很輕松地寫(xiě)出操作字節(jié)碼的 Java 代碼了。

          而切到 ASMified 標(biāo)簽欄,我們甚至可以直接獲取到 ASM 的使用代碼。

          常用方法

          在 ASM 的代碼實(shí)現(xiàn)里,最明顯的就是訪問(wèn)者模式,ASM 將對(duì)代碼的讀取和操作都包裝成一個(gè)訪問(wèn)者,在解析 JVM 加載到的字節(jié)碼時(shí)調(diào)用。

          ClassReader 是 ASM 代碼的入口,通過(guò)它解析二進(jìn)制字節(jié)碼,實(shí)例化時(shí)它時(shí),我們需要傳入一個(gè) ClassVisitor,在這個(gè) Visitor 里,我們可以實(shí)現(xiàn) visitMethod()/visitAnnotation() 等方法,用以定義對(duì)類(lèi)結(jié)構(gòu)(如方法、字段、注解)的訪問(wèn)方法。

          而 ClassWriter 接口繼承了 ClassVisitor 接口,我們?cè)趯?shí)例化類(lèi)訪問(wèn)器時(shí),將 ClassWriter “注入” 到里面,以實(shí)現(xiàn)對(duì)類(lèi)寫(xiě)入的聲明。

          Instrument


          介紹

          字節(jié)碼是修改完了,可是 JVM 在執(zhí)行時(shí)會(huì)使用自己的類(lèi)加載器加載字節(jié)碼文件,加載后并不會(huì)理會(huì)我們做出的修改,要想實(shí)現(xiàn)對(duì)現(xiàn)有類(lèi)的修改,我們還需要搭配 Java 的另一個(gè)庫(kù) instrument

          instrument 是 JVM 提供的一個(gè)可以修改已加載類(lèi)文件的類(lèi)庫(kù)。1.6以前,instrument 只能在 JVM 剛啟動(dòng)開(kāi)始加載類(lèi)時(shí)生效,之后,instrument 更是支持了在運(yùn)行時(shí)對(duì)類(lèi)定義的修改。

          使用

          要使用 instrument 的類(lèi)修改功能,我們需要實(shí)現(xiàn)它的 ClassFileTransformer 接口定義一個(gè)類(lèi)文件轉(zhuǎn)換器。它唯一的一個(gè) transform() 方法會(huì)在類(lèi)文件被加載時(shí)調(diào)用,在 transform 方法里,我們可以對(duì)傳入的二進(jìn)制字節(jié)碼進(jìn)行改寫(xiě)或替換,生成新的字節(jié)碼數(shù)組后返回,JVM 會(huì)使用 transform 方法返回的字節(jié)碼數(shù)據(jù)進(jìn)行類(lèi)的加載。

          JVM TI


          定義完了字節(jié)碼的修改和重定義方法,但我們?cè)趺床拍茏?JVM 能夠調(diào)用我們提供的類(lèi)轉(zhuǎn)換器呢?這里又要介紹到 JVM TI 了。

          介紹

          JVM TI(JVM Tool Interface)JVM 工具接口是 JVM 提供的一個(gè)非常強(qiáng)大的對(duì) JVM 操作的工具接口,通過(guò)這個(gè)接口,我們可以實(shí)現(xiàn)對(duì) JVM 多種組件的操作,從JVMTM Tool Interface 這里我們認(rèn)識(shí)到 JVM TI 的強(qiáng)大,它包括了對(duì)虛擬機(jī)堆內(nèi)存、類(lèi)、線程等各個(gè)方面的管理接口。

          JVM TI 通過(guò)事件機(jī)制,通過(guò)接口注冊(cè)各種事件勾子,在 JVM 事件觸發(fā)時(shí)同時(shí)觸發(fā)預(yù)定義的勾子,以實(shí)現(xiàn)對(duì)各個(gè) JVM 事件的感知和反應(yīng)。

          Agent

          Agent 是 JVM TI 實(shí)現(xiàn)的一種方式。我們?cè)诰幾g C 項(xiàng)目里鏈接靜態(tài)庫(kù),將靜態(tài)庫(kù)的功能注入到項(xiàng)目里,從而才可以在項(xiàng)目里引用庫(kù)里的函數(shù)。我們可以將 agent 類(lèi)比為 C 里的靜態(tài)庫(kù),我們也可以用 C 或 C++ 來(lái)實(shí)現(xiàn),將其編譯為 dll 或 so 文件,在啟動(dòng) JVM 時(shí)啟動(dòng)。

          這時(shí)再來(lái)思考 Debug 的實(shí)現(xiàn),我們?cè)趩?dòng)被 Debug 的 JVM 時(shí),必須添加參數(shù) -agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:3333,而 -agentlib 選項(xiàng)就指定了我們要加載的 Java Agent,jdwp 是 agent 的名字,在 linux 系統(tǒng)中,我們可以在 jre 目錄下找到 jdwp.so 庫(kù)文件。

          Java 的調(diào)試體系 jdpa 組成,從高到低分別為 jdi->jdwp->jvmti,我們通過(guò) JDI 接口發(fā)送調(diào)試指令,而 jdwp 就相當(dāng)于一個(gè)通道,幫我們翻譯 JDI 指令到 JVM TI,最底層的 JVM TI 最終實(shí)現(xiàn)對(duì) JVM 的操作。

          使用

          JVM TI 的 agent 使用很簡(jiǎn)單,在啟動(dòng) agent 時(shí)添加 -agent 參數(shù)指定我們要加載的 agent jar包即可。

          而要實(shí)現(xiàn)代碼的修改,我們需要實(shí)現(xiàn)一個(gè) instrument agent,它可以通過(guò)在一個(gè)類(lèi)里添加 premain() 或 agentmain() 方法來(lái)實(shí)現(xiàn)。而要實(shí)現(xiàn) 1.6 以上的動(dòng)態(tài) instrument 功能,實(shí)現(xiàn) agentmain 方法即可。

          在 agentmain 方法里,我們調(diào)用 Instrumentation.retransformClasses() 方法實(shí)現(xiàn)對(duì)目標(biāo)類(lèi)的重定義。

          另外往一個(gè)正在運(yùn)行的 JVM 里動(dòng)態(tài)添加 agent,還需要用到 JVM 的 attach 功能,Sun 公司的 tools.jar 包里包含的 VirtualMachine 類(lèi)提供了 attach 一個(gè)本地 JVM 的功能,它需要我們傳入一個(gè)本地 JVM 的 pid, tools.jar 可以在 jre 目錄下找到。

          agent生成

          另外,我們還需要注意 agent 的打包,它需要指定一個(gè) Agent-Class 參數(shù)指定我們的包括 agentmain 方法的類(lèi),可以算是指定入口類(lèi)吧。

          此外,還需要配置 MANIFEST.MF 文件的一些參數(shù),允許我們重新定義類(lèi)。如果你的 agent 實(shí)現(xiàn)還需要引用一些其他類(lèi)庫(kù)時(shí),還需要將這些類(lèi)庫(kù)都打包到此 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>

          另外在打包時(shí)需要使用 mvn assembly:assembl 命令生成 jar-with-dependencies 作為 agent。

          代碼實(shí)現(xiàn)


          我在測(cè)試時(shí)寫(xiě)了一個(gè)用以上技術(shù)實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的字節(jié)碼動(dòng)態(tài)修改的 Demo。

          被修改的類(lèi)

          TransformTarget 是要被修改的目標(biāo)類(lèi),正常執(zhí)行時(shí),它會(huì)三秒輸出一次 “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

          Agent 是執(zhí)行修改類(lèi)的主體,它使用 ASM 修改 TransformTarget 類(lèi)的方法,并使用 instrument 包將修改提交給 JVM。

          入口類(lèi),也是代理的 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!");
                  }
              }
          }

          執(zhí)行字節(jié)碼修改和轉(zhuǎn)換的類(lèi)。

          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(20);
                          mv.visitEnd();
                          TransformTarget.printSomething();
                      }
                      return mv;
                  }
              }
          }

          Attacher

          使用 tools.jar 里方法將 agent 動(dòng)態(tài)加載到目標(biāo) JVM 的類(lèi)。

          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");
              }
          }

          這樣,先啟動(dòng) TransformTarget 類(lèi),獲取到 pid 后將其傳入 Attacher 里,并指定 agent jar,將 agent attach 到 TransformTarget 中,原來(lái)輸出的 “hello” 就變成我們想要修改的 “bytecode replaced!” 了。

          小結(jié)


          掌握了字節(jié)碼的動(dòng)態(tài)修改技術(shù)后,再回頭看 Btrace 的原理就更清晰了,稍微摸索一下我們也可以實(shí)現(xiàn)一個(gè)簡(jiǎn)版的。另外很多大牛實(shí)現(xiàn)的各種 Java 性能分析工具的技術(shù)棧也不外如此,了解了這些,未來(lái)我們也可以寫(xiě)出適合自己的工具,至少能對(duì)別人的工具進(jìn)行修改~

          不得不說(shuō) Java 的生態(tài)真的非常繁榮,當(dāng)真是博大精深,查閱一個(gè)模塊的資料時(shí)能總引出一大堆新的概念,永遠(yuǎn)有學(xué)不完的新東西。

          往期熱門(mén)文章:

          1、歷史文章分類(lèi)導(dǎo)讀列表!精選優(yōu)秀博文都在這里了!》

          2、IDEA 2021首個(gè)大版本發(fā)布,新增了這幾個(gè)超實(shí)用功能!

          3、Optional 是個(gè)好東西,你真的會(huì)用么?
          4、【建議收藏】面試官會(huì)問(wèn)的位運(yùn)算奇淫技巧

          5、ConcurrentHashMap有十個(gè)提升性能的地方,你都知道嗎?
          6、程序員等級(jí)圖鑒
          7Java 中的 Switch 都支持 String 了,為什么不支持 long?
          8、為什么數(shù)據(jù)庫(kù)字段要使用NOT NULL?
          9CTO 說(shuō)了,用錯(cuò) @Autowired 和 @Resource 的人可以領(lǐng)盒飯了
          10、程序員離職事件始末

          瀏覽 36
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  性生活视频无码 | 十八女人高潮A片免费 | 五月丁香婷婷激情网 | 男人天堂最新网址 | 欧美黄片亚洲黄片 |