asm使用指南中文-md版&快速入門&詳解

asm4-guide-英文.pdf
asm4-guide-中文

還是人家官網文檔寫得好,什麼快速入門都不如官方文檔,閱讀兩小時,就知道怎麼回事了。

ASM使用指南中文版

1. 介紹

1.1. 動機

程序分析、程序生成和程序轉換都是非常有用的技術,可在許多應用環境下使用:

  • 程序分析,既可能只是簡單的語法分析(syntaxic parsing),也可能是完整的語義分 析 (sematic analysis),可用於查找應用程序中的潛在 bug、檢測未被用到的代碼、 對代碼 實施逆向工程,等等。
  • 程序生成,在編譯器中使用。這些編譯器不僅包括傳統編譯器,還包括用於分佈式程序 設計的 stub 編譯器或 skeleton 編譯器,以及 JIT(即時)編譯器,等等。
  • 程序轉換可,用於優化或混淆(obfuscate)程序、嚮應用程序中插入調試或性能監視 代 碼,用於面向方面的程序設計,等等。

所有這些技術都可針對任意程序設計語言使用,但對於不同語言,其使用的難易程度可能會 有 所不同。對於 Java 語言,它們可用於 Java 源代碼或編譯後的 Java 類。在使用經過編譯的類時, 其好處之一顯然就是不需要源代碼。因此,程序轉換可用於任何應用程序,既包括保密的源代碼, 也包含商業應用程序。使用已編譯類的另一個好處是,有可能在運行時,在馬上就要將類加載到Java 虛擬機之前,對類進行分析、生成或轉換(在運行時生成和編譯源代碼也可以,但其速度很 慢,而且需要一個完整的 java 編譯器)。其好處是,諸如 stub 編譯器或方面編織器等工具對用戶變爲透明。

由於程序分析、生成和轉換技術的用途衆多,所以人們針對許多語言實現了許多用於分析、 生 成和轉換程序的工具,這些語言中就包括 Java 在內。ASM 就是爲 Java 語言設計的工具之一, 用 於進行運行時(也是脫機的)類生成與轉換。於是,人們設計了 ASM1庫,用於處理經過編譯 的 Java 類。這個庫的設計使其儘可能保持快速和小型化。對於那些在運行時使用 ASM 進行動態 類生成或轉換的應用程序來說,儘可能提高庫的運行速度是非常重要的,這樣可以保證這些應 用 程序的速度不致下降過多。而保持 ASM 庫的小型化也非常重要,一方面是爲了在內存有 限的環 境中使用,另一方面,也爲了避免使那些使用 ASM 的小型應用程序或庫增大過多。

ASM 並不是惟一可生成和轉換已編譯 Java 類的工具,但它是最新、最高效的工具之一,可 從 http://asm.objectweb.org 下載。其主要優點如下:

1 ASM 的名字沒有任何含義:它只是引用 C 語言中的__asm__關鍵字,這個關鍵字允許執行一些用匯 編 語言編寫的函數。

  • 有一個簡單的模塊 API,設計完善、使用方便。
  • 文檔齊全,擁有一個相關的 Eclipse 插件。
  • 支持最新的 Java 版本——Java 7。 小而快、非常可靠。
  • 擁有龐大的用戶社區,可以爲新用戶提供支 持。
  • 源許可開放,幾乎允許任意使用。

1.2. 概述

1.2.1. 範圍

ASM 庫的目的是生成、轉換和分析以字節數組表示的已編譯 Java 類(它們在磁盤中的存儲 和 在 Java 虛擬機中的加載都採用這種字節數組形式)。爲此,ASM 提供了一些工具,使用高於字節級別的概念來讀寫和轉換這種字節數組,這些概念包括數值常數、字符串、Java 標識符、Java 類型、Java 類結構元素,等等。注意,ASM 庫的範圍嚴格限制於類的讀、寫、轉換和分析。具體來說,類的加載過程就超出了它的範圍之外

1.2.2. 模型

ASM 庫提供了兩個用於生成和轉換已編譯類的 API,一個是核心 API,以基於事件的形式來表示類,另一個是樹 API,以基於對象的形式來表示類。

在採用基於事件的模型時,類是用一系列事件來表示的,每個事件表示類的一個元素,比 如 它的一個標頭、一個字段、一個方法聲明、一條指令,等等。基於事件的 API 定義了一組 可能 事件,以及這些事件必須遵循的發生順序,還提供了一個類分析器,爲每個被分析元素生 成一個 事件,還提供一個類寫入器,由這些事件的序列生成經過編譯的類。

而在採用基於對象的模型時,類用一個對象樹表示,每個對象表示類的一部分,比如類本身、 一個字段、一個方法、一條指令,等等,每個對象都有一些引用,指向表示其組成部分的對象。 基 於對象的 API 提供了一種方法,可以將表示一個類的事件序列轉換爲表示同一個類的對象樹, 也 可以反過來,將對象樹表示爲等價的事件序列。換言之,基於對象的 API 構建在基於事件的API 之上。

這兩個 API 可以與“用於 XML 的簡單 API”(Simple API for XML,SAX)和用於 XML 文 檔的“文檔對象模型(Document Object Model,DOM)API”相比較:基於事件的 API 類似於 SAX,而基於對象的 API 類似於 DOM。基於對象的 API 構建在基於事件的 API之上,類似於 DOM 可在 SAX 的上層提供。

ASM 之所以要提供兩個 API,是因爲沒有哪種 API 是最佳的。實際上,每個 API 都有自己 的優缺點:

  • 基於事件的 API 要快於基於對象的 API,所需要的內存也較少,因爲它不需要在內 存中 創建和存儲用於表示類的對象樹(SAX 與 DOM 之間也有同樣的差異)。
  • 但在使用基於事件的 API 時,類轉換的實現可能要更難一些,因爲在任意給定時 刻, 類中只有一個元素可供使用(也就是與當前事件對應的元素),而在使用基於對 象的 API 時,可以在內存中獲得整個類。

注意,這兩個 API 都是僅能同時維護一個類,而且獨立於其他類,也就是說,它們不會維 護有關類層級結構的信息,如果類的轉換影響到其他類,那其他這些類的修改應當由用戶負責完 成。

1.2.3. 體系結構

ASM 應用程序擁有一個很強壯的體系結構方面(aspect)。事實上,對於基於事件的 API, 其組織結構是圍繞事件生成器(類分析器)、事件使用器(類寫入器)和各種預定義的事件篩選 器
進行的,在這一結構中可以添加用戶定義的生成器、使用器和篩選器。因此,這一 API 的使 用分爲兩個步驟:

  • 將事件生成器、篩選器和使用器組件組裝爲可能很複雜的體系結構。
  • 然後啓動事件生成器,以執行生成或轉換過程。

基於對象的 API 也有一個體繫結構方面:實際上,用於操作類樹的類生成器或轉換器組 件 是可以組成形成的,它們之間的鏈接代表着轉換的順序。

儘管典型 ASM 應用程序中的大多數組件體系結構都非常簡單,但還是可以想象一下類似於 如 下所示的複雜體系結構,其中的箭頭表示在類分析器、寫入器或轉換器之間進行的基於事件或 基於 對象的通信,在整個鏈中的任何位置,都可能會在基於事件與基於對象的表示之間進行轉換:
在這裏插入圖片描述

1.3. 組織

ASM 庫劃分爲幾個包,以幾個 jar 文件的形式進行分發:

  • org.objectweb.asmorg.objectweb.asm.signature包定義了基於事件的
    API,並提供了類分析器和寫入器組件。它們包含在 asm.jar 中。

  • org.objectweb.asm.util 包,位於asm-util.jar中,提供各種基於
    核心 API 的工具,可以在開發和調試 ASM 應用程序時使用。

  • org.objectweb.asm.commons 包提供了幾個很有用的預定義類轉換器,它們大 多 是基於核心 API 的。這個包包含在 asm-commons.jar中。

  • org.objectweb.asm.tree 包,位於asm-tree.jar 存檔文件中,定義了基於對 象的 API,並提供了一些工具,用於在基於事件和基於對象的表示方法之間進行轉換。

  • org.objectweb.asm.tree.analysis 包提供了一個類分析框架和幾個預定義的 類 分析器,它們以樹 API 爲基礎。這個包包含在 asm-analysis.jar 文件中。

本文檔分爲兩部分。第一部分介紹核心 API,即 asm、asm-util 和 asm-commons 存檔文 件。第二部分介紹樹 API,即 asm-tree 和 asm-analysis 存檔文件。每部分至少包含該 API 與類相關的一章內容、該 API 與方法相關的一章內容、該 API 與註釋、泛型等相關的一章內容。 每章都會介紹編程接口及相關的工具與預定義組件。所有示例的源代碼都可以從 ASM 網站上獲得。

這種組織形式便於循序漸進地介紹類文件特徵,但有時需要將同一個 ASM 類的介紹分散 到 幾節中。因此,建議依次閱讀本文檔。如需有關 ASM API 的參考手冊,請使用 Javadoc。

印刷約定
斜體 用於強調句子中的元素。
一般字體顯示 用於表示代碼段。
加粗字體用於強調代碼元素。
斜體加粗字體用於表示標記和代碼中的變量部分。

1.4. 致謝

感謝 François Horn 在製作本文檔期間提供的寶貴評論,這些意見極大地提升了本文檔的 結構和可讀性。

第一部分 核心 API

本章說明如何使用核心 ASM API 來生成和轉換經過編譯的 Java 類。首先介紹已編譯類,然 後將利用大量說明性示例,介紹用於生成和轉換已編譯類的相應 ASM 接口、組件和工具。方法、 註釋和泛型的內容將在之後各章中說明

2. class 類

2.1.結構體

2.1.1. 概述

已編譯類的總體結構非常簡單。實際上,與原生編譯應用程序不同,已編譯類中保留了來自 源代碼的結構信息和幾乎所有符號。事實上,已編譯類中包含如下各部分:

  • 專門一部分,描述類的修飾符(比如 publicprivate)、名字超類接口注 釋
  • 類中聲明的每個字段各有一部分。每一部分描述一個字段的修飾符、名字、類型和註釋。
  • 類中聲明的每個方法構造器各有一部分。每一部分描述一個方法的修飾符、名字、返回類型與參數類型、註釋。 它還以 Java 字節代碼指令的形式,包含了該方法的已編 譯 代碼。

但在源文件類和已編譯類之間還是有一些差異:

源文件的類和編譯類,結構會有編碼。比如對於類內變量的使用,會變成this.

  • 一個已編譯類僅描述一個類,而一個源文件中可以包含幾個類。比如,一個源文件描 述 了一個類,這個類又有一個內部類,那這個源文件會被編譯爲兩個類文件 : 主類 和內 部類各一個文件。但是,主類文件中包含對其內部類的引用,定義了內部方法 中定義的 類會包含引用,引向其封裝的方法。
  • 已編譯類中當然不包含註釋(comment),但可以包含類、字段、方法和代碼屬性,可 以 利用這些屬性爲相應元素關聯更多信息。Java 5 中引入可用於同一目的的註釋
    (annotaion)以後,屬性已經變得沒有什麼用處了。
  • 編譯類中不包含 packageimport 部分,因此,所有類型名字都必須是完全限定的。

另一個非常重要的結構性差異是已編譯類中包含常量池(constant pool)部分。這個池是一個數組,其中包含了在類中出現的所有數值、字符串和類型常量。這些常量僅在這個常量池部 分 中定義一次,然後可以利用其索引,在類文件中的所有其他各部分進行引用。幸好,ASM 隱藏 了與常量池有關的所有細節,所以我們不用再爲它操心了。圖 2.1 中總結了一個已編譯 類的整體 結構。其確切結構在《Java 虛擬機規範》第 4 節中描述。

Modifiers, name, super class, interfaces
Constant pool: numeric, string and type constants Source file name (optional)
Enclosing class reference
Annotation*
Attribute*
Inner class* Name
Field* Modifiers, name, type
Annotation*
Attribute*
Method* Modifiers, name, return and parameter types
Annotation*
Attribute*
Compiled code

在這裏插入圖片描述
*圖 2‐1 已編譯類的整體結構(表示零個或多個)

另一個重要的差別是 Java 類型在已編譯類和源文件類中的表示不同。後面幾節將解釋它 們 在已編譯類中的表示。

2.1.2. 內部名

在許多情況下,一種類型只能是類或接口類型。例如,一個類的超類、由一個類實現的接 口, 或者由一個方法拋出的異常就不能是基元類型或數組類型,必須是類或接口類型。這些類 型在已 編譯類中用內部名字表示。一個類的內部名就是這個類的完全限定名,其中的點號用斜 線代替。 例如,String 的內部名爲 java/lang/String

2.1.3. 類型描述符

內部名只能用於類或接口類型。所有其他 Java 類型,比如字段類型,在已編譯類中都是用
類型描述符表示的(見圖 2.2)。

Java 類型 類型描述符
boolean Z
char C
byte B
short S
int I
float F
long J
double D
Object Ljava/lang/Object;
int[] [I
Object[][] [[Ljava/lang/Object;

圖 2‐2 一些 Java 類型的類型描述符
基本類型的描述符是單個字符: Z 表示 booleanC 表示 charB 表示 byteS 表示 shortI 表示 intF 表示 floatJ 表示 longD 表示 double。一個類類型的描述符 是這個類的 內部名, 前面加上字符 L , 後面跟有一個分號。例如, String 的類型描述符爲 Ljava/lang/String;。而一個數組類型的描述符是一個方括號後面跟有該數組元素類型的描 述符。

2.1.4. 方法描述符

方法描述符是一個類型描述符列表,它用一個字符串描述一個方法的參數類型和返回類型。 方
法描述符以左括號開頭,然後是每個形參的類型描述符,然後是一個右括號,接下來是返回類 型的
類型描述符,如果該方法返回 void,則是 V(方法描述符中不包含方法的名字或參數名)。

源文件中的方法聲明 方法描述符
void m(int i, float f) (IF)V
int m(Object o) (Ljava/lang/Object;)I
int[] m(int i, String s) (ILjava/lang/String;)[I
Object m(int[] i) ([I)Ljava/lang/Object;

圖 2.3 方法描述符舉例

一旦知道了類型描述符如何工作,方法描述符的理解就容易了。例如,(I)I 描述一個方 法, 它接受一個 int 類型的參數,返回一個 int。圖 2.3 給出了幾個方法描述符示例。

2.2. 接口和組件

2.2.1. 介紹

用於生成和變轉已編譯類的 ASM API 是基於 ClassVisitor 抽象類的(見圖 2.4)。這 個 類中的每個方法都對應於同名的類文件結構部分(見圖 2.1)。簡單的部分只需一個方法調 用就能 訪問,這個調用返回 void,其參數描述了這些部分的內容。有些部分的內容可以達到 任意長度、 任意複雜度,這樣的部分可以用一個初始方法調用來訪問,返回一個輔助的訪問者 類。 visitAnnotation、visitField 和 visitMethod 方法就是這種情況,它們分別返 回 AnnotationVisitor、FieldVisitor 和 MethodVisitor.

public abstract class ClassVisitor{ 
	public ClassVisitor(int api);
	
	public ClassVisitor(int api, ClassVisitor cv);
	
	public void visit(int version, int access, String name, String signature, String superName, String[] interfaces);
	
	public void visitSource(String source, String debug);
	
	public void visitOuterClass(String owner, String name, String desc); 
	
	AnnotationVisitor visitAnnotation(String desc, boolean visible); 
	
	public void visitAttribute(Attribute attr);
	
	public void visitInnerClass(String name, String outerName, String innerName, int access);
	
	public FieldVisitor visitField(int access, String name, String desc, String signature, Object value);
	
	public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions); 
	
	void visitEnd();
} 

圖 2.4 ClassVisitor 類
針對這些輔助類遞歸適用同樣的原則。例如,FieldVisitor 抽象類中的每個方法(見圖2.5)對應於同名的類文件子結構,visitAnnotation 返回一個輔助的 AnnotationVisitor, 和在 ClassVisitor 中一樣。這些輔助訪問者類的創建和使用在隨後幾章中解釋:實際上, 本 章僅限於只需 ClassVisitor 類本身就能解決的簡單問題。

public abstract class FieldVisitor {
	public FieldVisitor(int api);
	public FieldVisitor(int api, FieldVisitor fv);
	public AnnotationVisitor visitAnnotation(String desc, boolean visible); 
	public void visitAttribute(Attribute attr);
	public void visitEnd();
}

圖 2.5 FieldVisitor 類
ClassVisitor 類的方法必須按以下順序調用(在這個類的Javadoc 中規定):

visit visitSource? visitOuterClass? 
( visitAnnotation| visitAttribute )*
( visitInnerClass | visitField |visitMethod )* visitEnd

這意味着必須首先調用 visit,然後是對 visitSource 的最多一個調用,接下來是對 visitOuterClass 的最多一個調用 , 然後是可按任意順序對 visitAnnotationvisitAttribute 的任意多個訪問 , 接下來是可按任意順序對 visitInnerClassvisitFieldvisitMethod 的任意多個調用,最後以一個 visitEnd 調用結束。

ASM 提供了三個基於 ClassVisitor API 的核心組件,用於生成和變化類:

  • ClassReader 類分析以字節數組形式給出的已編譯類,並針對在其 accept 方法 參數 中傳送的 ClassVisitor 實例,調用相應的 visitXxx 方法。這個類可以看 作一個事 件產生器。
  • ClassWriter 類是 ClassVisitor 抽象類的一個子類,它直接以二進制形式生成 編 譯後的類。它會生成一個字節數組形式的輸出, 其中包含了已編譯類, 可以用 toByteArray 方法來提取。這個類可以看作一個事件使用器。
  • ClassVisitor 類將它收到的所有方法調用都委託給另一個 ClassVisitor 類。 這個 類可以看作一個事件篩選器。

接下來的各節將用一些具體示例來說明如何使用這些組件來生成和轉換類。

2.2.2. 解析類

在分析一個已經存在的類時,惟一必需的組件是 ClassReader 組件。讓我們用一個例子 來 說明。假設希望打印一個類的內容,其方式類似於 javap 工具。第一步是編寫 ClassVisitor 類的一個子類,打印它所訪問的類的相關信息。下面是一種可能的實現方式,它有些過於簡化了:

public class ClassPrinter extends ClassVisitor {
	public ClassPrinter() {
          super(ASM4);
    }
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
          System.out.println(name + " extends " + superName + " {");
    }
    public void visitSource(String source, String debug) {}
    public void visitOuterClass(String owner, String name, String desc) {}
   	public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
              return null;
	}
	public void visitAttribute(Attribute attr) {}
	public void visitInnerClass(String name, String outerName, String innerName, int access) {}
	public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
  		System.out.println("    " + desc + " " + name);
  		return null;
  	}
	public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    	System.out.println("    " + name + desc);
    	return null;
  	}
  	public void visitEnd() {
    	System.out.println("}");
	} 
}

第二步是將這個 ClassPrinter 與一個 ClassReader 組件合併在一起,使 ClassReader 產生的事件由我們的 ClassPrinter 使用:

ClassPrinter cp = new ClassPrinter();
ClassReader cr = new ClassReader("java.lang.Runnable"); 
cr.accept(cp, 0);

第二行創建了一個 ClassReader,以分析 Runnable 類。在最後一行調用的 accept
法分析 Runnable 類字節代碼,並對 cp 調用相應的 ClassVisitor 方法。結果爲以下輸出:

// 真實的Runnable接口也只有一個方法 void run(),和這個輸出對應
java/lang/Runnable extends java/lang/Object
{ run()V}

注意,構建 ClassReader 實例的方式有若干種。必須讀取的類可以像上面一樣用名字指定, 也 可 以 像 字 母 數 組 或 InputStream 一 樣 用 值 來 指 定 。 利 用 ClassLoadergetResourceAsStream 方法,可以獲得一個讀取類內容的輸入流,如下:
cl.getResourceAsStream(classname.replace(’.’, ’/’) + ".class");

2.2.3. 生成類

爲生成一個類,惟一必需的組件是 ClassWriter 組件。讓我們用一個例子來進行說明。 考慮以下接口:

 package pkg;
      public interface Comparable extends Mesurable {
        int LESS = -1;
        int EQUAL = 0;
        int GREATER = 1;
        int compareTo(Object o);
}

可以對 ClassVisitor 進行六次方法調用來生成它:

ClassWriter cw = new ClassWriter(0);
cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, "pkg/Comparable", null, "java/lang/Object",new String[] { "pkg/Mesurable" });

cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I", null, new Integer(-1)).visitEnd();

cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I", null, new Integer(0)).visitEnd();

cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I", null, new Integer(1)).visitEnd(); cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo", "(Ljava/lang/Object;)I", null, null).visitEnd(); 

cw.visitEnd();

byte[] b = cw.toByteArray();

第一行創建了一個 ClassWriter 實例,它實際上將創建類的字節數組表示(構造器參數 在下一章解釋)。

visit 方法的調用定義了類的標頭。V1_5 參數1是一個常數,與所有其他 ASM 常量一樣,在 ASM Opcodes 接口中定義。它指明瞭類的版本——Java 1.5ACC_XXX 常量2是與 Java 修飾 符對 應的標誌。這裏規定這個類是一個接口,而且它是 public 和 abstract 的(因爲它不能 被實例化)。下一個3 參數以內部形式規定了類的名字(見 2.1.2 節)。回憶一下,已編譯類不包含 Package 和 Import 部分,因此,所有類名都必須是完全限定的。下一個參數4對應於泛型(見 4.1 節)。在我們的例子中,這個參數是 null,因爲這個接口並沒有由類型變量進行參數化。 第 五5 個參數是內部形式的超類(接口類隱式繼承自 Object)。最後一個參數是一個數組,其 中是 被擴展的接口,這些接口由其內部名指定。

一個完整的類定義 public class test<File> extend Object implent A , B { }五個參數對應五個

接下來對 visitField 的三次調用是類似的,用於定義三個接口字段。第一個參數是一組 標誌,對應於 Java 修飾符。這裏規定這些字段是 public、final 和 static 的。第二個參數 是字段的名字,與它在源代碼中的顯示相同。第三個參數是字段的類型,採用類型描述符形式。 這 裏,這些字段是 int 字段,它們的描述符是 I。第四個參數對應於泛型。在我們的例子中, 它是 null,因爲這些字段類型沒有使用泛型。最後一個參數是字段的常量值:這個參數必須僅用於真正的常量字段,也就是 final static 字段。對於其他字段,它必須爲 null。由於此處 沒有註釋, 所以立即調用所返回的 FieldVisitor 的 visitEnd 方法, 即對其 visitAnnotation 或 visitAttribute 方法沒有任何調用。

visitMethod 調用用於定義 compareTo 方法,同樣,第一個參數是一組對應於 Java 修飾
符的標誌。第二個參數是方法名,與其在源代碼中的顯示一樣。第三個參數是方法的描述符。第 四 個參數對應於泛型。在我們的例子中,它是 null,因爲這個方法沒有使用泛型。最後一個參 數是 一個數組,其中包括可由該方法拋出的異常,這些異常由其內部名指明。它在這裏爲 null, 因爲這個方法沒有聲明任何異常。visitMethod 方法返回 MethodVisitor(見圖 3.4),可用 於定義該方法的註釋和屬性,最重要的是這個方法的代碼。這裏,由於沒有註釋,而且這個方法 是抽象的,所以我們立即調用所返回的 MethodVisitor 的 visitEnd 方法。

類方法或者filed等結束後還有註釋,使用visitEnd代表着結束。
visitEnd 的最後一個調用是爲了通知 **cw:**這個類已經結束,對 toByteArray 的調用用於以字節數組的形式提取它。

使用生成的類

前面的字節數組可以存儲在一個 Comparable.class 文件中,供以後使用。或者,也可 以 用 ClassLoader 動態加載它。一種方法是定義一個 ClassLoader 子類,它的 defineClass 方法是公有的:

   class MyClassLoader extends ClassLoader {
          public Class defineClass(String name, byte[] b) {
            return defineClass(name, b, 0, b.length);
          }
}

然後,可以用下面的代碼直接調用所生成的類:

Class c = myClassLoader.defineClass("pkg.Comparable", b);

另一種加載已生成類的方法可能更清晰一些,那就是定義一個 ClassLoader 子類,它 的 findClass 方法被重寫,以在運行過程中生成所請求的類:

 class StubClassLoader extends ClassLoader {
        @Override
        protected Class findClass(String name) throws ClassNotFoundException {
          if (name.endsWith("_Stub")) {
            	ClassWriter cw = new ClassWriter(0);
            	...
            	byte[] b = cw.toByteArray();
            	return defineClass(name, b, 0, b.length);
			}
          return super.findClass(name);
        }
}

事實上,所生成類的使用方式取決於上下文,這已經超出了 ASM API 的範圍。如果你正 在 編寫編譯器,那類生成過程將由一個抽象語法樹驅動,這個語法樹代表將要編譯的程序,而 生成 的類將被存儲在磁盤上。如果你正在編寫動態代理類生成器或方面編織器,那將會以這種 或那種 方式使用一個 ClassLoader

2.2.4. 轉換(修改)類

到目前爲止,ClassReaderClassWriter 組件都是單獨使用的。這些事件是“人工” 產生,並且由 ClassWriter 直接使用,或者與之對稱地,它們由 ClassReader 產生,然後 “人工”使用,也就是由自定義的 ClassVisitor 實現使用。當這些組件一同使用時,事情開 始 變得真正有意義起來。第一步是將 ClassReader 產生的事件轉給 ClassWriter。其結果是, 類編寫器重新構建了由類讀取器分析的類:

byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
ClassReader cr = new ClassReader(b1); cr.accept(cw, 0);
byte[] b2 = cw.toByteArray(); // b2 和 b1 表示同一個類

這本身並沒有什麼真正的意義(還有其他更簡單的方法可以用來複制一個字節數組!), 但等 一等。下一步是在類讀取器和類寫入器之間引入一個 ClassVisitor:

byte[] b1 = ...;
    ClassWriter cw = new ClassWriter(0);
// cv 將所有事件轉發給 cw
ClassVisitor cv = new ClassVisitor(ASM4, cw) { }; ClassReader cr = new ClassReader(b1); cr.accept(cv, 0);
byte[] b2 = cw.toByteArray(); // b2 與 b1 表示同一個類

圖 2.6 給出了與上述代碼相對應的體系結構,其中的組件用方框表示,事件用箭頭表示(其
中的垂直時間線與程序圖中一樣)。
在這裏插入圖片描述
圖 2.6 轉換鏈
但結果並沒有改變,因爲 ClassVisitor 事件篩選器沒有篩選任何東西。但現在,爲了能 夠轉換一個類,只需重寫一些方法,篩選一些事件就足夠了。例如,考慮下面的 ClassVisitor 子類:

public class ChangeVersionAdapter extends ClassVisitor { 
		public ChangeVersionAdapter(ClassVisitor cv) {
          super(ASM4, cv);
        }
        @Override
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { 
        	cv.visit(V1_5, access, name, signature, superName, interfaces);
} }

這個類僅重寫了 ClassVisitor 類的一個方法。結果,所有調用都被不加改變地轉發到傳 送給構造器的類訪問器 cv,只有對 visit 方法的調用除外,在轉發它時,對類版本號進行了修改。相應的程序圖在圖 2.7 中給出。
在這裏插入圖片描述
圖 2.7 ChangeVersionAdapter 的程序圖
通過修改 visit 方法的其他參數,可以實現其他轉換,而不僅僅是修改類的版本。例如, 可以向實現接口的列表中添加一個接口。還可以改變類的名字,但進行這種改變所需要做的工作 要多得多,不只是改變 visit 方法的 name 參數了。實際上,類的名字可以出現在一個已編譯 類的許多不同地方,要真正實現類的重命名,必須修改類中出現的所有這些類名字。

改進

前面的轉換隻修改了原類的四個字節。但是,在使用上面的代碼時,整個 b1 均被分析,並利用相應的事件從頭從頭構建了 b2,這種做法的效率不是很高。如果將 b1 中不被轉換的部分 直 接複製到 b2 中,不對其分析,也不生成相應的事件,其效率就會高得多。ASM 自動爲方法 執行這一優化:

  • ClassReader 組件的 accept 方法參數中傳送了 ClassVisitor , 如果 ClassReader 檢測到這個 ClassVisitor 返回的 MethodVisitor 來自一個
    ClassWriter,這意味着這個方法的內容將不會被轉換,事實上,應用程序甚至不會 看到其內容。
  • 在這種情況下,ClassReader 組件不會分析這個方法的內容,不會生成相應事件, 只 是複製 ClassWriter 中表示這個方法的字節數組。

如果 ClassReader 和 ClassWriter 組件擁有對對方的引用,則由它們進行這種優化, 可設置如下:

// 和前面相比,這裏是互相擁有,前面是單一擁有
byte[] b1 = ...
ClassReader cr = new ClassReader(b1);
ClassWriter cw = new ClassWriter(cr, 0); ChangeVersionAdapter ca = new ChangeVersionAdapter(cw); cr.accept(ca, 0);
byte[] b2 = cw.toByteArray();

優缺點
執行這一優化後,由於 ChangeVersionAdapter 沒有轉換任何方法,所以以上代碼的速度可以達到之前代碼的兩倍。對於轉換部分或全部方法的常見轉換,這一速度提升幅度可能要 小 一些,但仍然是很可觀的:實際上在 10%到 20%的量級。遺憾的是,這一優化需要將原類中 定義的所有常量都複製到轉換後的類中。對於那些增加字段、方法或指令的轉換來說,這一點不成問題,但對於那些要移除或重命名許多類成員的轉換來說,這一優化將導致類文件大於未優 化 時的情況。因此,建議僅對“增加性”轉換應用這一優化。

使用轉換後的類

如上節所述,轉換後的類 b2 可以存儲在磁盤上,或者用 ClassLoader 加載。但在ClassLoader 中執行的類轉換隻能轉換由這個類加載器加載的類。如果希望轉換所有類,則必 須將轉換放在 ClassFileTransformer 內部,見 java.lang.instrument 包中的定義(更 多細節,請參閱這個軟件包的文檔):

   public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new ClassFileTransformer() {
          public byte[] transform(ClassLoader l, String name, Class c,
              ProtectionDomain d, byte[] b)
              throws IllegalClassFormatException {
            ClassReader cr = new ClassReader(b);
            ClassWriter cw = new ClassWriter(cr, 0);
            ClassVisitor cv = new ChangeVersionAdapter(cw);
            cr.accept(cv, 0);
            return cw.toByteArray();
} });
}

2.2.5. 移除類成員

上一節用於轉換類版本的方法當然也可用於 ClassVisitor 類的其他方法。例如,通過 改 變 visitFieldvisitMethod 方法的 access 或 name 參數,可以改變一個字段 或一個方 法的修飾字段或名字。另外,除了在轉發的方法調用中使用經過修改的參數之外,還 可以選擇根 本不轉發該調用。其效果就是相應的類元素被移除

例如,下面的類適配器移除了有關外部類及內部類的信息,還刪除了一個源文件的名字,也
就是由其編譯這個類的源文件(所得到的類仍然具有全部功能,因爲刪除的這些元素僅用於調 試 目的)。這一移除操作是通過在適當的訪問方法中不轉發任何內容而實現的:

 public class RemoveDebugAdapter extends ClassVisitor {
        public RemoveDebugAdapter(ClassVisitor cv) {
          super(ASM4, cv);
        }
        @Override
        public void visitSource(String source, String debug) {
        }
        @Override
        public void visitOuterClass(String owner, String name, String desc) {
        }
        @Override
        public void visitInnerClass(String name, String outerName,
            String innerName, int access) {
        }
}

這一策略對於字段和方法是無效的,因爲 visitFieldvisitMethod 方法必須返回一 個結果。要移除字段或方法,不得轉發方法調用,並向調用者返回 null。例如,下面的類適配 器移除了一個方法,該方法由其名字及描述符指明(僅使用名字不足以標識一個方法,因爲一個 類中可能包含若干個具有不同參數的同名方法):

 public class RemoveMethodAdapter extends ClassVisitor {
        private String mName;
        private String mDesc;
        public RemoveMethodAdapter( ClassVisitor cv, String mName, String mDesc) {
          super(ASM4, cv);
          this.mName = mName;
		  this.mDesc = mDesc;
        }
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
          if (name.equals(mName) && desc.equals(mDesc)) {
         // 不要委託至下一個訪問器 -> 這樣將移除該方
            return null;
			}
          return cv.visitMethod(access, name, desc, signature, exceptions);
        }
}

2.2.6. 添加類成員

上述討論的是少轉發一些收到的調用,我們還可以多“轉發”一些調用,也就是發出的調用 數多於收到的調用,其效果就是增加了類成員。新的調用可以插在原方法調用之間的若干位置, 只要遵守各個 visitXxx 必須遵循的調用順序即可(見 2.2.1 節)。

例如,如果要向一個類中添加一個字段,必須在原方法調用之間添加對 visitField 的一 個新調用,而且必須將這個新調用放在類適配器的一個訪問方法中。比如,不能在 visit 方法 中 這樣做, 因爲這樣可能會導致對 visitField 的調用之後跟有 visitSource 、 visitOuterClass、visitAnnotationvisitAttribute,這是無效的。出於同樣的原 因,不能將這個新調用放在 visitSource、visitOuterClass、visitAnnotation 或 visitAttribute 方法中 . 僅有的可能位置是 visitInnerClass 、 visitField 、 visitMethod 或 visitEnd 方法。

如果將這個新調用放在 visitEnd 方法中,那這個字段將總會被添加(除非增加顯式條件), 因爲這個方法總會被調用。如果將它放在 visitField 或 visitMethod 中,將會添加幾個字 段:原類中的每個字段和方法各有一個相應的字段。這兩種解決方案都可能發揮應有的作用;具 體取決於你的需求。例如,可以僅添加一個計數器字段,用於計算對一個對象的調用次數,也可 以爲每個方法添加一個計數器,用於分別計算對每個方法的調用次數。

注意:事實上,惟一真正正確的解決方案是在 visitEnd 方法中添加更多調用,以添加新成員。實際上,
一個類中不得包含重複成員,要確保一個新成員沒有重複成員,惟一方法就是將它與所有已有成員進行對 比,只有在 visitEnd 方法中訪問了所有這些成員後才能完成這一工作。這種做法是相當受限制的。在
實踐中,使用程序員不大可能使用的生成名,比如_counter$或_4B7F_ i 就足以避免重複成員了, 並不需要將它們添加到 visitEnd 中。注意,在第一章曾經討論過,樹 API 沒有這一限制:可以在任意 時刻向使用這個 API 的轉換中添加新成員。

爲了舉例闡述以上討論,下面給出一個類適配器,它會向類中添加一個字段,除非這個字段 已經存在:

 public class AddFieldAdapter extends ClassVisitor {
        private int fAcc;
        private String fName;
        private String fDesc;
        private boolean isFieldPresent;
        public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName, String fDesc) {
          	super(ASM4, cv);
          	this.fAcc = fAcc;
          	this.fName = fName;
          	this.fDesc = fDesc;
        }
        @Override
        public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
          if (name.equals(fName)) {
            isFieldPresent = true;
		    }
          return cv.visitField(access, name, desc, signature, value);
        }
        
        @Override
        public void visitEnd() {
          if (!isFieldPresent) {
            FieldVisitor fv = cv.visitField(fAcc, fName, fDesc, null, null);
                // 如果返回null 意味着之前的方法被移除了
            	if (fv != null) {
              		fv.visitEnd();
            	}
			}
          cv.visitEnd();
        }
}

這個字段被添加在 visitEnd 方法中。visitField 方法未被重寫爲修改已有字段或刪 除 一個字段,只是檢測一下我們希望添加的字段是否已經存在。注意 visitEnd 方法中在調 用 fv.visitEnd() 之前的 fv != null 檢測:這是因爲一個類訪問器可以在 visitField 中返 回 null,在上一節已經看到這一點。

2.2.7. 轉化鏈

到目前爲止,我們已經看到一些由 ClassReader、類適配器和 ClassWriter 組成的簡單 轉換鏈。當然可以使用更爲複雜的轉換鏈,將幾個類適配器鏈接在一起。將幾個適配器鏈接在一 起,就可以組成幾個獨立的類轉換,以完成複雜轉換。還要注意,轉換鏈不一定是線性的。我們可以編寫一個 ClassVisitor,將接收到的所有方法調用同時轉發給幾個 ClassVisitor:

public class MultiClassAdapter extends ClassVisitor { 			
	protected ClassVisitor[] cvs;
	public MultiClassAdapter(ClassVisitor[] cvs) {
          super(ASM4);
          this.cvs = cvs;
    }
    @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
          for (ClassVisitor cv : cvs) {
            cv.visit(version, access, name, signature, superName, interfaces);
			}
	}
... 
}

反過來,幾個類適配器可以委託至同一 ClassVisitor(這需要採取一些預防措施,確保 比 如 visit 和 visitEnd 針對這個 ClassVisitor 恰好僅被調用一次)。因此,諸如圖 2.8 所 示的這樣一個轉換鏈是完全可行的。
在這裏插入圖片描述圖 2.8 一個複雜轉換

2.3. 工具

除了 ClassVisitor 類和相關的 ClassReader、ClassWriter 組件之外,ASM 還在 org.objectweb.asm.util包中提供了幾個工具,這些工具在開發類生成器或適配器時可能 非 常有用,但在運行時不需要它們。ASM 還提供了一個實用類,用於在運行時處理內部名、類 型描述符和方法描述符。所有這些工具都將在下面介紹。

2.3.1. 類型 Type

在前幾節已經看到,ASM API 公開 Java 類型的形式就是它們在已編譯類中的存儲形式,也 就是說,作爲內部特性或類型描述符。也可以按照它們在源代碼中的形式來公開它們,使代碼更 便 於閱讀。但這樣就需要在 ClassReader 和 ClassWriter 中的兩種表示形式之間進行系統 轉 換,從而使性能降低。這就是爲什麼 ASM 沒有透明地將內部名和類型描述符轉換爲它們等價的源代碼形式。但它提供了 Type 類,可以在必要時進行手動轉換。

一個 Type 對象表示一種 Java 類型,既可以由類型描述符構造,也可以由 Class 對象構建。 Type 類還包含表示基元類型的靜態變量。例如,Type.INT_TYPE 是表示 int 類型的 Type 對 象。

getInternalName 方 法 返 回 一 個 Type 的 內 部 名 。 例 如 ,Type.getType(String.class).getInternalName() 給出 String 類的內部名,即 “java/lang/String”。這一方法只能對類或接口類型使用。

getDescriptor 方法返回一個 Type 的描述符。比如 , 在代碼中可以不使用 “Ljava/lang/String;” , 而 是 使 用 Type.getType(String.class). getDescriptor()。或者,可以不使用 I,而是使用 Type.INT_TYPE.getDescriptor()

Type 對象還可以表示方法類型。這種對象既可以從一個方法描述符構建,也可以由 Method 對 象 構 建 。 getDescriptor 方 法 返 回 與 這 一 類 型 對 應 的 方 法 描 述 符 。 此 外 , getArgumentTypesgetReturnType 方法可用於獲取與一個方法的參數類型和 返回類型 相對應的 Type 對象。例如,**Type.getArgumentTypes("(I)V")**返回一個僅有一個 元素 Type.INT_TYPE 的數組。與此類似 , 調用 Type.getReturnType("(I)V") 將返回 Type.VOID_TYPE 對象。

2.3.2 TraceClassVisitor

要確認所生成或轉換後的類符合你的預期,ClassWriter 返回的字母數組並沒有什麼真正 的用處,因爲它對人類來說是不可讀的。如果有文本表示形式,那使用起來就容易多了。這正是 TraceClassVisitor 類提供的東西。從名字可以看出,這個類擴展了 ClassVisitor 類, 並生成所訪問類的文本表示。因此, 我們不是用 ClassWriter 來生成類, 而是使用 TraceClassVisitor,以獲得關於實際所生成內容的一個可讀軌跡。甚至可以同時使用這兩 者,這樣要更好一些。除了其默認行爲之外,TraceClassVisitor 實際上還可以將對其方 法 的所有調用委託給另一個訪問器,比如 ClassWriter:

ClassWriter cw = new ClassWriter(0);
TraceClassVisitor cv = new TraceClassVisitor(cw, printWriter);
cv.visit(...);
...
cv.visitEnd();
byte b[] = cw.toByteArray();

這一代碼創建了一個 TraceClassVisitor,將它自己接收到的所有調用都委託給 cw,然 後將這些調用的一份文本表示打印到 printWriter。例如,如果在 2.2.3 節的例子中使用 TraceClassVisitor,將會得出:

// 類版本號 49.0 (49)
// 訪問標誌 1537
public abstract interface pkg/Comparable implements pkg/Mesurable
{ // 訪問標誌 25
   public final static I LESS = -1
//訪問標誌 25
   public final static I EQUAL = 0 //訪問標誌 25
   public final static I GREATER = 1 //訪問標誌 1025
   public abstract compareTo(Ljava/lang/Object;)I

}

注意,可以在生成鏈或轉換鏈的任意位置使用 TraceClassVisitor,以查看在鏈中這 一 點發生了什麼,並非一定要恰好在 ClassWriter 之前使用。還要注意,有了這個適配器 生成 的類的文本表示形式,可能很輕鬆地用 String.equals() 來對比兩個類。

2.3.3. CheckClassAdapter

ClassWriter 類並不會覈實對其方法的調用順序是否恰當,以及參數是否有效。因此,有可能會生成一些被 Java 虛擬機驗證器拒絕的無效類。爲了儘可能提前檢測出部分此類錯誤,可 以使用 CheckClassAdapter 類。和 TraceClassVisitor 類似, 這個類也擴展了ClassVisitor 類,並將對其方法的所有調用都委託到另一個 ClassVisitor,比如一個 TraceClassVisitor 或一個 ClassWriter。但是,這個類並不會打印所訪問類的文本表示, 而是驗證其對方法的調用順序是否適當,參數是否有效,然後纔會委託給下一個訪問器。當發 生 錯誤時,會拋出 IllegalStateExceptionIllegalArgumentException

爲覈對一個類,打印這個類的文本表示形式,最終創建一個字節數組表示形式,應當使用類 似於如下代碼:

ClassWriter cw = new ClassWriter(0);
TraceClassVisitor tcv = new TraceClassVisitor(cw, printWriter); 
CheckClassAdapter cv = new CheckClassAdapter(tcv); 
cv.visit(...);
...
cv.visitEnd();
byte b[] = cw.toByteArray();

注意,如果以不同順序將這些類訪問器鏈在一起,那它們執行的操作也將以不同順序完成。 例如,利用以下代碼,這些覈對工作將在軌跡之後進行

    ClassWriter cw = new ClassWriter(0);
    CheckClassAdapter cca = new CheckClassAdapter(cw);
    TraceClassVisitor cv = new TraceClassVisitor(cca, printWriter); 

和使用 TraceClassVisitor 時一樣,也可以在一個生成鏈或轉換鏈的任意位置使用 CheckClassAdapter,以查看該鏈中這一點的類,而不一定只是恰好在 ClassWriter 之前使用。

2.3.4. ASMifier

這個類爲 TraceClassVisitor 工具提供了一種替代後端(該工具在默認情況下使用Textifier 後端,生成如上所示類型的輸出)。這個後端使 TraceClassVisitor 類的每個方法都會打印用於調用它的 Java 代碼。例如,調用 **visitEnd()**方法將打印 cv.visitEnd();。
其結果是,當一個具有 ASMifier 後端的 TraceClassVisitor 訪問器訪問一個類時,它會打 印用 ASM 生成這個類的源代碼。如果用這個訪問器來訪問一個已經存在的類,那這一點是很有 用的。例如,如果你不知道如何用 ASM 生成某個已編譯類,可以編寫相應的源代碼,用 javac 編譯它,並用 ASMifier 來訪問這個編譯後的類。將會得到生成這個已編譯類的 ASM 代碼!

ASMifier 類也可以在命令行中使用。例如,使用以下命令。

java -classpath asm.jar:asm-util.jar \
           org.objectweb.asm.util.ASMifier \
           java.lang.Runnable

將會生成一些代碼,經過縮進後,這些代碼就是如下模樣:

package asm.java.lang;
import org.objectweb.asm.*;
      public class RunnableDump implements Opcodes {
        	public static byte[] dump() throws Exception {
          		ClassWriter cw = new ClassWriter(0);
          		FieldVisitor fv;
          		MethodVisitor mv;
          		AnnotationVisitor av0;
          		// 別忘了,這個viist就是生成方法 ClassWriter中有介紹
         		cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, "java/lang/Runnable", null, "java/lang/Object", null);
          
         		 {
           			 mv = cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "run", "()V",null, null);
           			 mv.visitEnd();
         		 }
         	 	cw.visitEnd();
         		return cw.toByteArray();
        }
}

3. 方法

本章解釋如何用核心 ASM API 生成和轉換已編譯方法。首先介紹編譯後的方法,然後介
紹 用於生成和轉換它們的相應 ASM 接口、組件和工具,並給出大量說明性示例。

3.1. 結構

在編譯類的內部,方法的代碼存儲爲一系列的字節碼指令。爲生成和轉換類,最根本的就 是 要了解這些指令,並理解它們是如何工作的。本節將對這些指令進行全面概述,這些內容足 以開 始編寫簡單的類生成器與轉換器代碼。如需完整定義,應當閱讀 Java 虛擬機規範。

3.1.1. 執行模型

在介紹字節代碼指令之前,有必要先來介紹 Java 虛擬機執行模型。我們知道,Java 代碼 是 在線程內部執行的。每個線程都有自己的執行棧,棧由幀組成。每個幀表示一個方法調用: 每次 調用一個方法時,會將一個新幀壓入當前線程的執行棧。當方法返回時,或者是正常返 回,或者 是因爲異常返回,會將這個幀從執行棧中彈出,執行過程在發出調用的方法中繼續進 行(這個方 法的幀現在位於棧的頂端)。

每一幀包括兩部分:一個局部變量部分和一個操作數棧部分。局部變量部分包含可根據索 引 以隨機順序訪問的變量。由名字可以看出,操作數棧部分是一個棧,其中包含了供字節代碼 指令用作操作數的值。這意味着這個棧中的值只能按照“後入先出”順序訪問。不要將操作數 棧和線 程的執行棧相混淆:執行棧中的每一幀都包含自己的操作數棧

局部變量部分與操作數棧部分的大小取決於方法的代碼。這一大小是在編譯時計算的,並 隨 字節代碼指令一起存儲在已編譯類中。因此,對於對應於某一給定方法調用的所有幀,其局 部變 量與操作數棧部分的大小相同,但對應於不同方法的幀,這一大小可能不同。
圖 3.1 一個具有 3 幀的執行棧
圖 3.1 一個具有 3 幀的執行棧
圖 3.1 給出了一個具有 3 幀的示例執行棧。第一幀包含 3 個局部變量,其操作數棧的最大值 爲 4,其中包含兩個值。第二幀包含 2 個局部變量,操作數棧中有兩個值。最後是第三幀,位 於執行棧的頂端,包含 4 個局部變量和兩個操作數。

在創建一個幀時,會將其初始化,提供一個空棧,並用目標對象 this(對於非靜態方 法) 及該方法的參數來初始化其局部變量。例如,調用方法 a.equals(b) 將創建一幀,它有 一個空 棧,前兩個局部變量被初始化爲 ab(其他局部變量未被初始化)。

局部變量部分和操作數棧部分中的每個槽(slot)可以保存除 longdouble 變量之外 的 任意 Java 值。long 和 double 變量需要兩個槽。這使局部變量的管理變得複雜:例 如,第 i 個 方法參數不一定存儲在局部變量 i 中。例如,調用 Math.max(1L, 2L) 創建一 個幀,1L 值位 於前兩個局部變量槽中,值 2L 存儲在第三和第四個槽中。

3.1.2. 字節碼指令

字節代碼指令由一個標識該指令的操作碼和固定數目的參數組成:

  • 操作碼( opcode )是一個無符號字節(unsigned byte)值——即字節代碼名,由助記符號標識。例如,操作碼 0 用 助 記符號 NOP 表示,對應於不做任何操作的指令。
  • 參數是靜態值,確定了精確的指令行爲。它們緊跟在操作碼之後給出。比如 GOTO 標 記 指令(其操作碼的值爲 167)以一個指明下一條待執行指令的標記作爲參數標 記。不要 將指令參數與指令操作數相混淆:參數值是靜態已知的,存儲在編譯後的 代碼中,而 操作數值來自操作數棧,只有到運行時才能知道。

字節代碼指令可以分爲兩類:

  • 少部分指令,被設計用來在局部變量和操作數棧之間傳送值;
  • 其 他一些指令僅用於操作數棧:它們從棧中彈出一些值,根據這些值計算一個結果,並將它壓回棧 中。

ILOAD, LLOAD, FLOAD, DLOAD 和 ALOAD 指令讀取一個局部變量,並將它的值壓到操 作數棧中。 它們的參數是必須讀取的局部變量的索引 i。ILOAD 用於加載一個 boolean、byte、 char、short 或 int 局部變量。LLOAD、FLOAD 和 DLOAD 分別用於加載 long、float 或 double
值。(LLOAD 和 DLOAD 實際加載兩個槽 i 和 i+1)。最後,ALOAD 用於加載任意非基本類型值,即對 象和數組引用。與之對應,ISTORE、LSTORE、FSTORE、DSTOREASTORE指 令從操作數棧 中彈出一個值,並將它存儲在由其索引 i 指定的局部變量中。

可以看到,xLOADxSTORE 指令被賦入了類型(事實上,下面將要看出,幾乎所有指令 都被賦予了類型)。它用於確保不會執行非法轉換。實際上,將一個值存儲在局部變量中,然後 再 以不同類型加載它,是非法的。例如,ISTORE 1 ALOAD 1 序列是非法的——它允許將一個 任 意內存位置存儲在局部變量 1 中,並將這個地址轉換爲對象引用!但是,如果向一個局部變 量中存儲一個值,而這個值的類型不同於該局部變量中存儲的當前值,卻是完全合法的。這意味着一個局部變量的類型,即這個局部變量中所存值的類型可以在方法執行期間發生變化。

上面已經說過,所有其他字節代碼指令都僅對操作數棧有效。它們可以劃分爲以下類別(見 附件 A.1):

  • stack棧 這些指令用於處理棧上的值:POP 彈出棧頂部的值,DUP 壓入頂部棧值的一個副本, SWAP 彈出兩個值,並按逆序壓入它們,等等。
  • Constants常量 這些指令在操作數棧壓入一個常量值:ACONST_NULL壓入nullICONST_0壓入 int 值 0,FCONST_0 壓入 0fDCONST_0 壓入 0dBIPUSH b 壓入字節值 bSIPUSH s 壓入 short 值 s,LDC cst 壓入任意 int、float、long、double、String 或 class常量 cst,等等。
  • Arithmetic and logic算術與邏輯 這些指令從操作數棧彈出數值,合併它們,並將結果壓入棧中。它們沒有任何 參數。xADD、xSUB、xMUL、xDIV 和 xREM 對應於**+、-、*、/和%運算,其中 x 爲 I、 L、F 或 D 之一。類似地,還有其他對應於<<、>>、>>>、|、&和^**運算的指令,用於處理 int 和 long 值。
  • Casts類型變換 這些指令從棧中彈出一個值,將其轉換爲另一類型,並將結果壓入棧中。它們對
    應於 Java 中的類型轉換表達式。I2F, F2D, L2D 等將數值由一種數值類型轉換爲另一種類型。CHECKCAST t 將一個引用值轉換爲類型 t。
  • Objects對象 這些指令用於創建對象、鎖定它們、檢測它們的類型,等等。例如,NEW type 指令將
    一個 type 類型的新對象壓入棧中(其中 type 是一個內部名)。
  • Fields字段 這些指令讀或寫一個字段的值。GETFIELD owner name desc 彈出一個對象引用,並壓入其 name 字段中的值。PUTFIELD owner name desc 彈出一個值和一個對象引用,並 將這個值存儲在它的 name 字段中。在這兩種情況下,該對象都必須是 owner 類型,它的字段必須爲 desc 類型。GETSTATICPUTSTATIC 是類似指令,但用於靜態字段。
  • METHODS 方法 這些指令調用一個方法或一個構造器。它們彈出值的個數等於其方法參數個數加 1 (用於目標對象),並壓回方法調用的結果。INVOKEVIRTUAL owner name desc 調用在 類 owner 中定義的 name 方法,其方法描述符爲 descINVOKESTATIC 用於靜態方法, INVOKESPECIAL 用於私有方法和構造器,INVOKEINTERFACE 用於接口中定義的方法。最後,對於 Java 7 中的類,INVOKEDYNAMIC 用於新動態方法調用機制。
  • Arrays數組 這些指令用於讀寫數組中的值。xALOAD指令彈出一個索引和一個數組,並壓入此索
    引處數組元素的值。xASTORE 指令彈出一個值、一個索引和一個數組,並將這個值存儲在該數組的這一索引處。這裏的 x 可以是 I、L、F、D 或 A,還可以是 B、C 或 S
  • Jumps 跳轉 這些指令無條件地或者在某一條件爲真時跳轉到一條任意指令。它們用於編譯if、 for、do、while、breakcontinue 指令。例如,IFEQ label 從棧中彈出一個 int 值,如果這個值爲 0,則跳轉到由這個 label 指定的指令處(否則,正常執行下一條指令)。還有許多其他跳轉指令,比如 IFNE 或 IFGE。最後,TABLESWITCH 和 1 對應於 identifier.class Java 語法。方法 LOOKUPSWITCH 對應於 switch Java 指令。
  • Return 返回 最後,xRETURN 和 RETURN 指令用於終止一個方法的執行,並將其結果返回給調 用 者。RETURN 用於返回 void 的方法,xRETURN 用於其他方法。

3.1.3. 示例

讓我們看一些基本示例,具體體會一下字節代碼指令是如何工作的。考慮下面的 bean 類:

package pkg;
public class Bean {
	private int f;
    public int getF() {
        return this.f;
    }
    public void setF(int f) {
          this.f = f;
	} 
}

getter 方法的字節代碼爲

	// 初始化話是局部變量棧第一個元素爲this
	// ALOAD 用於加載任意非基本類型值到操作數棧,即對 象和數組引用
	// 0是局部變量的索引,就是第一個所以是this
	// 取出局部變量棧,索引0,壓入操作數棧
 	ALOAD 0
 	// GETFIELD owner name desc 
 	// 彈出一個值和對象引用,並將值壓入其 name 字段中的值
 	// 這裏取出F壓入棧中
    GETFIELD pkg/Bean f I
    // 彈出INT值,f
    IRETURN

第一條指令讀取局部變量 0(它在爲這個方法調用創建幀期間被初始化爲 this),並將這個 值壓入操作數棧中。第二個指令從棧中彈出這個值,即 this,並將這個對象的 f 字段壓入棧中,即 this.f。最後一條指令從棧中彈出這個值,並將其返回給調用者。圖 3.2 中給出了這個方法 執行幀的持續狀態。
在這裏插入圖片描述
圖 3.2 getF 方法的持續幀狀態:a) 初始狀態,b) 在 ALOAD 0 之後,c) 在 GETFIELD 之後

setter 方法的字節代碼:

	// 初始化時,壓入局部變量 f 與 this如下圖 a
    ALOAD 0
    // 局部變量棧彈出第一個值-this,進入操作數棧 如 b
    ILOAD 1
    // 局部變量棧再彈出第二個值,進入操作數棧 如 c
    PUTFIELD pkg/Bean f I
    // PUTFIELD 彈出一個對象和引用,存儲在name f中
    RETURN

在這裏插入圖片描述
圖 3.3 setF 方法的持續狀態:a) 初始狀態,b) 在 ALOAD 0 之後,c)在 ILOAD 1 之後,d) 在PUTFIELD 之後

和之前一樣,第一條指令將 this 壓入操作數棧。第二條指令壓入局部變量 1,在爲這個方 法調用創建幀期間,以 f 參數初始化該變量。第三條指令彈出這兩個值,並將 int 值存儲在被 引用對象的 f 字段中,即存儲在 this.f 中。最後一條指令在源代碼中是隱式的,但在編譯後的代碼中卻是強制的,銷燬當前執行幀,並返回調用者。這個方法執行幀的持續狀態如圖 3.3 所示。

構造器

Bean 類還有一個默認的公有構造器,由於程序員沒有定義顯式的構造器,所以它是由編譯 器生成的。這個默認的公有構造器被生成爲 Bean() { super(); }。這個構造器的字節代碼 如下:

 	ALOAD 0
    INVOKESPECIAL java/lang/Object <init> ()V
    RETURN

第一條指令將 this 壓入操作數棧中。第二條指令從棧中彈出這個值,並調用在 Object 對 象中定義的 <init> 方法。這對應於 super() 調用,也就是對超類 Object 構造器的調用。 在這裏可以看到,在已編譯類和源類中對構造器的命名是不同的:在編譯類中,它們總是被命 名 爲 <init>,而在源類中,它們的名字與定義它們的類同名。最後一條指令返回調用者。

更復雜的一個方法

public void checkAndSetF(int f){ 
	if (f >= 0) {
       this.f =f; 
    } else {
       throw new IllegalArgumentException();
	} 
}

這個新 setter 方法的字節代碼如下:

	ILOAD 1
	IFLT label
	ALOAD 0
	ILOAD 1
	PUTFIELD pkg/Bean f I 
GOTO end
label:
	NEW java/lang/IllegalArgumentException
	DUP
	INVOKESPECIAL java/lang/IllegalArgumentException<init> ()V 
	ATHROW
end:
	RETURN

第一條指令將初始化爲 f 的局部變量 1 壓入操作數棧。IFLT 指令從棧中彈出這個值,並將它與 0 進行比較。如果它小於**(LT)0**,則跳轉到由 label 標記指定的指令,否則不做任何事 情,繼續執行下一條指令。接下來的三條指令與 setF 方法中相同。GOTO 指令無條件跳轉到由 end 標記指定的指令,也就是 RETURN 指令。labelend 標記之間的指令創建和拋出一個 異 常:NEW 指令創建一個異常對象,並將它壓入操作數棧中。DUP 指令在棧中重複這個值。 INVOKESPECIAL 指令彈出這兩個副本之一,並對其調用異常構造器。最後,ATHROW 指令彈出 剩下的副本,並將它作爲異常拋出(所以不會繼續執行下一條指令)。

3.1.4. 異常處理器

不存在用於捕獲異常的字節代碼:而是將一個方法的字節代碼與一個異常處理器列表關聯在一起,這個列表規定了在某方法中一給定部分拋出異常時必須執行的代碼。異常處理器類似於 try catch 塊:它有一個範圍,也就是與 try 代碼塊內容相對應的一個指令序列,還有一個處 理器,對應於 catch 塊中的內容。這個範圍由一個起始標記和一個終止標記指定,處理器由一 個起始標記指定。比如下面的源代碼:

 public static void sleep(long d) {
        try {
          Thread.sleep(d);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
}

可被編譯爲

TRYCATCHBLOCK try catch catch java/lang/InterruptedException 
try:
	LLOAD 0
	INVOKESTATIC java/lang/Thread sleep (J)V
	RETURN
catch:
	INVOKEVIRTUAL java/lang/InterruptedException printStackTrace ()V
	RETURN

Trycatch 標記之間的代碼對應於 try 塊,而 catch 標記之後的代碼對應於 catchTRYCATCHBLOCK 行指定了一個異常處理器,覆蓋了 trycatch 標記之間的範圍,有一個開 始 於 catch 標記的處理器,用於處理一些異常,這些異常的類是 InterruptedException 的子類。這意味着,如果在 trycatch 之間拋出了這樣一個異常,棧將被清空,異常被 壓入 這個空棧中,執行過程在 catch 處繼續。

3.1.5. 幀 frame

除了字節代碼指令之外,用 Java 6 或更高版本編譯的類中還包含一組棧映射幀,用於加快 Java 虛擬機中類驗證過程的速度。棧映射幀給出一個方法的執行幀在執行過程中某一時刻的狀 態。更準確地說,它給出了在就要執行某一特定字節代碼指令之前,每個局部變量槽和每個操作 數棧槽中包含的值的類型。
例如,如果考慮上一節的 getF 方法,可以定義三個棧映射幀,給出執行幀在即將執行ALOAD、即將執行 GETFIELD 和即將執行 IRETURN 之前的狀態。這三個棧映射幀對應於圖 3.2 給出的三種情況,可描述如下,其中第一個方括號中的類型對應於局部變量,其他類型對應於操 作數棧:

如下代碼之前的執行幀狀態 指令
[pkg/Bean] [] ALOAD 0
[pkg/Bean] [pkg/Bean] GETFIELD
[pkg/Bean] [I] IRETURN

可以對 checkAndSetF 方法進行相同操作:

如下代碼之前的執行幀狀態
[pkg/Bean I] [] ILOAD 1
[pkg/Bean I] [I] IFLT label
[pkg/Bean I] [] ALOAD 0
[pkg/Bean I] [pkg/Bean] ILOAD 1
[pkg/Bean I] [pkg/Bean I] [pkg/Bean I] [] PUTFIELD
[pkg/Bean I] [] GOTO end
[pkg/Bean I] [] label :
[pkg/Bean I] [Uninitialized(label)] NEW
[pkg/Bean I] [Uninitialized(label) Uninitialized(label)] [pkg/Bean I] INVOKESPECIAL
[pkg/Bean I] [] end :
[pkg/Bean I] [] RETURN

除了 Uninitialized(label) 類型之外,它與前面的方法均類似。這是一種僅在棧映射幀 中使用的特殊類型,它指定了一個對象,已經爲其分配了內存,但還沒有調用其構造器。參數規 定了創建此對象的指令。對於這個類型的值,只能調用一種方法,那就是構造器。在調用它時, 在 幀中出現的所有這一類型都被代以一個實際類型,這裏是 IllegalArgumentException。 棧映 射幀可使用三種其他特殊類型:UNINITIALIZED_THIS 是構造器中局部變量 0 的初始類 型, TOP 對應於一個未定義的值,而 NULL 對應於 null

上文曾經說過,從 Java 6 開始,除了字節代碼之外,已編譯類中還包含了一組棧映射幀。爲 節省空間,已編譯方法中並沒有爲每條指令包含一個幀:事實上,它僅爲那些對應於跳轉目標或 異常處理器的指令,或者跟在無條件跳轉指令之後的指令包含幀。事實上,可以輕鬆、快速地由 這些幀推斷出其他幀。

checkAndSetF 方法的情景中,這意味着僅存儲兩個幀:一個用於 NEW 指令,因爲 它是 IFLT 指令的目標,還因爲它跟在無條件跳轉 GOTO 指令之後,另一個用於 RETURN 指令,因爲 它是 GOTO 指令的目標,還因爲它跟在“無條件跳轉”ATHROW 指令之後。

爲節省更多空間,對每一幀都進行壓縮:僅存儲它與前一幀的差別,而初始幀根本不用存 儲, 可以輕鬆地由方法參數類型推導得出。在 checkAndSetF 方法中,必須存儲的兩幀是相 同的, 都等於初始幀,所以它們被存儲爲單字節值,由 F_SAME 助記符表示。可以在與這些 幀相關聯 的字節代碼指令之前給出這些幀。這就給出了 F_SAME 方法的最終字節代碼:

	ILOAD 1
	IFLT label
	ALOAD 0
	ILOAD 1
	PUTFIELD pkg/Bean f I GOTO end
label:
F_SAME
    NEW java/lang/IllegalArgumentException
    DUP
    INVOKESPECIAL java/lang/IllegalArgumentException <init> ()V
    ATHROW
end:
F_SAME
	RETURN

3.2. 接口和組件

3.2.1. 介紹

用於生成和轉換已編譯方法的 ASM API 是基於 MethodVisitor 抽象類的(見圖 3.4),它 由 ClassVisitorvisitMethod 方法返回。除了一些與註釋和調試信息有關的方法之外(這些方法在下一章解釋),這個類爲每個字節代碼指令類別定義了一個方法,其依據就是這些指令 的 參數個數和類型(這些類別並非對應於 3.1.2 節給出的類別)。這些方法必須按以下順序調用(在 MethodVisitor 接口的 Javadoc 中還規定了其他一些約束條件):

visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )* 
( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn | visitLocalVariable | visitLineNumber )*
visitMaxs )?
 visitEnd

這就意味着,對於非抽象方法,如果存在註釋和屬性的話,必須首先訪問它們,然後是該方 法的字節代碼。對於這些方法,其代碼必須按順序訪問,位於對 visitCode 的調用(有且僅有 一個調用)與對 visitMaxs 的調用(有且僅有一個調用)之間。

abstract class MethodVisitor { 
// public accessors ommited MethodVisitor(int api);
	MethodVisitor(int api, MethodVisitor mv);
	AnnotationVisitor visitAnnotationDefault();
	AnnotationVisitor visitAnnotation(String desc, boolean visible);
	AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible);
	void visitAttribute(Attribute attr);
	void visitCode();
	void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack);
	void visitInsn(int opcode);
	void visitIntInsn(int opcode, int operand);
	void visitVarInsn(int opcode, int var);
	void visitTypeInsn(int opcode, String desc);
	void visitFieldInsn(int opc, String owner, String name, String desc); 
	void visitMethodInsn(int opc, String owner, String name, String desc); 
	void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs);
	void visitJumpInsn(int opcode, Label label);
	void visitLabel(Label label);
	void visitLdcInsn(Object cst);
	void visitIincInsn(int var, int increment);
	void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels); void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels);
	void visitMultiANewArrayInsn(String desc, int dims);
	void visitTryCatchBlock(Label start, Label end, Label handler, String type);
    void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index);
	void visitLineNumber(int line, Label start); 
	void visitMaxs(int maxStack, int maxLocals); 
	void visitEnd();
}

圖 3.4 MethodVisitor 類
於是,visitCode 和 visitMaxs 方法可用於檢測該方法的字節代碼在一個事件序列中的 開始與結束。和類的情況一樣,visitEnd 方法也必須在最後調用,用於檢測一個方法在一個事 件序列中的結束。
可以將 ClassVisitor 和 MethodVisitor 類合併,生成完整的類:

ClassVisitor cv = ...;
      cv.visit(...);
      MethodVisitor mv1 = cv.visitMethod(..., "m1", ...);
      mv1.visitCode();
      mv1.visitInsn(...);
      ...
      mv1.visitMaxs(...);
      mv1.visitEnd();
      MethodVisitor mv2 = cv.visitMethod(..., "m2", ...);
      mv2.visitCode();
      mv2.visitInsn(...);
      ...
      mv2.visitMaxs(...);
      mv2.visitEnd();
      cv.visitEnd();

注意,並不一定要在完成一個方法之後才能開始訪問另一個方法。事實上,MethodVisitor 實例是完全獨立的,可按任意順序使用(只要還沒有調用 cv.visitEnd()):

	  ClassVisitor cv = ...;
      cv.visit(...);
      MethodVisitor mv1 = cv.visitMethod(..., "m1", ...);
      mv1.visitCode();
      mv1.visitInsn(...);
      ...
      MethodVisitor mv2 = cv.visitMethod(..., "m2", ...);
      mv2.visitCode();
      mv2.visitInsn(...);
      ...
      mv1.visitMaxs(...);
      mv1.visitEnd();
      ...
      mv2.visitMaxs(...);
      mv2.visitEnd();
      cv.visitEnd();

ASM 提供了三個基於 MethodVisitor API 的核心組件,用於生成和轉換方法:

  • ClassReader 類分析已編譯方法的內容 , 在其 accept 方法的參數中傳送了 ClassVisitor ,ClassReader 類 將 針 對 這 一 ClassVisitor 返 回的 MethodVisitor 對象調用相應方法。
  • ClassWriter 的 visitMethod 方法返回 MethodVisitor 接口的一個實現,它直接以二進制形式生成已編譯方法。
  • MethodVisitor 類將它接收到的所有方法調用委託給另一個 MethodVisitor 方法。 可以將它看作一個事件篩選器。

ClassWriter 選項

在 3.1.5 節已經看到,爲一個方法計算棧映射幀並不是非常容易:必須計算所有幀,找出與 跳轉目標相對應的幀,或者跳在無條件跳轉之後的幀,最後壓縮剩餘幀。與此類似,爲一個方法 計算局部變量與操作數棧部分的大小要容易一些,但依然算不上非常容易。

幸好 ASM 能爲我們完成這一計算。在創建 ClassWriter 時,可以指定必須自動計算哪些 內容

  • 在使用 new ClassWriter(0) 時,不會自動計算任何東西。必須自行計算幀、局部 變 量與操作數棧的大小。
  • 在使用 new ClassWriter(ClassWriter.COMPUTE_MAXS) 時,將爲你計算局部變量
    與操作數棧部分的大小。還是必須調用 visitMaxs,但可以使用任何參數:它們將被 忽略並重新計算。使用這一選項時,仍然必須自行計算這些幀。
  • new ClassWriter(ClassWriter.COMPUTE_FRAMES) 時,一切都是自動計算。 不 再需要調用 visitFrame,但仍然必須調用 visitMaxs(參數將被忽略並重新計 算)。

這些選項的使用很方便,但有一個代價:COMPUTE_MAXS 選項使 ClassWriter 的速度降 低 10%,而使用 COMPUTE_FRAMES 選項則使其降低50%。這必須與我們自行計算時所耗費的時 間進行比較:在特定情況下,經常會存在一些比 ASM 所用算法更容易、更快速的計算方法,但 ASM 使用的算法必須能夠處理所有情況。
注意,如果選擇自行計算這些幀,可以讓 ClassWriter 爲你執行壓縮步驟。爲此,只 需 要用 **visitFrame(F_NEW, nLocals, locals, nStack, stack)**訪問未壓縮幀,其 中的 nLocalsnStack 是局部變量的個數和操作數棧的大小,locals 和 stack 是包 含相應類 型的數組(更多細節請參閱 Javadoc)。

還要注意,爲了自動計算幀, 有時需要計算兩個給定類的公共超類。默認情況下, ClassWriter 類會在 getCommonSuperClass 方法中進行這一計算,它會將兩個類加載到JVM 中,並使用反射 API。如果我們正在生成幾個相互引用的類,那可能會導致問題,因爲被 引用 的類可能尚未存在。在這種情況下,可以重寫 getCommonSuperClass 方法來解決這一問題。

3.2.2. 生成方法

對於方法的生成就是主要是理解幀和棧,操作數模型,然後去構造

 	  mv.visitCode();
      mv.visitVarInsn(ALOAD, 0);
      mv.visitFieldInsn(GETFIELD, "pkg/Bean", "f", "I");
      mv.visitInsn(IRETURN);
      mv.visitMaxs(1, 1);
      mv.visitEnd();

第一個調用啓動字節代碼的生成過程。然後是三個調用,生成這一方法的三條指令(可以看 出,字節代碼與 ASM API 之間的映射非常簡單)。對 visitMaxs 的調用必須在已經訪問了所有 這些指令後執行。它用於爲這個方法的執行幀定義局部變量和操作數棧部分的大小。在 3.1.3 節 可以看出,這些大小爲每部分 1 個槽,最後一次調用用於結束此方法的生成過程。

setF 方法和構造器的字節代碼可以用一種類似方法生成。一個更有意義的示例是 checkAndSetF 方法:

	mv.visitCode();
	mv.visitVarInsn(ILOAD, 1);
	Label label = new Label();
	mv.visitJumpInsn(IFLT, label);
	mv.visitVarInsn(ALOAD, 0);
	mv.visitVarInsn(ILOAD, 1);
	mv.visitFieldInsn(PUTFIELD, "pkg/Bean", "f", "I");
	Label end = new Label();
	mv.visitJumpInsn(GOTO, end);
	mv.visitLabel(label); mv.visitFrame(F_SAME,0, null, 0, null);
	mv.visitTypeInsn(NEW, "java/lang/IllegalArgumentException"); 			
	mv.visitInsn(DUP);
	mv.visitMethodInsn(INVOKESPECIAL,"java/lang/IllegalArgumentException", "<init>", "()V"); 
	mv.visitInsn(ATHROW);
	mv.visitLabel(end);
	mv.visitFrame(F_SAME, 0, null, 0, null); mv.visitInsn(RETURN);
	mv.visitMaxs(2, 2);
    mv.visitEnd();

visitCode 和 visitEnd 調用之間,可以看到恰好映射到 3.1.5 節末尾所示字節代碼的 方法調用:每條指令、標記或幀分別有個調用(僅有的例外是 label 和 end Label 對象的聲明 和構造)。

注意:Label 對象規定了跟在這一標記的 visitLabel 之後的指令。例如,end 規定了 RETURN 指令,
而不是隨後馬上要訪問的幀,因爲它不是一條指令。用幾條標記指定同一指令是完全合法的,但一個標記 只能 恰好指定一條指令。換句話說,有可能用不同標記對 visitLabel 進行連續調用,但一條指令中的 一個標 記則必須用 visitLabel 恰好訪問一次。最後一條約束是,標記不能共享,每個方法都必須擁有 自己的標記。

3.2.3. 轉換方法

你現在應當已經猜到,方法可以像類一樣進行轉換,也就是使用一個方法適配器將它收到的 方法調用轉發出去,並進行一些修改:改變參數可用於改變各具體指令;不轉發某一收到的調用 將刪除一條指令;在接收到的調用之間插入調用,將增加新的指令。MethodVisitor 類提供 了這樣一種方法適配器的基本實現,它只是轉發它接收到的所有方法,而未做任何其他事情。

爲了理解可以如何使用方法適配器,讓我們考慮一種非常簡單的適配器,刪除方法中的 NOP 指令(因爲它們不做任何事情,所以刪除它們沒有任何問題):

public class RemoveNopAdapter extends MethodVisitor {

	public RemoveNopAdapter(MethodVisitor mv) {
          super(ASM4, mv);
        }
     @Override
    public void visitInsn(int opcode) {
          if (opcode != NOP) {
            mv.visitInsn(opcode);
		}
	} 
}

這個適配器可以在一個類適配器內部使用,如下所示:

public class RemoveNopClassAdapter extends ClassVisitor {
        public RemoveNopClassAdapter(ClassVisitor cv){
          super(ASM4, cv);
        }
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { 
        	MethodVisitor mv;
			mv = cv.visitMethod(access, name, desc, signature, exceptions); 
			if (mv != null) {
           	 	mv = new RemoveNopAdapter(mv);
         	 }
			return mv; 
		}
}

換言之,類適配器只是構造一個方法適配器(封裝鏈中下一個類訪問器返回的方法訪問器), 並返回這個適配器。其效果就是構造了一個類似於類適配器鏈的方法適配器鏈(見圖 3.5)。
在這裏插入圖片描述
圖 3.5 RemoveNopAdapter 的程序圖

但注意,這種相似性並非強制的:完全有可能構造一個與類適配器鏈不相似的方法適配器鏈。 每種方法甚至還可以有一個不同的方法適配器鏈。例如,類適配器可以選擇僅刪除方法中的 NOP, 而不移除構造器中的該指令。可以執行如下:

mv = cv.visitMethod(access, name, desc, signature, exceptions); 
if (mv != null && !name.equals("<init>")) {
} 
mv = new RemoveNopAdapter(mv);
...

在這種情況下,構造器的適配器鏈更短一些。與之相反,構造器的適配器鏈也可以更長一些, 在 visitMethod 內部創建幾個鏈接在一起的適配器。方法適配器鏈的拓撲結構甚至都可以不 同於類適配器。例如,類適配器可能是線性的,而方法適配器鏈具有分支:


      public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) {
        	MethodVisitor mv1, mv2;
        	mv1 = cv.visitMethod(access, name, desc, signature, exceptions);
        	mv2 = cv.visitMethod(access, "_" + name, desc, signature, exceptions);
        	return new MultiMethodAdapter(mv1, mv2);
}

現在已經明白瞭如何使用方法適配器,將它們合併在一個類適配器內部,現在就來看看如 何 實現一個比RemoveNopAdapter 更有意義的適配器

3.2.4. 無狀態轉換

假設我們需要測量一個程序中的每個類所花費的時間。我們需要在每個類中添加一個靜態計時器字段,並需要將這個類中每個方法的執行時間添加到這個計時器字段中。換句話說,有這樣 一個類 C:

public class C {
	public void m() throws Exception {
          Thread.sleep(100);
	} 
}

我們希望將它轉換爲:

public class C {
	public static long timer;
	public void m() throws Exception {
		timer -= System.currentTimeMillis();
		Thread.sleep(100);
		timer += System.currentTimeMillis();
} }

爲了瞭解可以如何在 ASM 中實現它, 可以編譯這兩個類, 並針對這兩個版本比較TraceClassVisitor 的輸出(或者是使用默認的 Textifier 後端,或者是使用 ASMifier 後端)。使用默認後端時,得到下面的差異之處(以粗體表示):

GETSTATIC C.timer : J // 粗
INVOKESTATIC java/lang/System.currentTimeMillis()J // 粗 
LSUB // 粗
PUTSTATIC C.timer : J // 粗
LDC 100
INVOKESTATIC java/lang/Thread.sleep(J)V
GETSTATIC C.timer : J // 粗
INVOKESTATIC java/lang/System.currentTimeMillis()J // 粗 
LADD // 粗
PUTSTATIC C.timer : J // 粗
RETURN 
MAXSTACK = 4
MAXLOCALS = 1

可以看到,我們必須在方法的開頭增加四條指令,在返回指令之前添加四條其他指令。還需 要 更新操作數棧的最大尺寸。此方法代碼的開頭部分用 visitCode 方法訪問。因此,可以通過 重寫方法適配器的這一方法,添加前四條指令:

public void visitCode() {
	mv.visitCode();
	mv.visitFieldInsn(GETSTATIC, owner, "timer", "J"); 
	mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
            "currentTimeMillis", "()J");
	mv.visitInsn(LSUB);
	mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}

其中的 owner 必須被設定爲所轉換類的名字。現在必須在任意 RETURN 之前添加其他四條指令,還要在任何 xRETURN 或 ATHROW 之前添加,它們都是終止該方法執行過程的指令。這些指令沒有任何參數,因此在 visitInsn 方法中訪問。於是,可以重寫這一方法,以增加指令:

 public void visitInsn(int opcode) {
        if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
          	mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
          	mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
              "currentTimeMillis", "()J");
          	mv.visitInsn(LADD);
          	mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
		}
       	mv.visitInsn(opcode);
      }

最後,必須更新操作數棧的最大大小。我們添加的指令壓入兩個 long 值,因此需要操作數棧中的四個槽。在此方法的開頭,操作數棧初始爲空,所以我們知道在開頭添加的四條指令需要 一 個大小爲 4 的棧。還知道所插入的代碼不會改變棧的狀態(因爲它彈出的值的數目與壓入的數 目 相同)。因此,如果原代碼需要一個大小爲 s 的棧,那轉換後的方法所需棧的最大大小爲 max(4, s)。遺憾的是,我們還在返回指令前面添加了四條指令,我們並不知道操作數棧恰在執行這些指 令 之前時的大小。只知道它小於或等於 s。因此,我們只能說,在返回指令之前添加的代碼可能 要求 操作數棧的大小達到 s+4。這種最糟情景在實際中很少發生:使用常見編譯器時,RETURN 之前的操作數棧僅包含返回值,即,它的大小最多爲 0、1 或 2。但如果希望處理所有可能情 景, 那就需要考慮最糟情景。1必須重寫 visitMaxs 方法如下:

 public void visitMaxs(int maxStack, int maxLocals) {
        mv.visitMaxs(maxStack + 4, maxLocals);
}

當然,也可以不需要爲最大棧大小操心,而是依賴 COMPUTE_MAXS 選項,此外,它會計 算 最優值,而不是最差情景中的值。但對於這種簡單的轉換,以人工更新 maxStack 並不需 要花 費太多精力。

現在就出現一個很有意義的問題:棧映射幀怎麼樣呢?原代碼不包含任何幀,轉換後的代碼 也 沒有包含,但這是因爲我們用作示例的特定代碼造成的嗎?是否在某些情況下必須更新幀呢? 答案 是否定的,因爲 1)插入的代碼並沒有改變操作數棧,2) 插入代碼中沒有包含跳轉指令,3) 原 代碼的跳轉指令(或者更正式地說,是控制流圖)沒有被修改。這意味着原幀沒有發生變化,而 且不需要爲插入代碼存儲新幀,所以壓縮後的原幀也沒有發生變化。

現在可以將所有元素一起放入相關聯的 ClassVisitorMethodVisitor 子類中:

public class AddTimerAdapter extends ClassVisitor {
        private String owner;
        private boolean isInterface;
        public AddTimerAdapter(ClassVisitor cv) {
          super(ASM4, cv);
        }
        
        @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
          	cv.visit(version, access, name, signature, superName, interfaces);
owner = name;
          	isInterface = (access & ACC_INTERFACE) != 0;
        }
        
        @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[]  exceptions) {
          MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
              exceptions);
          if (!isInterface && mv != null && !name.equals("<init>")) {
          		mv = new AddTimerMethodAdapter(mv);
}
return mv; }
        @Override public void visitEnd() {
          if (!isInterface) {
            FieldVisitor fv = cv.visitField(ACC_PUBLIC + ACC_STATIC, "timer", "J", null, null);
            if (fv != null) {
              fv.visitEnd();
              }
          }
          cv.visitEnd();
        }
        
class AddTimerMethodAdapter extends MethodVisitor {
          public AddTimerMethodAdapter(MethodVisitor mv) {
            	super(ASM4, mv);
           }
           
          @Override public void visitCode() {
            	mv.visitCode();
           		mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
            	mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
            	mv.visitInsn(LSUB);
            	mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
          }
          
          @Override public void visitInsn(int opcode) {
            if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
              mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
              mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                  "currentTimeMillis", "()J");
              mv.visitInsn(LADD);
              mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
			}
            mv.visitInsn(opcode);
          }
          
          @Override public void visitMaxs(int maxStack, int maxLocals) {
			mv.visitMaxs(maxStack + 4, maxLocals);
		}
	} 
}

這個類適配器用於實例化方法適配器(構造器除外),還用於添加計時器字段,並將被轉換 的類的名字存儲在一個可以由方法適配器訪問的字段中。

3.2.5. 有狀態轉換

上一節看到的轉換是局部的,不會依賴於在當前指令之前訪問的指令:在開頭添加的代碼 總 是相同的,而且總會被添加,對於在每個 RETURN 指令之前添加的代碼也是如此。這種轉 換稱 爲無狀態轉換。它們的實現很簡單,但只有最簡單的轉換具有這一性質。

更復雜的轉換需要記憶在當前指令之前已訪問指令的狀態。例如,考慮這樣一個轉換,它將 刪 除所有出現的 ICONST_0 IADD 序列,這個序列的操作就是加入 0,沒有什麼實際效果。顯然,
在訪問一條 IADD 指令時,只有當上一條被訪問的指令是 ICONST_0 時,才必須刪除該指令。 這就要求在方法適配器中存儲狀態。因此,這種轉換被稱爲有狀態轉換。

讓我們更仔細地研究一下這個例子。在訪問 ICONST_0 時,只有當下一條指令是 IADD 時才必須將其刪除。問題是,下一條指令還是未知的。解決方法是將是否刪除它的決定推遲到下 一 條指令:如果下一指令是 IADD,則刪除兩條指令,否則,發出 ICONST_0 和當前指令。
要實現一些刪除或替代某一指令序列的轉換,比較方便的做法是引入一個 MethodVisitor 子類,它的 visitXxx Insn 方法調用一個公用的 visitInsn() 方法:

public abstract class PatternMethodAdapter extends MethodVisitor { 
	protected final static int SEEN_NOTHING = 0;
	protected int state;
	public PatternMethodAdapter(int api, MethodVisitor mv) {
          super(api, mv);
        }
        
	@Overrid 
	public void visitInsn(int opcode) { 
		visitInsn();
		mv.visitInsn(opcode);
    }
    
    @Override 
    public void visitIntInsn(int opcode, int operan){
		visitInsn();
        mv.visitIntInsn(opcode, operand);
    }
...
	protected abstract void visitInsn(); 
	
	}

然後,上述轉換可實現如下:

 public class RemoveAddZeroAdapter extends PatternMethodAdapter {
        private static int SEEN_ICONST_0 = 1;
        public RemoveAddZeroAdapter(MethodVisitor mv) {
          super(ASM4, mv);
        }
        
        @Override public void visitInsn(int opcode) {
			if (state == SEEN_ICONST_0) {
            	if (opcode == IADD) {
              		state = SEEN_NOTHING;
              		return;
				} 
			}
          	visitInsn();
          	if (opcode == ICONST_0) {
            	state = SEEN_ICONST_0;
            	return; 
          	}
  		  	mv.visitInsn(opcode);
}
	@Override protected void visitInsn() {
  		if (state == SEEN_ICONST_0) {
    		mv.visitInsn(ICONST_0);
  		}	
  		state = SEEN_NOTHING;
	}
}

visitInsn(int) 方法首先判斷是否已經檢測到該序列。在這種情況下,它重新初始化state,並立即返回,其效果就是刪除該序列。在其他情況下,它會調用公用的 visitInsn
法,如果 ICONST_0 是最後一條被訪問序列,它就會發出該指令。於是,如果當前指令是 ICONST_0,它會記住這個事實並返回,延遲關於這一指令的決定。在所有其他情況下,當前指 令都被轉發到下一訪問器。

標籤和幀 Labels and frames

在前幾節已經看到,對Labels和frames的訪問是恰在它們的相關指令之前進行。換句話說,儘管它 們 本身並不是指令,但它們是與指令同時受到訪問的。這對於檢測指令序列的轉換會有影響,但 這一影響實際上是一種優勢。事實上,如果刪除的指令之一是一條跳轉指令的目標,會發生什麼情況呢?如果某一指令可能跳轉到 ICONST_0,這意味着有一個指定這一指令的標記。在刪除 了這兩條指令後,這個標記將指向跟在被刪除 IADD 之後的指令,這正是我們希望的。但如 果某一指令可能跳轉到 IADD,我們就不能刪除這個指令序列(不能確保在這一跳轉之前, 已 經在棧中壓入了一個 0)。幸好,在這種情況下,ICONST_0 和 IADD 之間必然有一個標 記,可以很輕鬆地檢測到它。

這一推理過程對於棧映射幀是一樣的:如果訪問介於兩條指令之間的一個棧映射幀,那就不 能刪除它們。要處理這兩種情況,可以將標記和幀看作是模型匹配算法中的指令。這一點可以在PatternMethodAdapter 中完成(注意,visitMaxs 也會調用公用的 visitInsn 方法;它 用於處理的情景是:方法的末尾是必須被檢測序列的一個前綴):

public abstract class PatternMethodAdapter extends MethodVisitor {
        ...
        @Override public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) {
		  visitInsn();
          mv.visitFrame(type, nLocal, local, nStack, stack);
        }
		@Override 
		public void visitLabel(Label label) { 			
			visitInsn();
			mv.visitLabel(label);
        }
        @Override 
        public void visitMaxs(int maxStack, int maxLocals) {
			visitInsn();
         	mv.visitMaxs(maxStack, maxLocals);
        }
}

在下一章將會看到,編譯後的方法中可能包含有關源文件行號的信息,比如用於異常棧軌跡。 這一信息用 visitLineNumber 方法訪問,它也與指令同時被調用。但是,在一個指令序列的 中間給出行號,對於轉換或刪除該指令的可能性不會產生任何影響。解決方法是在模式匹配算法 中完全忽略它們。

一個更復雜的例子

上面的例子可以很輕鬆地推廣到更復雜的指令序列。例如,考慮一個轉換,它會刪除對字 段 進行自我賦值的操作,這種操作通常是因爲鍵入錯誤,比如 f = f;,或者是在字節代碼 中,ALOAD 0 ALOAD 0 GETFIELD f PUTFIELD f。在實現這一轉換之前,最好是將狀態 機設計爲能夠 識別這一序列(見圖 3.6)。
在這裏插入圖片描述圖 3.6 ALOAD 0 ALOAD 0 GETFIELD f PUTFIELD f 的狀態機

每個轉換都標有一個條件(當前指令的值)和一個操作(必鬚髮出的指令序列,以粗體表示)。 例 如,如果當前指令不是 ALOAD 0,則由 S1 轉換到 S0。在這種情況下,導致進入這一狀態的
ALOAD 0 將被髮出。注意從 S2 到其自身的轉換:在發現三個或三個以上的連續 ALOAD 0 時會 發 生這一情況。在這種情況下,將停留在已經訪問兩個 ALOAD 0 的狀態中,併發出第三個 ALOAD 0。找到狀態機之後,相應方法適配器的編寫就簡單了。(8 種 Switch 情景對應於圖中的 8 種轉換):

class RemoveGetFieldPutFieldAdapter extends PatternMethodAdapter {
        private final static int SEEN_ALOAD_0 = 1;
        private final static int SEEN_ALOAD_0ALOAD_0 = 2;
        private final static int SEEN_ALOAD_0ALOAD_0GETFIELD = 3;
        private String fieldOwner;
        private String fieldName;
        private String fieldDesc;
        public RemoveGetFieldPutFieldAdapter(MethodVisitor mv {
        	super(mv); 
        	}
        @Override
        public void visitVarInsn(int opcode, int var) {
          switch (state) {
          	case SEEN_NOTHING: // S0 -> S1
            	if (opcode == ALOAD && var == 0) {
              		state = SEEN_ALOAD_0;
              		return;
				}
            	break;
          	case SEEN_ALOAD_0: // S1 -> S2
            	if (opcode == ALOAD && var == 0) {
              		state = SEEN_ALOAD_0ALOAD_0;
              		return;
				}
    			break;
  			case SEEN_ALOAD_0ALOAD_0: // S2 -> S2
    			if (opcode == ALOAD && var == 0) {
      				mv.visitVarInsn(ALOAD, 0);
      			return;
				}
				break; 
		}
  		visitInsn();
  		mv.visitVarInsn(opcode, var);
}
@Override
public void visitFieldInsn(int opcode, String owner, String name,
    String desc) {
  switch (state) {
  case SEEN_ALOAD_0ALOAD_0: // S2 -> S3
    if (opcode == GETFIELD) {
      state = SEEN_ALOAD_0ALOAD_0GETFIELD;
      fieldOwner = owner;
      fieldName = name;
      fieldDesc = desc;
      return;
}
    break;
  case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 -> S0
    if (opcode == PUTFIELD && name.equals(fieldName)){
      state = SEEN_NOTHING;
      return;
	}
	break;
 	}
  visitInsn();
  mv.visitFieldInsn(opcode, owner, name, desc);
}

	@Override 
	protected void visitInsn() {
  		switch (state) {
  			case SEEN_ALOAD_0: // S1 -> S0
    			mv.visitVarInsn(ALOAD, 0);
    			break;
  			case SEEN_ALOAD_0ALOAD_0: // S2 -> S0
    			mv.visitVarInsn(ALOAD, 0);
    			mv.visitVarInsn(ALOAD, 0);
    			break;
  			case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 -> S0
    			mv.visitVarInsn(ALOAD, 0);
    			mv.visitVarInsn(ALOAD, 0);
    			mv.visitFieldInsn(GETFIELD,fieldOwner,fieldName, fieldDesc);
				break; 
			}
 		state = SEEN_NOTHING;
	}
}

注意,出於和 3.2.4 節中 AddTimerAdapter 同樣的原因,本節給出的有狀態轉換也不需要 轉換棧映射幀:原幀在轉換後仍然有效。它們甚至不需要轉換局部變量和操作數棧大小。最後, 還必須注意,有狀態轉換並不限於檢測和轉換指令序列的轉換。許多其他類型的轉換也是有狀態 的。比如,下一節介紹的方法適配器就屬於這種情景。

3.3. 工具

org.objectweb.asm.commons 包中包含了一些預定義的方法適配器,可用於定義我們 自 己的適配器。這一節將介紹其中的三個,並用 3.2.4 節的 AddTimerAdapter 示例說明如何使 用它們。我們還說說明,如何利用上一章看到的工具來簡化方法生成或轉換。

3.3.1. 基本工具

2.3 節介紹的工具也可用於方法。

Type

許多字節代碼指令,比如 xLOAD、xADD 或 xRETURN 依賴於將它們應用於哪種類型。Type 類提供了一個 getOpcode 方法,可用於爲這些指令獲取與一給定類型相對應的操作碼。這一方 法的參數是一個 int 類型的操作碼,針對哪種類型調用該方法,則返回該哪種類型的操作碼。 例 如 t.getOpcode(IMUL),若 t 等於 Type.FLOAT_TYPE,則返回 FMUL

TraceClassVisitor

這個類在上一章已經介紹過,它打印它所訪問類的文本表示,包括類的方法的文本表示,其 方式非常類似於這一章使用的方式。因此,可以將它用來跟蹤在一個轉換鏈中任意點處所生成或 所轉換方法的內容。例如:

 java -classpath asm.jar:asm-util.jar \
           org.objectweb.asm.util.TraceClassVisitor \
           java.lang.Void

將輸出:

      // class version 49.0 (49)
      // access flags 49
public final class java/lang/Void {
        // access flags 25
        // signature Ljava/lang/Class<Ljava/lang/Void;>;
        // declaration: java.lang.Class<java.lang.Void>
        public final static Ljava/lang/Class; TYPE
        // access flags 2
        private <init>()V
          ALOAD 0
          INVOKESPECIAL java/lang/Object.<init> ()V
          RETURN
          MAXSTACK = 1
          MAXLOCALS = 1
        // access flags 8
        static <clinit>()V
          LDC "void"
          INVOKESTATIC java/lang/Class.getPrimitiveClass (...)...
          PUTSTATIC java/lang/Void.TYPE : Ljava/lang/Class;
          RETURN
          MAXSTACK = 1
          MAXLOCALS = 0
}

它說明如何生成一個靜態塊 static { … },也就是用 <clinit> 方法(用於 CLass INITializer)。注意,如果希望跟蹤某一個方法在鏈中某一點處的內容,而不是跟蹤類的
所 有內容,可以用 TraceMethodVisitor 代替TraceClassVisitor(在這種情況下,必須顯 式指定後端;這裏使用了一個 Textifier):

public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {

        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
 		if (debug && mv != null && ...) {  // 如果必須跟蹤此方法
 			Printer p = new Textifier(ASM4) {
				@Override 
				public void visitMethodEnd() { 	
					// 在其被訪問後輸出它			
					print(aPrintWriter); 
				} 
			};
          mv = new TraceMethodVisitor(mv, p);
        }
		return new MyMethodAdapter(mv);
}

這一代碼輸出該方法經 MyMethodAdapter 轉換過後的結果。

CheckClassAdapter

這個類也已經在上一章介紹過,它檢查 ClassVisitor 方法的調用順序是否適當,參數是 否有效,所做的工作與 MethodVisitor 方法相同。因此,可用於檢查 MethodVisitor API 在一個轉換鏈中任意點的使用是否正常。和 TraceMethodVisitor 類似, 可以用CheckMethodAdapter 類來檢查一個方法,而不是檢查它的整個類:

public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
	if (debug && mv != null && ...) { // if this method must be checked 
		mv = new CheckMethodAdapter(mv);
	}
	return new MyMethodAdapter(mv); 
}

這一代碼驗證 MyMethodAdapter 正確地使用了 MethodVisitor API。但要注意,這一 適配器並沒有驗證字節代碼是正確的:例如,它沒有檢測出 ISTORE 1 ALOAD 1 是無效的。 實際上,如果使用 CheckMethodAdapter 的其他構造器(見 Javadoc),並且在 visitMaxs 中提供有效的 maxStack 和 maxLocals 參數,那這種錯誤是可以被檢測出來的。

ASMifier

這個類已經在上一章介紹過,也用於處理方法的內容。利用它,可以知道如何用 ASM 生成 一些編譯後的代碼:只需要用 Java 編寫相應的源代碼,用 javac 編譯它,然後用 ASMifier 訪 問這個類。你會得到 ASM 代碼,以生成與源代碼相對應的字節代碼。

3.3.2. AnalyzerAdapter

這個方法適配器根據 visitFrame 中訪問的幀,在每條指令之前計算的棧映射幀。實際上, 如 3.1.5 節中的解釋,visitFrame 僅在方法中的一些特定指令前調用,一方面是爲了節省空間, 另 一方面也是因爲“其他幀可以輕鬆快速地由這些幀推導得出”。這就是這個適配器所做的工作。 當 然,它僅對那些包含預計算棧映射幀的類有效,也就是對於用 Java 6 或更高版本編譯的有效(或 者 用一個使用 COMPUTE_FRAMES 選項的 ASM 適配器升級到 Java 6)。

在我們的 AddTimerAdapter 示例中,這個適配器可用於獲得操作數棧恰在 RETURN指令 之前的大小,從而允許爲 visitMaxs 中的 maxStack 計算一個最優的已轉換值(事實上,在實踐中並不建議使用這一方法,因爲它的效率要遠低於使用 COMPUTE_MAXS):

class AddTimerMethodAdapter2 extends AnalyzerAdapter { 
	private int maxStack;
	public AddTimerMethodAdapter2(String owner, int access, String name, String desc, MethodVisitor mv) {
          super(ASM4, owner, access, name, desc, mv);
        }
        
  	@Override public void visitCode() {
		super.visitCode();
		mv.visitFieldInsn(GETSTATIC, owner, "timer", "J"); 	
		mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J"); mv.visitInsn(LSUB);
		mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J"); 
		maxStack = 4;
 	}
   	@Override 
   	public void visitInsn(int opcode) {
          if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
            	mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
            	mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
            	mv.visitInsn(LADD);
            	mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
            	maxStack = Math.max(maxStack, stack.size() + 4);
			}
		 super.visitInsn(opcode); 
	}
	@Override 
	public void visitMaxs(int maxStack, int maxLocals) { 	
		super.visitMaxs(Math.max(this.maxStack, maxStack), maxLocals);
	}
 }

stack 字段在 AnalyzerAdapter 類中定義,包含操作數棧中的類型。更準確地說, 在一 個 visitXxx Insn 中,且在調用被重寫的方法之前,它會列出操作數棧正好在這條指 令之前的 狀態。注意,必須調用被重寫的方法,使 stack 字段被正確更新(因此,用 super 代替源代碼 中的 mv)。

或者, 也可以通過調用超類中的方法來插入新指令: 其方法就是這些指令的幀將由 AnalyzerAdapter 計算,由於這個適配器會根據它計算的幀來更新 visitMaxs 的參數,所
以我們不需要自己來更新它們:

class AddTimerMethodAdapter3 extends AnalyzerAdapter {
	public AddTimerMethodAdapter3(String owner, int access, String name, String desc, MethodVisitor mv) {
          super(ASM4, owner, access, name, desc, mv);
        }
	@Override 
	public void visitCode() {
		super.visitCode();
		super.visitFieldInsn(GETSTATIC, owner, "timer", "J"); 
		super.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J"); 
		super.visitInsn(LSUB); 	
		super.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
    }
    @Override 
    public void visitInsn(int opcode) {
		if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) { 	
			super.visitFieldInsn(GETSTATIC, owner, "timer", "J"); 
			super.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
			super.visitInsn(LADD); 
			super.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
		}
        super.visitInsn(opcode);
  	}
}

3.3.3. LocalVariablesSorter

這個方法適配器將一個方法中使用的局部變量按照它們在這個方法中的出現順序重新進行 編 號。例如,在一個有兩個參數的方法中,第一個被讀取或寫入且索引大於或等於 3 的局部變量 (前三個局部變量對應於 this 及兩個方法參數,因此不會發生變化)被賦予索引 3,第二個被賦 予索引 4,以此類推。在向一個方法中插入新的局部變量時,這個適配器很有用。沒有這個適配器,就需要在所有已有局部變量之後添加新的局部變量,但遺憾的是,在 visitMaxs 中,要直到方法的末尾處才能知道這些局部變量的編號。
爲說明如何使用這個適配器,假定我們希望使用一個局部變量來實現 AddTimerAdapter:

 public class C {
        public static long timer;
        public void m() throws Exception {
          long t = System.currentTimeMillis();
          Thread.sleep(100);
          timer += System.currentTimeMillis() - t;
		}
}

這一點很容易做到: 只需擴展 LocalVariablesSorter , 並使用這個類中定義的 newLocal 方法。

class AddTimerMethodAdapter4 extends LocalVariablesSorter { 
	private int time;
	public AddTimerMethodAdapter4(int access, String desc, MethodVisitor mv) {
          super(ASM4, access, desc, mv);
    }
    @Override 
    public void visitCode() {
          super.visitCode();
          mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
          time = newLocal(Type.LONG_TYPE);
		  mv.visitVarInsn(LSTORE, time); 
	}
    @Override
    public void visitInsn(int opcode) {
          if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
            	mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
				mv.visitVarInsn(LLOAD, time);
				mv.visitInsn(LSUB);
				mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
             	mv.visitInsn(LADD);
            	mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
          }
          super.visitInsn(opcode);
        }
        @Override 
        public void visitMaxs(int maxStack, int maxLocals) {
          	super.visitMaxs(maxStack + 4, maxLocals);
		} 
}

注意,在對局部變量重新編號後,與該方法相關聯的原幀變爲無效,在插入新局部變量後更 不必說了。幸好,還是可能避免從頭重新計算這些幀的:事實上,並不存在必須添加或刪除的 幀, 只需對原幀中局部變量的內容進行重新排序, 爲轉換後的方法獲得幀就 “ 足夠 ” 了。 LocalVariablesSorter 會自動負責完成。如果還需要爲你的方法適配器進行增量棧映射幀 更新,可以由這個類的源代碼中獲得靈感。

前面曾經說過,這個類的原版本中存在關於最糟情景下 maxStack 取值的問題,在上面可 以 看出,使用局部變量並不能解決這個問題。如果希望用 AnalyzerAdapter 解決這個問題, 除了LocalVariablesSorter 之外,必須通過委託使用這些適配器,而不是通過繼承(因爲 不可能存在多個繼承):

class AddTimerMethodAdapter5 extends MethodVisitor {
 		public LocalVariablesSorter lvs;
		public AnalyzerAdapter aa;
		private int time;
        private int maxStack;
        public AddTimerMethodAdapter5(MethodVisitor mv) {
          super(ASM4, mv);
        }
        @Override 
        public void visitCode() {
          mv.visitCode();
          mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
		  time = lvs.newLocal(Type.LONG_TYPE); 	
		  mv.visitVarInsn(LSTORE, time);
		  maxStack = 4;
        }
        @Override 
        public void visitInsn(int opcode) {
          	if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
            	mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J"); 
            	mv.visitVarInsn(LLOAD, time); 
            	mv.visitInsn(LSUB);
				mv.visitFieldInsn(GETSTATIC, owner, "timer", "J"); 
				mv.visitInsn(LADD);
				mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J"); 
				maxStack = Math.max(aa.stack.size() + 4, maxStack);
			}
			mv.visitInsn(opcode);
        	}
		@Override 
		public void visitMaxs(int maxStack, int maxLocals) { 
			mv.visitMaxs(Math.max(this.maxStack, maxStack), maxLocals);
		}
}

爲使用這個適配器,必須將一個LocalVariablesSorter 鏈 接 到 一 個AnalyzerAdapter,再將它自身連接到你的適配器:第一個適配器將對局部變量排序,並相應
地更新幀,分析適配器將計算中間幀,在此過程中會考慮上一個適配器中完成的重新編號,你
的 適配器將可以訪問這些重新編號的中間幀。這個鏈接可以在 visitMethod 中構造如下:

mv = cv.visitMethod(access, name, desc, signature, exceptions); 
if (!isInterface && mv != null && !name.equals("<init>")) {
	AddTimerMethodAdapter5 at = new AddTimerMethodAdapter5(mv); 
	at.aa = new AnalyzerAdapter(owner, access, name, desc, at); 
	at.lvs = new LocalVariablesSorter(access, desc, at.aa); 
	return at.lvs;
}

3.3.4. AdviceAdapter

這個方法適配器是一個抽象類,可用於在一個方法的開頭以及恰在任意 RETURNATHROW指令之前插入代碼。它的主要好處就是對於構造器也是有效的,在構造器中,不能將代碼恰好插 入到構造器的開頭,而是插在對超構造器的調用之後。事實上,這個適配器的大多數代碼都專門 用於檢測對這個超構造器的調用。

仔細研究 3.2.4 節中的 AddTimerAdapter 類將會看到,AddTimerMethodAdapter 因爲 這一原因而未被用於構造器。這一方法適配器從 AdviceAdapter 繼承而來,可以對其進行改進,以便對於構造器同樣有效(注意,AdviceAdapter 繼承自 LocalVariablesSorter,所以也可以輕鬆使用一個局部變量):

class AddTimerMethodAdapter6 extends AdviceAdapter {
public AddTimerMethodAdapter6(int access, String name, String desc, MethodVisitor mv) {
          super(ASM4, mv, access, name, desc);
          }
	@Override 
	protected void onMethodEnter() {
  		mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
  		mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
  		mv.visitInsn(LSUB);
  		mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
	@Override 
	protected void onMethodExit(int opcode) {
  		mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
  		mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
  		mv.visitInsn(LADD);
  		mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
	@Override 
	public void visitMaxs(int maxStack, int maxLocals) {
  		super.visitMaxs(maxStack + 4, maxLocals);
	}
}

4.元數據

本章解釋如何用核心 API 生成和轉換編譯後的 Java 類元數據,比如註釋。每一節都首先介 紹一種元數據類型,然後給出用於生成和轉換這些元數據的相應 ASM 接口、組件和工具,並給出一些說明性示例。

4.1. 泛型

諸如 List<E> 之類的泛型類,以及使用它們的類,包含了有關它們所聲明或使用的泛型的 信 息。這一信息不是由字節代碼指令在運行時使用,但可通過反射 API 訪問。它還可以供編譯 器使用,以進行分離編譯。

4.1.1. 結構

出於後向兼容的原因,有關泛型的信息沒有存儲在類型或方法描述符中(它們的定義遠早於 Java 5 中對泛型的引入),而是保存在稱爲類型、方法和類簽名的類似構造中。在涉及泛型時, 除了描述符之外,這些簽名也會存儲在類、字段和方法聲明中(泛型不會影響方法的字節代碼: 編譯器用它們執行靜態類型檢查,但會在必要時重新引入類型轉換,就像這些方法未被使用一樣 進行編譯)。

類型簽名

與類型和方法描述符不同,類型簽名的語法非常複雜,這也是因爲泛型的遞歸本質造成的(一 個泛型可以將另一泛型作爲參數——例如,考慮 List<List<E>>)。其語法由以下規則給出(有 關這些規則的完整描述,請參閱《Java 虛擬機規範》):


TypeSignature: Z | C | B | S | I | F | J | D | FieldTypeSignature 
FieldTypeSignature: ClassTypeSignature | [ TypeSignature | TypeVar 
ClassTypeSignature: L Id ( / Id )* 
TypeArgs? ( . Id TypeArgs? )* ; 
TypeArgs: < TypeArg+ >TypeArg: * | ( + | - )? FieldTypeSignature 
TypeVar: T Id ;
  • 第一條規則表明,類型簽名或者是一個基本類型描述符,或者是一個字段類型簽名。
  • 第二條 規則將一個字段類型簽名定義爲一個類類型簽名、數組類型簽名或類型變量。
  • 第三條規則定義類 類型簽名:它們是類類型描述符,在主類名之後或者內部類名之後的尖括號中可能帶有類型參數以點爲前綴)。
  • 其他規則定義了類型參數和類型變量。注意,一個類型參數可能是一個完整的 字段類型簽名,帶有它自己的類型參數:因此,類型簽名可能非常複雜(見圖 4.1)
Ljava/util/List<TE;>;
List<?> Ljava/util/List<*>;
List<? extends Number> Ljava/util/List<+Ljava/lang/Number;>;
List<? super Integer> Ljava/util/List<-Ljava/lang/Integer;>
List<List[]> Ljava/util/List<[Ljava/util/List<Ljava/lang/String;>;>;
HashMap<K, V>.HashIterator Ljava/util/HashMap<TK;TV;>.HashIterator<TK;>;

圖 4.1 類型簽名舉例

方法簽名

方法簽名擴展了方法描述符,就像類型簽名擴展了類型描述符。方法簽名描述了方法參數的 類型簽名及其返回類型的簽名。與方法描述符不同的是,它還包含了該方法所拋出異常的簽名, 前面帶有^前綴,還可以在尖括號之間包含可選的形式類型參數

MethodTypeSignature:
	TypeParams? ( TypeSignature* ) ( TypeSignature | V ) Exception*
Exception: ^ClassTypeSignature | ^TypeVar
TypeParams: < TypeParam+ >
TypeParam: Id : FieldTypeSignature? ( : FieldTypeSignature )*

比如以下泛型靜態方法的方法簽名,它以類型變量 T 爲參數: static <T> Class<? extends T> m (int n)
它是以下方法簽名:
<T:Ljava/lang/Object;>(I)Ljava/lang/Class<+TT;>;

類簽名

最後要說的是類簽名,不要將它與類類型簽名相混淆,它被定義爲其超類的類型簽名,後面 跟有所實現接口的類型簽名,以及可選的形式類型參數:

ClassSignature: TypeParams? ClassTypeSignature ClassTypeSignature*

例 如 , 一 個 被 聲 明 爲 C<E> extends List<E>的 類 的 類 籤 名 就 是
<E:Ljava/lang/Object;>Ljava/util/List<TE;>;。

4.1.2. 接口和組件

和描述符的情況一樣,也出於相同的效果原因(見 2.3.1 節),ASM API 公開簽名的形式與 它們在編譯類中的存儲形式相同(簽名主要出現在 ClassVisitor 類的 visitvisitFieldvisitMethod 方法中, 分別作爲可選類、類型或方法簽名參數)。幸好它還在org.objectweb.asm.signature 包中提供了一些基於 SignatureVisitor 抽象類的 工 具,用於生成和轉換籤名(見圖 4.2)。

public abstract class SignatureVisitor
{ 
	public final static char EXTENDS =+;
	public final static char SUPER =-;
	public final static char INSTANCEOF ==;
	public SignatureVisitor(int api);
	public void visitFormalTypeParameter(String name);
	public SignatureVisitor visitClassBound();
	public SignatureVisitor visitInterfaceBound();
	public SignatureVisitor visitSuperclass();
	public SignatureVisitor visitInterface(); 
	public SignatureVisitor visitParameterType(); 	
	public SignatureVisitor visitReturnType(); 
	public SignatureVisitor visitExceptionType(); 	
	public void visitBaseType(char descriptor); 
	public void visitTypeVariable(String name); 
	public SignatureVisitor visitArrayType();
  	public void visitClassType(String name);
	public void visitInnerClassType(String name);
	public void visitTypeArgument();
	public SignatureVisitor visitTypeArgument(char wildcard);
	public void visitEnd();
 }

圖 4.2 SignatureVisitor 類

這個抽象類用於訪問類型簽名、方法簽名和類簽名。用於類型簽名的方法以粗體顯示,必須 按 以下順序調用,它反映了前面的語法規則(注意,其中兩個返回了 SignatureVisitor: 這 是因爲類型簽名的遞歸定義導致的):

visitBaseType | visitArrayType | visitTypeVariable | ( visitClassType visitTypeArgument*
( visitInnerClassType visitTypeArgument* )* visitEnd ) )

用於訪問方法簽名的方法如下:

( visitFormalTypeParameter visitClassBound? visitInterfaceBound* )* visitParameterType* visitReturnType visitExceptionType*

最後,用於訪問類簽名的方法爲:

( visitFormalTypeParameter visitClassBound?
visitInterfaceBound* )* visitSuperClass visitInterface*

這些方法大多返回一個 SignatureVisitor: 它是準備用來訪問類型簽名的。注意,不
同 於 ClassVisitor 返 回 的 MethodVisitorsSignatureVisitor 返 回 的 SignatureVisitors 不得爲 null,而且必須順序使用:事實上,在完全訪問一個嵌套簽名之前,不得訪問父訪問器的任何方法。

和類的情況一樣,ASM API 基於這個 API 提供了兩個組件:SignatureReader 組件 分析 一個簽名,並針對一個給定的簽名訪問器調用適當的訪問方法;SignatureWriter 組 件基於 它接收到的方法調用生成一個簽名。

利用與類和方法相同的原理,這兩個類可用於生成和轉換籤名。例如,假定我們希望對出現 在 某些簽名中的類名進行重命名。這一效果可以用以下簽名適配器完成,除 visitClassTypevisitInnerClassType 方法之外,它將自己接收到的所有其他方法調用都不加修改地加以 轉發 (這裏假設 sv 方法總是返回 thisSignatureWriter 就屬於這種情況):

public class RenameSignatureAdapter extends SignatureVisitor {
	private SignatureVisitor sv;
	private Map<String, String> renaming;
	private String oldName;
	public RenameSignatureAdapter(SignatureVisitor sv, Map<String, String> renaming) {
		super(ASM4);
		this.sv = sv; 
		this.renaming = renaming;
    }
    public void visitFormalTypeParameter(String name) {
          sv.visitFormalTypeParameter(name);
    }
    public SignatureVisitor visitClassBound() {
          sv.visitClassBound();
          return this;
    }
    public SignatureVisitor visitInterfaceBound() {
          sv.visitInterfaceBound();
          return this;
    }
    ...
    public void visitClassType(String name) {
          oldName = name;
          String newName = renaming.get(oldName);
          sv.visitClassType(newName == null ? name : newName);
    }
    public void visitInnerClassType(String name) {
          oldName = oldName + "." + name;
          String newName = renaming.get(oldName);
          sv.visitInnerClassType(newName == null ? name : newName);
 	}
	public void visitTypeArgument() {
  		sv.visitTypeArgument();
	}
	public SignatureVisitor visitTypeArgument(char wildcard) {
  		sv.visitTypeArgument(wildcard);
  		return this;
	}
	public void visitEnd() {
  		sv.visitEnd();
	}
}

因此,以下代碼的結果爲"LA<TK;TV;>.B<TK;>;":

String s = "Ljava/util/HashMap<TK;TV;>.HashIterator<TK;>;"; 
Map<String, String> renaming = new HashMap<String, String>(); 
renaming.put("java/util/HashMap", "A");
renaming.put("java/util/HashMap.HashIterator", "B"); 
SignatureWriter sw = new SignatureWriter();
SignatureVisitor sa = new RenameSignatureAdapter(sw, renaming); 
SignatureReader sr = new SignatureReader(s); 
sr.acceptType(sa); 
sw.toString();

4.1.3. 工具

2.3 節給出的 TraceClassVisitorASMifier 類以內部形式打印類文件中包含的簽名。 利 用它們,可以通過以下方式找出與一個給定泛型相對應的簽名:編寫一個具有某一泛型的 Java 類,編譯它,並用這些命令行工具來找出對應的簽名。

4.2. 註解

類、字段、方法和方法參數註釋,比如**@Deprecated** 或 @Override,只要它們的保留策 略不是 RetentionPolicy.SOURCE,它們就會被存儲在編譯後的類中。這一信息不是在運 行 時供字節代碼指令使用,但是,如果保留策略是 RetentionPolicy.RUNTIME,則可以 通過 反射 API 訪問它。它還可以供編譯器使用。

4.2.1. 結構

源 代 碼 中 的 註解 可 以 具 有 各 種 不 同 形 式 , 比 如 @Deprecated 、 @Retention(RetentionPolicy.CLASS)或@Task(desc=“refactor”, id=1)。但在內 部,所有註解 的形式都是相同的,由一種註解 類型和一組名稱/值對規定,其中的取值僅限於如 下幾種:

  • 基本類型,String 或 Class 值
  • 枚舉值
  • 註解值
  • 上述值的數組形式

注意,一個註釋中可以包含其他註釋,甚至可以包含註釋數組。因此,註釋可能非常複雜。

4.2.2. 接口和組件

用於生成和轉換註釋的 ASM API 是基於 AnnotationVisitor 抽象類的(見圖 4.3)。

public abstract class AnnotationVisitor {
	public AnnotationVisitor(int api);
	public AnnotationVisitor(int api, AnnotationVisitor av);
	public void visit(String name, Object value);
	public void visitEnum(String name, String desc, String value); 
	public AnnotationVisitor visitAnnotation(String name, String desc); 
	public AnnotationVisitor visitArray(String name);
	public void visitEnd();
}

圖 4.3 AnnotationVisitor 類
這個類的方法用於訪問一個註釋的名稱/值對(註釋類型在訪問這一類型的方法中訪問,即visitAnnotation 方法)。第一個方法用於基本類型、String 和 Class 值(後者用 Type 對象表 示),其他方法用於枚舉、註釋和數組值。可以按任意順序調用它們,visitEnd 除外:

(visit | visitEnum | visitAnnotation | visitArray )* visitEnd

注意,兩個方法返回 AnnotationVisitor: 這是因爲註釋可以包含其他註釋。另外,與 ClassVisitor 返回的 MethodVisitor 不同,這兩個方法返回的 AnnotationVisitors必須順序使用:事實上,在完全訪問一個嵌套註釋之前,不能調用父訪問器的任何方法。

還要注意,visitArray 方法返回一個 AnnotationVisitor,以訪問數組的元素。但 是, 由於數組的元素未被命名,因此,name 參數被 visitArray 返回的訪問器的方法忽 略,可以 設定爲 null

添加、刪除和檢測註解

與字段和方法的情景一樣,可以通過在 visitAnnotation 方法中返回 null 來刪除註解:

 public class RemoveAnnotationAdapter extends ClassVisitor {
        private String annDesc;
        public RemoveAnnotationAdapter(ClassVisitor cv, String annDesc) {
          super(ASM4, cv);
          this.annDesc = annDesc;
        }
        @Override
        public AnnotationVisitor visitAnnotation(String desc, boolean vis) {
          if (desc.equals(annDesc)) {
            return null;
		}
          return cv.visitAnnotation(desc, vis);
        }
}

類註釋的添加要更難一些,因爲存在一些限制條件:必須調用 ClassVisitor 類的方
法。 事實上,所有可以跟在 visitAnnotation 之後的方法都必須重寫,以檢測什麼時候已
經訪問 了所有註釋(因爲 visitCode 方法的原因,方法註釋的添加更容易一些):

public class AddAnnotationAdapter extends ClassVisitor {
	private String annotationDesc;
	private boolean isAnnotationPresent;
	public AddAnnotationAdapter(ClassVisitor cv, String annotationDesc) {
          super(ASM4, cv);
          this.annotationDesc = annotationDesc;
        }
        
    	@Override 
    	public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            int v = (version & 0xFF) < V1_5 ? V1_5 :version;
			cv.visit(v, access, name, signature, superName, interfaces); }
        @Override 
        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
          if (visible && desc.equals(annotationDesc)) {
            	isAnnotationPresent = true;
		}
          return cv.visitAnnotation(desc, visible);
        }
        
        @Override 
        public void visitInnerClass(String name, String outerName, String innerName, int access) {
          	addAnnotation();
          	cv.visitInnerClass(name, outerName, innerName, access);
        }
        @Override
        public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
          addAnnotation();
          return cv.visitField(access, name, desc, signature, value);
        }
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
          addAnnotation();
          return cv.visitMethod(access, name, desc, signature, exceptions);
        }
        
        @Override 
        public void visitEnd() {
          	addAnnotation();
          	cv.visitEnd();
        }
        
        private void addAnnotation() {
          if (!isAnnotationPresent) {
            AnnotationVisitor av = cv.visitAnnotation(annotationDesc, true);
            if (av != null) {
              av.visitEnd();
            }
            isAnnotationPresent = true;
          }
	}
}

注意,如果類版本低於 1.5,這個適配器將其更新至該版本。這是必要地,因爲對於版本低 於 1.5 的類,JVM 會忽略其中的註釋。
註釋在類和方法適配器中的最後一種應用情景,也可能是最常見的應用情景,就是以註釋實 現 轉換的參數化。例如,你可能僅對於那些具有**@Persistent** 註釋的字段來轉換字段的訪問, 僅對
於那些擁有 @Log 註釋的方法添加記錄代碼,如此等等。所有這些應用情景都可以很輕鬆地 實現,因爲註釋是必須首先訪問的:必須在字段和方法之前訪問類註釋,必須在代碼之前訪問方
法和參數註釋。因此,只需在檢測到所需註釋時設定一個標誌,然後在後面的轉換中使用,就 像 上面的例子用 isAnnotationPresent 標誌所做的事情。

4.2.3. 工具

2.3節介紹的TraceClassVisitor, CheckClassAdapter和ASMifier類也支持註釋
( 就像對於方法一樣 , 還可能使 用 TraceAnnotationVisitorCheckAnnotationAdapter,在各個註釋的級別工作,而不是在類級別工作)。
它們可用於查看如何生成某個特定註釋。例如,使用以下代碼:

java -classpath asm.jar:asm-util.jar \
           org.objectweb.asm.util.ASMifier \
           java.lang.Deprecated

將輸出如下代碼(經過微小的重構)

package asm.java.lang;
import org.objectweb.asm.*;
public class DeprecatedDump implements Opcodes {
public static byte[] dump() throws Exception {
	ClassWriter cw = new ClassWriter(0);
	AnnotationVisitor av;
	cw.visit(V1_5, 
			ACC_PUBLIC + ACC_ANNOTATION + ACC_ABSTRACT + ACC_INTERFACE, 
			"java/lang/Deprecated", 
			null, 
			"java/lang/Object",
            new String[] { "java/lang/annotation/Annotation" });
{
	av = cw.visitAnnotation("Ljava/lang/annotation/Documented;", true);
	av.visitEnd();
}
	av = cw.visitAnnotation("Ljava/lang/annotation/Retention;", true);
	av.visitEnum("value", "Ljava/lang/annotation/RetentionPolicy;", "RUNTIME");
	av.visitEnd();
}
    cw.visitEnd();
    return cw.toByteArray();
} }

此代碼說明如何用 ACC_ANNOTATION 標誌創建一個註釋類,並說明如何創建兩個類注 釋, 一個沒有值,一個具有枚舉值。方法註釋和參數註釋可以採用 MethodVisitor 類中定 義的 visitAnnotationvisitParameterAnnotation 方法以類似方式創建。

4.3. 調試DEBUG

javac -g 編譯的類中包含了其源文件的名字、源代碼行編號與字節代碼指令之間的 映射、源代碼中局部變量名與字節代碼中局部變量槽之間的映射。當這一可選信息可用時, 會 在調試器中和異常棧軌跡中使用它們。

4.3.1. 結構

一個類的源文件名存儲在一個專門的類文件結構部分中(見圖 2.1)。
源代碼行編號與字節代碼指令之間的映射存儲爲一個由 (line number, label) 對組成的列表 中,放在方法的已編譯代碼部分中。例如,如果 l1、l2 和 l3 是按此順序出現的三個標記,則下 面各對:

(n1, l1) 
(n2, l2)
(n3, l3)

意味着 l1l2 之間的指令來自行 n1,l2 和 l3 之間的指令來自 n2,l3 之後的指令來自行 n3
注意,一個給定行號可以出現在幾個對中。這是因爲,對於出現在一個源代碼行中的表達式,其 在 字節代碼中的相應指令可能不是連續的。例如,for (init; cond; incr) statement; 通常是按以下 順序編譯的:
init statement incr cond

源代碼中局部變量名與字節代碼中局部變量槽之間的映射,以 (name, type descriptor, type signature, start, end, index) 等多元組列表的形式存儲在該方法的已編譯代碼節中。這樣一個多元組 的 含義是:在兩個標記 startend 之間,槽 index 中的局部變量對應於源代碼中的局部變量,其 名字和類型由多元組的前三個元素組出。注意,編譯器可以使用相同的局部變量槽來存儲具有 不 同作用範圍的不同源局部變量。反之,同一個源代碼局部變量可能被編譯爲一個具有非連續 作用 範圍的局部變量槽。例如,有可能存在一種類似如下的情景:

l1:
... // 這裏的槽 1 包含局部變量 i
l2:
... // 這裏的槽 1 包含局部變量 j
l3:
... // 這裏的槽 1 再次包含局部變量 i end:
相應的多元組爲:
("i", "I", null, l1, l2, 1) ("j", "I", null, l2, l3, 1) ("i", "I", null, l3, end, 1)

4.3.2. 接口和組件

調試信息用 ClassVisitorMethodVisitor 類的三個方法訪問:
源文件名用 ClassVisitor 類的 visitSource 方法訪問;

  • 源文件名用 ClassVisitor 類的 visitSource 方法訪問;
  • 源代碼行號與字節代碼指令之間的映射用 MethodVisitor 類的 visitLineNumber 方法訪問,每次訪問一對;
  • 源代碼中局部變量名與字節代碼中局部變量槽之間的映射用 MethodVisitor 類的 visitLocalVariable 方法訪問,每次訪問一個多元組。
  public class MyAdapter extends MethodVisitor {
        int currentLine;
        public MyAdapter(MethodVisitor mv) {
          super(ASM4, mv);
        }
        @Override
        public void visitLineNumber(int line, Label start) {
          mv.visitLineNumber(line, start);
          currentLine = line;
}
... }

visitLineNumber 方法必須在已經訪問了作爲參數傳送的標記之後進行調用。在實踐中, 就是在訪問這一標記後立即調用它,從而可以非常容易地知道一個方法訪問器中當前指令的源代 碼行:

 public class MyAdapter extends MethodVisitor {
        int currentLine;
        public MyAdapter(MethodVisitor mv) {
          super(ASM4, mv);
        }
        @Override
        public void visitLineNumber(int line, Label start) {
          mv.visitLineNumber(line, start);
          currentLine = line;
}
... }

類似地,visitLocalVariable 方法方法必須在已經訪問了作爲參數傳送的標記之後調 用。下面給出一些方法調用示例,它們對應於上一節給出的名稱值對和多元組:

visitLineNumber(n1, l1);
visitLineNumber(n2, l2);
visitLineNumber(n3, l3); 
visitLocalVariable("i", "I", null, l1, l2, 1); 
visitLocalVariable("j", "I", null, l2, l3, 1); 
visitLocalVariable("i", "I", null, l3, end, 1);

忽略調試信息

爲了訪問行號和局部變量名,ClassReader 類可能需要引入“人爲”Label 對象,也 就 是說,跳轉指令並不需要它們,它們只是爲了表示調試信息。這可能會在諸如 3.2.5 節介紹 的情 景中導致錯誤判斷,在該情景中,指令序列中部的一個 Label 被認爲是一個跳轉目標, 因此禁 止這一序列被刪除。
爲避免這種誤判,可以在 ClassReader.accept 方法中使用 SKIP_DEBUG 選項。有了 這 一選項,類讀取器不會訪問調試信息,不會爲它創建人爲標記。當然,調試信息會從類中刪 除, 因此,只有在不會爲應用程序造成問題時才能使用這一選項。

注意:ClassReader 類提供了其他一些選項,比如:SKIP_CODE,用於跳過對已編譯代碼的訪問(如 果 只需要類的結構,那這個選項是很有用的);SKIP_FRAMES,用於跳過棧映射幀;EXPAND_FRAMES, 用於解壓縮這些幀。

4.3.3. 工具

和泛型與註釋的情景一樣,可以使用 TraceClassVisitor、CheckClassAdapter 和
ASMifier
類來了解如何使用調試信息。

5. 向後兼容性

過去已經在類文件格式中引入了新的元素,未來還將繼續添加新元素(例如,用於模塊化、 Java 類型的註釋,等等)。到 ASM 3.x,這樣的每一次變化都會導致 ASM API 中的後向不兼容變 化,這不是件好事情。爲解決這些問題,ASM 4.0 中已經引入了一種新機制。它的目的是確保未 來 所有 ASM 版本都將與之前直到 ASM 4.0 的任意版本保持後向兼容,即使向類文件格式中引入 了 新的功能時也能保持這種兼容性。這意味着,從 4.0 開始,爲一個 ASM 版本編寫的類生成器、 類 分析器或類適配器,將可以在任何未來 ASM 版本中使用。但是,僅靠 ASM 自身是不能確保 這一性質的。它需要用戶在編寫代碼時遵循一些簡單的準則。本章的目的就是介紹這些準則,並 大致介紹一下 ASM 核心 API 中用於確保後向兼容性的內部機制。

注意: ASM 4.0 中引入的後向兼容機制要求將ClassVisitor、FieldVisitor、 MethodVisitor 等由接口變爲抽象類,具有一個以 ASM 版本爲參數的構造器。如果你的代碼是爲 ASM 3.x 實現的,可以將其升級至 ASM 4.0:將代碼分析器和適配器中的 implementsextends 替換, 並在它們的構 造器中指定一個 ASM 版本。此外,ClassAdapter 和 MethodAdapter 還被合併到
ClassVisitor 和 MethodVisitor 中。要轉換代碼 , 只需用 ClassVisitor 代替 ClassAdapter,用 MethodVisitor 代替 MethodAdapter。另外,如果定義了自定義的 FieldAdapterAnnotationAdapter 類 , 現 在 可 以 用 FieldVisitor 和 AnnotationVisitor 代替它們。

5.1. 介紹

5.1.1. 向後兼容約定

在給出用以確保後向兼容性的規則之前,首先給出“後向兼容”的更準確定義。

首先,研究一下新的類文件特徵如何影響代碼生成器、分析器和適配器是非常重要的。也就 是 說,在不受任何實現和二進制兼容問題影響時,在引入這些新特徵之前設計的類生成器、分析 器或 適配器在進行這些修改之後還是否有效?換言之,如果有一個在引入這些新功能之前設計的 轉換鏈,假定這些新功功直接被忽略,原封不動地通過轉換鏈,那這個轉換鏈還是否依然有效? 事實上,類生成器、分析器和適配器受到的影響是不同的:

  • 類生成器不受影響:它們生成具有某一固定類版本的代碼,這些生成的類在未來的 JVM 版本中依然有效,因爲 JVM 確定了後向二進制兼容。
  • 類分析器可能受到影響,也可能不受影響。例如,有一段用於分析字節代碼指令的代碼,它是爲 Java 4 編寫的,它也許能夠正常處理 Java 5 類,儘管 Java 5 中引入了注 釋。但 同一段代碼也許不再能處理 Java 7 類,因爲它不能忽略新的動態調用指令。
  • 類適配器可能受到影響,也可能不受影響。死代碼清除工具不會因爲引入註釋而受到 影 響,甚至不會受到新的動態調用指令的影響。但另一方面,這兩種新特性可能都 會影 響到爲類進行重命名的工具。

這表明,新的類文件特性可能會對已有的類分析器或適配器產生不可預測的影響。如果新 的 特性直接被忽略,原封不動地通過一個分析鏈或轉換鏈,這個鏈在某些情況下可以運行,不 產生 錯誤,並給出有效結果,而在某些情況下,也可以運行,不產生錯誤,但卻給出無效結 果,而在 另外一些情況下,可能會在執行期間失敗。第二種情景的問題尤其嚴重,因爲它會在 用戶不知曉 的情況下破壞分析鏈或轉換鏈的語義,從而導致難以找出 Bug。爲解決這一問 題,我們認爲最好 不要忽略新特性,而是隻要在分析鏈或轉換鏈中遇到未知特性,就產生一條 錯誤。這種錯誤發出 信號:這個鏈也許能夠處理新的類格式,也許不能,鏈的編寫者必須分析 具體情景,並在必要時 進行更新。

所有上述內容引出了後向兼容性約定的如下定義:

  • ASM 版本 X 是爲版本號低於小等於 x 的 Java 類編寫的。它不能生成版本號 y>x 的類, 如 果在 ClassReader.accept 中,以一個版本號大於 x 的類作爲輸入,它必須失敗。
  • 對於爲 ASM X 編寫且遵循了以下所述規則的代碼,當輸入類的版本不超過 x,對於 ASM 未來任意大於 X 的版本 Y,該代碼都能不加修改地正常工作。
  • 對於爲 ASM X 編寫且遵循了以下所述規則的代碼,當輸入類的聲明版本爲 y,但僅 使 用了在不晚於版本 x 中定義的功能,則在使用 ASM Y 或任意未來版本時,該代 碼能夠 不加修改地正常工作。
  • 對於爲 ASM X 編寫且遵循了以下所述規則的代碼,當輸入類使用了在版本號爲 y>x 的 類中定義的功能時,對於 ASM X 或任意其他未來版本,該代碼都必須失敗。

注意,最後三點與類生成器無關,因爲它沒有類輸入。

5.1.2. 一個例子

爲說明這些用戶規則及用於保證後向兼容性的內部 ASM 機制,本章假定將向 Java 8 類中添加兩個新的假設屬性,一個用於存儲類的作者,另一個用於存儲它的許可。還假設這些新的屬性 在 ASM 5.0 中通過 ClassVisitor 的兩個新方法公開,一個是:
void visitLicense(String license);

用於訪問許可,還有一個是 visitSource 的新版本,用於在訪問源文件名和調試信息的同 時 訪問作者1:
@Deprecated void visitSource(String source, String debug);
作者和許可屬性是可選的,即對 visitLicense 的調用並非強制的,在一個 visitSource調用中,author 可能是 null

5.2. 準則

本節給出一些規則,在使用 ASM API 時,要想確保你的代碼在所有未來 ASM 版本中都 有 效(其意義見上述約定),就必須遵循這些規則。

首先,如果編寫一個類生成器,那不需要遵循任何規則。例如,如果正在爲 ASM 4.0 編 寫 一個類生成器,它可能包含一個類似於 visitSource(mySource, myDebug)的調用, 當然 不包含對 visitLicense 的調用。如果不加修改地用 ASM 5.0 運行它,它將會調用過 時的visitSource方法,但 ASM5.0ClassWriter將會在內部將它重定 向 到 visitSource(null, mySource, myDebug),生成所期望的結果(但其效率要稍 低於直接 將代碼升級爲調用這個新方法)。同理,缺少對 visitLicense 的調用也不會造 成問題(所生 成的類版本也沒有變化,人們並不指望這個版本的類中會有一個許可屬性)。

另一方面,如果編寫一個類分析器或類適配器,也就是說,如果重寫 ClassVisitor 類(或 者任何其他類似的類,比如 FieldVisitor 或 MethodVisitor),就必須遵循一些規則,如
下所述。

5.2.1. 基本規則

這裏考慮一個類的簡單情況:直接擴展 ClassVisitor (討論和規則對於其他訪問器類 都 是相同的;間接子類的情景在下一節討論)。在這種情況下,只有一條規則:

規則 1: 要爲 ASM X 編寫一個 ClassVisitor 子類,就以這個版本號爲參數,調用 ClassVisitor 構造器,在這個版本的 ClassVisitor 類中,絕對不要重寫或調用棄用的方法(或者將在之後版本引入的方法)。

class MyClassAdapter extends ClassVisitor {
        public MyClassAdapter(ClassVisitor cv) {
			super(ASM5, cv); 
		}
        ...
        public void visitSource(String author, String source, String debug) { // optional
          ...
          super.visitSource(author, source, debug); // optional
        }
        public void visitLicense(String license) { // optional
          ...
          super.visitLicense(license); // optional
        }
}

一旦針對 ASM5.0 升級之後,必須刪除 visitSource(String, String),這個類看起 來必須類似於如下所示:

class MyClassAdapter extends ClassVisitor {
	public MyClassAdapter(ClassVisitor cv) {
		super(ASM5, cv); }
        ...
        public void visitSource(String author,
            String source, String debug) { // optional
         	 ...
          	super.visitSource(author, source, debug); // optional
        }
        public void visitLicense(String license) { // optional
          ...
          super.visitLicense(license); // optional
        }
}

它是如何工作的呢?在 ASM 4.0 中,ClassVisitor 的內部實現如下

public abstract class ClassVisitor {
        int api;
        ClassVisitor cv;
        public ClassVisitor(int api, ClassVisitor cv) {
          this.api = api;
          this.cv = cv;
        }
        ...
        public void visitSource(String source, String debug) {
          if (cv != null) cv.visitSource(source, debug);
        }
}

在 ASM 5.0 中,這一代碼變爲:

public abstract class ClassVisitor {
         ...
        public void visitSource(String source, String debug) {
          	if (api < ASM5) {
            	if (cv != null) 
            		cv.visitSource(source, debug);
          	} else {
            	visitSource(null, source, debug);
			} 
		}
        public void visitSource(Sring author, String source, String debug) {
          	if (api < ASM5) {
            	if (author == null) {
              		visitSource(source, debug);
            } else {
              	throw new RuntimeException();
			}
		} else {
            if (cv != null) 
            	cv.visitSource(author, source, debug);
          }
        }
        public void visitLicense(String license) {
          if (api < ASM5) 
          		throw new RuntimeException();
          if (cv != null) 
          		cv.visitSource(source, debug);
        }
}

如果 MyClassAdapter 4.0 擴展了 ClassVisitor 4.0,那一切都將如預期中一樣正常工 作。 如果升級到 ASM 5.0,但沒有修改代碼,MyClassAdapter 4.0 現在將擴展 ClassVisitor 5.0。但 api 字段仍將是 ASM4 <ASM5,容易看出,在這種情況下,在調用 visitSource(String,String) 時,ClassVisitor 5.0 的行爲特性類似於 ClassVisitor 4.0。此外,如果用一 個 null 作者一訪問新的 visitSource 方法,該調用將被重定向至舊版本。最後,如果在 輸入類 中找到非 null 作者或許可,執行過程將會失敗,與約定中的規定一致(或者是在新 的 visitSource 方法中,或者是在 visitLicense 中)。

如果升級到 ASM 5.0 ,並同時升級代碼,現在將擁有擴展了 ClassVisitor 5.0MyClassAdapter 5.0。api 字段現在是 ASM5,visitLicense 和新的 visitSource 方法 的行爲就是直接將調用委託給下一個訪問者 cv。此外,舊的 visitSource 方法現在 將調用重 定向至新的 visitSource 方法,這樣可以確保:如果在轉換鏈中,在我們自己的 類適配器之 前使用了一箇舊類適配器,那 MyClassAdapter 5.0 不會錯過這個訪問事件。

ClassReader 將總是調用每個訪問方法的最新版本。因此,如果隨 ASM 4.0 使用MyClassAdapter 4.0,或者隨 ASM 5.0 使用 MyClassAdapter 5.0,將不會產生重定向。只 有在隨 ASM 5.0 使用 MyClassAdapter 4.0 時,纔會在 ClassVisitor 中發生重定向(在 新 visitSource 方法的第 3 行)。因此,儘管舊代碼在新 ASM 版本中仍能正常使用,但 它的 運行速度要慢一些。將其升級爲使用新的 API,將恢復其性能。

5.2.2. 繼承規則

上述規則對於 ClassVisitor 或任意其他類似類的直接子類都足夠了。對於間接子類, 也 就是說,如果定義了一個擴展 ClassVisitor 的子類 A1,而它本身又由 A2 擴 展,…它本身 又由 An 擴展,則必須爲同一 ASM 版本編寫所有這些子類。事實上,在一 個繼承鏈中混用不同 版本將導致同時重寫同一方法的幾個版本, 比如 visitSource(String,String) 和 visitSource(String,String,String),它們的 行爲可能不同,導致錯誤或不可預測的 結果。如果這些類的來源不同,每個來源被獨立升 級、單獨發佈,那幾乎不可能保證這一性 質。這就引出第二條規則:

規則 2:不要使用訪問器的繼承,而要使用委託(即訪問器鏈)。一種好的做法是讓 你 的訪問器類在默認情況爲 final 的,以確保這一特性。
事實上,這一規則有兩個例外:

  • 如果能夠完全由自己控制繼承鏈,並同時發佈層次結構中的所有類,那就可以使用訪問 器的繼承。但必須確保層次結構中的所有類都是爲同一 ASM 版本編寫的。仍然要讓 層 次結構的葉類是 final 的。
  • 如果除了葉子類之外,沒有其他類重寫任何訪問方法(例如,如果只是爲了引入方便的方 法而在 ClassVisitor 和具體訪問類之間使用了中間類),那就可以使用“訪問器”的繼 承。仍然要讓層次結構的葉類是 final 的(除非它們也沒有重寫任何訪問方法;在這種 情況下,提供一個以 ASM 版本爲參數的構造器,使子類可以指定它們是爲哪個版本編 寫的)。

二. 樹API

本章解釋如何用 ASM 樹 API 來生成和轉換類。首先介紹樹 API 本身,然後解釋如何用核心
API 來組成它。用於方法、註釋和泛型內容的樹 API 將在隨後各章介紹。

6.類 class

6.1. 接口和組件

6.1.1. 介紹

用於生成和轉換已編譯 Java 類的 ASM 樹 API 是基於 ClassNode 類的(見圖 6.1)。

public class ClassNode ... { 
	public int version;
	public int access;
	public String name;
    public String signature;
    public String superName;
    public List<String> interfaces;
    public String sourceFile;
    public String sourceDebug;
    public String outerClass;
    public String outerMethod;
    public String outerMethodDesc;
    public List<AnnotationNode> visibleAnnotations;
    public List<AnnotationNode> invisibleAnnotations;
    public List<Attribute> attrs;
    public List<InnerClassNode> innerClasses;
    public List<FieldNode> fields;
    public List<MethodNode> methods;
}

圖 6.1 ClassNode類(僅給出了字段)

可以看出,這個類的公共字段對應於圖 2.1 中給出的類文件結構部分。這些字段的內容與核 心 API 相同。例如,name 是一個內部名字,signature 是一個類簽名(見 2.1.2 節和 4.1 節)。 一些字段包含其他 XxxNode 類:這些類將在隨後各章詳細介紹,它們擁有一種類似的結構,
即 擁有一些字段,對應於類文件結構的子部分。例如,FieldNode 類看起來是這樣的:

public class FieldNode ... {
        public int access;
        public String name;
        public String desc;
        public String signature;
        public Object value;
        public FieldNode(int access, String name, String desc, String signature, Object value) {
          ...
		}
	... 
}

MethodNode 類是類似的:

public class MethodNode ... {
        public int access;
        public String name;
        public String desc;
        public String signature;
        public List<String> exceptions;
        ...
        public MethodNode(int access, String name, String desc, String signature, String[] exceptions)
        {... }
}

6.1.2. 生成類

用樹 API 生成類的過程就是:創建一個 ClassNode 對象,並初始化它的字段。例如,2.2.3 節的 Comparable 接口可用如下代碼生成(其代碼數量大體與 2.2.3 節相同):

ClassNode cn = new ClassNode();
cn.version = V1_5;
cn.access = ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE;
cn.name = "pkg/Comparable";
cn.superName = "java/lang/Object"; cn.interfaces.add("pkg/Mesurable");
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I", null, new Integer(-1)));
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I", null, new Integer(0)));
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I", null, new Integer(1))); 
cn.methods.add(new MethodNode(ACC_PUBLIC + ACC_ABSTRACT, "compareTo", "(Ljava/lang/Object;)I", null, null));

使用樹 API 生成類時,需要多花費大約 30%的時間(見附錄 A.1),佔用的內存也多於 使用 核心 API。但可以按任意順序生成類元素,這在一些情況下可能非常方便

6.1.3. 添加和刪​​除類成員

添加和刪除類就是在 ClassNode 對象的 fields 或 methods 列表中添加或刪除元素。例 如,如果像下面這樣定義了 ClassTransformer 類,以便能夠輕鬆地編寫類轉換器:

 public class ClassTransformer {
        protected ClassTransformer ct;
        public ClassTransformer(ClassTransformer ct) {
          this.ct = ct;
        }
        public void transform(ClassNode cn) {
          if (ct != null) {
            ct.transform(cn);
          }
} }

則 2.2.5 節中的 RemoveMethodAdapter 可實現如下:

 public class RemoveMethodTransformer extends ClassTransformer {
        private String methodName;
        private String methodDesc;
        public RemoveMethodTransformer(ClassTransformer ct, String methodName, String methodDesc) {
          	super(ct);
          	this.methodName = methodName;
          	this.methodDesc = methodDesc;
        }
        @Override 
        public void transform(ClassNode cn) {
          Iterator<MethodNode> i = cn.methods.iterator();
			while (i.hasNext()) {
				MethodNode mn = i.next();
				if (methodName.equals(mn.name) && methodDesc.equals(mn.desc)) {
              		i.remove();
            }
}
          	super.transform(cn);
        }
}

可以看出,它與核心 API 的主要區別是需要迭代所有方法,而在使用核心 API 時是不需要 這樣做的(這一工作會在 ClassReader 中爲你完成)。事實上,這一區別對於幾乎所有基於樹的轉換都是有效的。例如,在用樹 API 實現 2.2.6 節的 AddFieldAdapter 時,它還需要一個 迭代器:

public class AddFieldTransformer extends ClassTransformer {
        private int fieldAccess;
        private String fieldName;
        private String fieldDesc;
        public AddFieldTransformer(ClassTransformer ct, int fieldAccess, String fieldName, String fieldDesc) {
          	super(ct);
          	this.fieldAccess = fieldAccess;
          	this.fieldName = fieldName;
          	this.fieldDesc = fieldDesc;
        }
        @Override 
        public void transform(ClassNode cn) {
          boolean isPresent = false;
          for (FieldNode fn : cn.fields) {
            if (fieldName.equals(fn.name)) {
              isPresent = true;
              break; 
              }
		   }
          if (!isPresent) {
            cn.fields.add(new FieldNode(fieldAccess, fieldName, fieldDesc,null, null));
}
          super.transform(cn);
        }
}

和生成類的情景一樣,使用樹 API 轉換類時,所花費的時間和佔用的內存也要多於使用核 心 API 的時候。但使用樹 API 有可能使一些轉換的實現更爲容易。比如有一個轉換,要向一個 類中 添加註解,包含其內容的數字簽名,就屬於上述情景。在使用核心 API 時,只有在訪問了 整個類之後才能計算數字簽名,但這時再添加包含其內容的註解就太晚了,因爲對註釋的訪問 必 須位於類成員之前。而在使用樹 API 時,這個問題就消失了,因爲這時不存在此種限制。

事實上,有可能用核心 API 實現 AddDigitialSignature 示例,但隨後,必須分兩遍來 轉換這個類。第一遍,首先用一個 ClassReader(沒有 ClassWriter) 來訪問這個類,以根 據 類的內容來計算數字簽名。在第二遍,重複利用同一個 ClassReader 對類進行第一次訪問, 這 一次是向一個 ClassWriter 鏈接一個 AddAnnotationAdapter。通過推廣這一論述過程, 我 們可以看出,事實上,任何轉換都可以僅用核心 API 來實現,只需在必要時分幾遍完成。但 這樣 就提高了轉換代碼的複雜性,要求在各遍之間存儲狀態(這種狀態可能非常複雜,需要一個 完整的 樹形表示!),而且對一個類進行多次分析是有成本的,必需將這一成本與構造相應ClassNode 的成本進行比較。

結論是:樹 API 通常用於那些不能由核心 API 一次實現的轉換。但當然也存在例外。例如 一個混淆器不能由核心 API 一遍實現,因爲必須首先在原名稱和混淆後的名字之間建立了完整 的 映射之後,纔可能轉換類,而這個映射的建立需要對所有類進行分析。但樹 API 也不是一個 好的解決方案,因爲它需要將所有待混淆類的對象表示保存在內存中。在這種情況下,最好是分 兩 遍使用核心 API:一遍用於計算原名與混淆後名稱之間的映射(一個簡單的散列表,它需要的 內存要遠少於所有類的完整對象表示),另一遍用於根據這一映射來轉換類。

6.2. 組件合成

到現在爲止,我們只是看到了如何創建和轉換 ClassNode 對象,但還沒有看到如何由一個 類的字節數組表示來構造一個 ClassNode,或者反過來,由 ClassNode 構造這個字節數組。 事實上,這一功能可以通過合成核心 API 和樹 API 組件來完成,本節就來解釋這一內容。

6.2.1. 介紹

除了圖 6.1 所示的字段之外,ClassNode 類擴展了 ClassVisitor 類,還提供了一個accept 方法,它以一個 ClassVisitor 爲參數。Accept 方法基於 ClassNode 字段值生成事件,而 ClassVisitor 方法執行逆操作,即根據接到的事件設定 ClassNode 字段:

public class ClassNode extends ClassVisitor { ...
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces[]) {
          this.version = version;
          this.access = access;
          this.name = name;
          this.signature = signature;
          ...
		}
		...
		public void accept(ClassVisitor cv) {
          	cv.visit(version, access, name, signature);
			... 
		}	
}

要由字節數組構建 ClassNode,可以將它與 ClassReader 合在一起,使 ClassReader 生成的事件可供 ClassNode 組件使用,從而初始化其字段(由上述代碼可以看出):

ClassNode cn = new ClassNode();
 ClassReader cr = new ClassReader(...); 
 cr.accept(cn, 0);

反過來,可以將 ClassNode 轉換爲其字節數組表示,只需將它與 ClassWriter 合在 一起 即可,從而使 ClassNodeaccept 方法生成的事件可供 ClassWriter 使用:

ClassWriter cw = new ClassWriter(0); 
cn.accept(cw);
byte[] b = cw.toByteArray();

6.2.2. 模式匹配

要用樹 API 轉換類,可以將這些元素放在一起:

ClassNode cn = new ClassNode(ASM4);
ClassReader cr = new ClassReader(...);
cr.accept(cn, 0);
... // here transform cn as you want
ClassWriter cw = new ClassWriter(0);
cn.accept(cw);
byte[] b = cw.toByteArray();

還可能與核心 API 一起使用基於樹的類轉換器,比如類適配器。有兩種常見模式可用於此 種情景。第一種模式使用繼承:

public class MyClassAdapter extends ClassNode { public MyClassAdapter(ClassVisitor cv) {
          super(ASM4);
          this.cv = cv;
        }
@Override 
public void visitEnd() {
// put your transformation code here 
	accept(cv);
} 
}

當這個類適配器用在一個經典的轉換鏈時:

ClassWriter cw = new ClassWriter(0); 
ClassVisitor ca = new MyClassAdapter(cw); 
ClassReader cr = new ClassReader(...);
cr.accept(ca, 0);
 byte[] b = cw.toByteArray();

cr 生成的事件供 ClassNode ca 使用,從而初始化這個對象的字段。最後,在使用 visitEnd 事 件時,ca 執行轉換,並通過調用其 accept 方法,生成與所轉換類對應的新事件,然後由 cw 使用。如果假定 ca 改變了類版本,則相應原程序圖如圖 6.2 所示。
在這裏插入圖片描述
圖 6.2 MyClassAdapter 的程序圖

與圖 2.7 中 ChangeVersionAdapter 的程序圖進行對比,可以看出,ca 和 cw 之間的事 件發生在 cr 和 ca 之間的事件之後,而不是像正常類適配器一樣同時進行。事實上,對於所有 基於樹的轉換都是如此,同時還解釋了爲什麼它們受到的限制要少於基於事件的轉換。

第二種模式可用於以類似程序圖獲得相同結果,它使用的是委託而非繼承:

public class MyClassAdapter extends ClassVisitor {
        ClassVisitor next;
        public MyClassAdapter(ClassVisitor cv) {
			super(ASM4, new ClassNode());
			next = cv; 
		}
		@Override 
		public void visitEnd() { 
			ClassNode cn = (ClassNode) cv;
			// 將轉換代碼放在這裏 
			cn.accept(next);
			} 
		}

這一模式使用兩個對象而不是一個,但其工作方式完全與第一種模式相同:接收到的事件用 於 構造一個 ClassNode,它被轉換,並在接收到最後一個事件後,變回一個基於事件的表示。

這兩種模式都允許用基於事件的適配器來編寫基於樹的類適配器。它們也可用於將基於樹的 適配器組合在一起,但如果只需要組合基於樹的適配器,那這並非最佳解決方案:在這種情況 下, 使用諸如 ClassTransformer 的類將會避免在兩種表示之間進行不必要的轉換。

7.方法

本章解釋如何用 ASM 樹 API 生成和轉換方法。首先介紹樹 API 本身,給出一些說明性
示例, 然後說明如何用核心 API 編寫它。用於泛型和註釋的樹 API 在下一章介紹。

7.1. 接口和組件

7.1.1. 介紹

用於生成和轉換方法的 ASM 樹 API 是基於 MethodNode 類的(見圖 7.1)

public class MethodNode ... { 
		public int access;
		public String name;
		public String desc;
        public String signature;
        public List<String> exceptions;
        public List<AnnotationNode> visibleAnnotations;
        public List<AnnotationNode> invisibleAnnotations;
        public List<Attribute> attrs;
        public Object annotationDefault;
        public List<AnnotationNode>[] visibleParameterAnnotations;
        public List<AnnotationNode>[] invisibleParameterAnnotations;
        public InsnList instructions;
        public List<TryCatchBlockNode> tryCatchBlocks;
        public List<LocalVariableNode> localVariables;
        public int maxStack;
        public int maxLocals;
}

圖 7.1 MethodNode 類(僅給出字段)
這個類的大多數字段都類似於 ClassNode 的對應字段。最重要的是從 instructions 字 段開始的最後幾個。這個 instructions 字段是一個指令列表,用一個 InsnList 對象 管理, 它的公共 API 如下:

  public class InsnList { // public accessors omitted
        int size();
        AbstractInsnNode getFirst();
        AbstractInsnNode getLast();
        AbstractInsnNode get(int index);
        boolean contains(AbstractInsnNode insn);
        int indexOf(AbstractInsnNode insn);
        void accept(MethodVisitor mv);
        ListIterator iterator();
        ListIterator iterator(int index);
        AbstractInsnNode[] toArray();
        void set(AbstractInsnNode location, AbstractInsnNode insn);
        void add(AbstractInsnNode insn);
        void add(InsnList insns);
        void insert(AbstractInsnNode insn);
        void insert(InsnList insns);
        void insert(AbstractInsnNode location, AbstractInsnNode insn);
        void insert(AbstractInsnNode location, InsnList insns);
        void insertBefore(AbstractInsnNode location, AbstractInsnNode insn);
        void insertBefore(AbstractInsnNode location, InsnList insns);
        void remove(AbstractInsnNode insn);
        void clear();
}

InsnList 是一個由指令組成的雙向鏈表,它們的鏈接存儲在 AbstractInsnNode 對象 本身中。這一點極爲重要,因爲它對於必須如何使用指令對象和指令列表的方式有許多影響:

  • 一個 AbstractInsnNode 對象在一個指令列表中最多出現一次。
  • 一個 AbstractInsnNode 對象不能同時屬於多個指令列表。
  • 一個結果是:如果一個 AbstractInsnNode 屬於某個列表,要將它添加到另一列表,
    必須先將其從原列表中刪除。
  • 另一結果是:將一個列表中的所有元素都添加到另一個列表中,將會清空第一個列表。
    AbstractInsnNode 類是表示字節代碼指令的類的超類。它的公共 API 如下:
  public abstract class AbstractInsnNode {
        public int getOpcode();
        public int getType();
        public AbstractInsnNode getPrevious();
        public AbstractInsnNode getNext();
        public void accept(MethodVisitor cv);
        public AbstractInsnNode clone(Map labels);
}

它的子類是 Xxx InsnNode 類,對應於 MethodVisitor 接口的 visitXxx Insn 方 法, 而且其構造方式完全相同。例如,VarInsnNode 類對應於 visitVarInsn 方法,且 具有以下 結構:

 public class VarInsnNode extends AbstractInsnNode {
        public int var;
        public VarInsnNode(int opcode, int var) {
          super(opcode);
          this.var = var;
        }
        ...
}

lable與frame,還有行號,儘管它們並不是指令,但也都用 AbstractInsnNode 類的子類表 示,即 LabelNode、FrameNode 和 LineNumberNode 類。這樣就允許將它們恰好插在列表 中對應的真實指令之前,與核心 API 中一樣(在覈心 API 中,就是恰在相應的指令之前訪問標 記和幀)。因此,很容易使用 AbstractInsnNode 類提供的 getNext 方法找到跳轉指令的目 標:這是目標標記之後第一個是真正指令的 AbstractInsnNode。另一個結果是:與核心 API 一樣,只要標記保持不變,刪除指令並不會破壞跳轉指令。

7.1.2. 生成方法

用樹 API 生成一個方法包括:創建一個 MethodNode,初始化其字段。最重要的部分是 方 法代碼的生成。比如,3.1.5 節的 checkAndSetF 方法可生成如下:


      MethodNode mn = new MethodNode(...);
      InsnList il = mn.instructions;
      il.add(new VarInsnNode(ILOAD, 1));
      LabelNode label = new LabelNode();
      il.add(new JumpInsnNode(IFLT, label));
      il.add(new VarInsnNode(ALOAD, 0));
      il.add(new VarInsnNode(ILOAD, 1));
      il.add(new FieldInsnNode(PUTFIELD, "pkg/Bean", "f", "I"));
      LabelNode end = new LabelNode();
      il.add(new JumpInsnNode(GOTO, end));
      il.add(label);
      il.add(new FrameNode(F_SAME, 0, null, 0, null));
      il.add(new TypeInsnNode(NEW, "java/lang/IllegalArgumentException"));
      il.add(new InsnNode(DUP));
      il.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/IllegalArgumentException", "<init>", "()V"));
      il.add(new InsnNode(ATHROW));
      il.add(end);
      il.add(new FrameNode(F_SAME, 0, null, 0, null));
      il.add(new InsnNode(RETURN));
      mn.maxStack = 2;
      mn.maxLocals = 2;

和類的情景一樣,使用樹 API 來生成方法時,花費的時間和佔用的內存都要多於使用核心 API 的情況。但可以按照任意順序來生成其內容。具體來說,這些指令可按非順序方式生成,這 在一些情況下是很有用的。

比如,考慮一個壓縮編譯器。通常,要編譯表達式 e1+e2,首先發送 e1 的代碼,然後發出 e2 的代碼,然後發出將這兩個值相加的代碼。但如果 e1 和 e2 不是同一基元類型,必須恰在 e1 的代 碼之後插入一個轉換操作,恰在 e2 的代碼之後插入另一個。但是究竟發出哪些轉換操作 取決於 e1 和 e2 的類型。

現在,如果表達式的類型是由發出已編譯代碼的方法返回的,那在使用核心 API 時就會存 在一個問題:只有在已經編譯了 e2 之後才能知道必須插在 e1 之後的轉換,但這時已經太晚了, 因爲我們不能在之前訪問的指令之間插入指令。1在使用樹 API 時不存在這一問題。
例如,一種 可能性是使用比如下面所示的 compile 方法:

public Type compile(InsnList output) { 
	InsnList il1 = new InsnList(); 
	InsnList il2 = new InsnList(); 
	Type t1 = e1.compile(il1);
	Type t2 = e2.compile(il2); 
	Typet=...;// 計算 t1 和 t2 的公共超類 型 
	output.addAll(il1); // 在常量時間內完成 
	output.add(...); // 由 t1 到 t 的轉換指令 
	output.addAll(il2); // 在常量時間內完成 
	output.add(...); // 由 t2 到 t 的轉換指令
	output.add(new InsnNode(t.getOpcode(IADD)));
	return t;
} 

7.1.3. 轉換方法

用樹 API 轉換方法只需要修改一個 MethodNode 對象的字段,特別是 instructions 列表。儘管這個列表可以採用任意方式修改,但常見做法是通過迭代修改。事實上,與通用 ListIterator 約定不同,InsnList 返回的 ListIterator 支持許多併發列表修改1。事實上,可以使用 InsnList 方法刪除包括當前元素在內的一或多個元素,刪除下一個元素之後的 一或多個元素(也就是說,不是緊隨當今元素之後的元素,而是它後面一個元素之後的元素),
或者在當前元素之前或其後續者之後插入一或多個元素。這些修改將反映在迭代器中,即在下 一 元素之後插入(或刪除)的元素將在迭代器中被看到(或不被看到)。

如果需要在一個列表的指令 i 之後插入幾條指令,那另一種修改指令列表的常見做法是將 這 些新指令插入一個臨時指令列表中,再在一個步驟內將這個臨時列表插到主列表中:

InsnList il = new InsnList(); il.add(...);
...
il.add(...);
mn.instructions.insert(i, il);

逐條插入指令也是可行的,但卻非常麻煩,因爲必須在每次插之後更新插入點。

7.1.4. 無狀態和有狀態轉換

讓我們用一些示例來具體看看如何用樹 API 轉換方法。爲了看出核心 API 和樹 API 之間的 區 別 , 重 新 實 現 3.2.4 節 的 AddTimerAdapter 示 例 和 3.2.5 節 的
RemoveGetFieldPutFieldAdapter 是有意義的。計時器示例可實現如下:

public class AddTimerTransformer extends ClassTransformer {
        public AddTimerTransformer(ClassTransformer ct) {
			super(ct); 
		}
        @Override 
        public void transform(ClassNode cn) {
          for (MethodNode mn : (List<MethodNode>) cn.methods) {
            if ("<init>".equals(mn.name) || "<clinit>".equals(mn.name)) {
             	 continue;
            }
            InsnList insns = mn.instructions;
            if (insns.size() == 0) {
              	continue;
			}
            Iterator<AbstractInsnNode> j = insns.iterator();
            while (j.hasNext()) {
              		AbstractInsnNode in = j.next();
             	 	int op = in.getOpcode();
              		if ((op >= IRETURN && op <= RETURN) || op == ATHROW) {
               	 		InsnList il = new InsnList();
               	 		il.add(new FieldInsnNode(GETSTATIC, cn.name, "timer","J"));
                		il.add(new MethodInsnNode(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J"));
						il.add(new InsnNode(LADD));
						il.add(new FieldInsnNode(PUTSTATIC, cn.name, "timer","J"));
						insns.insert(in.getPrevious(), il);
				} 
			}
            InsnList il = new InsnList();
            il.add(new FieldInsnNode(GETSTATIC, cn.name, "timer", "J"));
            il.add(new MethodInsnNode(INVOKESTATIC,"java/lang/System","currentTimeMillis", "()J"));
			il.add(new InsnNode(LSUB));
			il.add(new FieldInsnNode(PUTSTATIC, cn.name, "timer", "J")); 
			insns.insert(il);
			mn.maxStack += 4;
          }
          int acc = ACC_PUBLIC + ACC_STATIC;
          cn.fields.add(new FieldNode(acc, "timer", "J", null, null));
          super.transform(cn);
	}
}

在這裏可以看出上一節討論的用於在指令列表中插入若干指令的模式,其中包含了使用臨時 指令列表。這個示例還表明,有可能在迭代一個指令表的時候向當前指令之前插入指令。注意, 在使用核心 API 和樹 API 時,實現這一適配器所需要的代碼數量大體相同。

(如果假定 MethodTransformer 類似於上一章的 ClassTransformer 類,)刪除 了字 段的自我賦值的方法適配器(見 3.2.5 節)可實現如下:

public class RemoveGetFieldPutFieldTransformer extends MethodTransformer {
        public RemoveGetFieldPutFieldTransformer(MethodTransformer mt) {
          super(mt);
        }
        @Override 
        public void transform(MethodNode mn) {
          InsnList insns = mn.instructions;
          Iterator<AbstractInsnNode> i = insns.iterator();
          while (i.hasNext()) {
            	AbstractInsnNode i1 = i.next();
            	if (isALOAD0(i1)) {
              		AbstractInsnNode i2 = getNext(i1);
              		if (i2 != null && isALOAD0(i2)) {
                		AbstractInsnNode i3 = getNext(i2);
                		if (i3 != null && i3.getOpcode() == GETFIELD) {
                  			AbstractInsnNode i4 = getNext(i3);
              				if (i4 != null && i4.getOpcode() == PUTFIELD) {
                				if (sameField(i3, i4)) {
                      				while (i.next() != i4) {
                      					insns.remove(i1);
                      					insns.remove(i2);
                      					insns.remove(i3);
                      					insns.remove(i4);
                      			}
							} 
						}
					} 
				}
			} 
		}
          	super.transform(mn);
    }
        private static AbstractInsnNode getNext(AbstractInsnNode insn) {
          	do {
          		insn = insn.getNext();
            	if (insn != null && !(insn instanceof LineNumberNode)) {
					break;
			 	}
          	}while (insn != null);
          		return insn;
        }
        private static boolean isALOAD0(AbstractInsnNode i) {
          	return i.getOpcode() == ALOAD && ((VarInsnNode) i).var == 0;
        }
        private static boolean sameField(AbstractInsnNode i, AbstractInsnNode j) {
          return ((FieldInsnNode) i).name.equals(((FieldInsnNode) j).name);
		} 
}

在這裏再次看到,有可能在對一個指令清單迭代時從中刪除指令。但要注意 while (i.next() != i4) 循環:必須將迭代器放在必須刪除的指令之後(因爲不可能刪除恰在當前
指令之後的指令)。基於訪問器和基於樹的實現都可以在被檢測序列的中部檢測到標記和幀,在 這 種情況下,不要刪除它。但要忽略序列中的行號(見 getNext 方法),使用基於樹的 API 時的代碼數量要多於使用核心 API 的情況。但是,這兩種實現之間的主要區別是:在使用樹 API 時,不需要狀態機。特別是有三個或更多個連續 ALOAD 0 指令的特殊情景(它很容易被忽視),不再成爲問題了。

利用上述實現,一條給定指令可能會被查看多次,這是因爲在 while 循環中的每一步,i2、 i3 和 i4 也可能會在這一迭代中被查看(在未來迭代中還會查看它們)。事實上,有可能使用一 種更高效的實現,使每條指令最多被查看一次:

public class RemoveGetFieldPutFieldTransformer2 extends MethodTransformer {
        ...
        @Override 
        public void transform(MethodNode mn) {
          	InsnList insns = mn.instructions;
          	Iterator i = insns.iterator();
          	while (i.hasNext()) {
            	AbstractInsnNode i1 = (AbstractInsnNode) i.next();
            if (isALOAD0(i1)) {
              	AbstractInsnNode i2 = getNext(i);
              	if (i2 != null && isALOAD0(i2)) {
              		AbstractInsnNode i3 = getNext(i);
                	while (i3 != null && isALOAD0(i3)) {
                  		i1 = i2;
                  		i2 = i3;
                  		i3 = getNext(i);
                	}
                	if (i3 != null && i3.getOpcode() == GETFIELD) {
                  		AbstractInsnNode i4 = getNext(i);
                  		if (i4 != null && i4.getOpcode() == PUTFIELD) {
                  			if (sameField(i3, i4)) {
                      			insns.remove(i1);
                      			insns.remove(i2);
                      			insns.remove(i3);
                      			insns.remove(i4);
							}
						}
					} 
				}
			} 
			  super.transform(mn);
		}
    
        private static AbstractInsnNode getNext(Iterator i) {
          	while (i.hasNext()) {
            	AbstractInsnNode in = (AbstractInsnNode) i.next();
            	if (!(in instanceof LineNumberNode)) {
					return in; 
				}
			}
          	return null;
        }
        ... 
}

與上一個實現的區別在於 getNext 方法,它現在是對列表迭代器進行操作。當序列被識別 出來時,迭代器恰好位於它的後面,所以不再需要 while (i.next() != i4) 循環。但這裏 再 次出現了三個或多個連續 ALOAD 0 指令的特殊情況(見 while (i3 != null) 循環)。

7.1.5. 全局轉換

到目前爲止,我們看到的所有方法轉換都是局部的,甚至有狀態的轉換也是如此,所謂“局 部”是指,一條指令 i 的轉換僅取決於與 i 有固定距離的指令。但還存在一些全局轉換,在這 種 轉換中,指令 i 的轉換可能取決於與 i 有任意距離的指令。對於這些轉換,樹 API 真的很 有幫助, 也就是說,使用核心 API 實現它們將會非常非常複雜。

下面的轉換就是這樣一個例子:用向 label 的跳轉代替向 GOTO label 指令的跳轉,然後 用一個 RETURN 指令代替指向這個 RETURN 指令的 GOTO。實際中,一個跳轉指令的目標與這條 指令的距離可能爲任意遠,可能在它的前面,也可能在其之後。這樣一個轉換可實現如下:

public class OptimizeJumpTransformer extends MethodTransformer {
        public OptimizeJumpTransformer(MethodTransformer mt) {
			super(mt); 
		}
        @Override 
        public void transform(MethodNode mn) {
          	InsnList insns = mn.instructions;
          	Iterator<AbstractInsnNode> i = insns.iterator();
          	while (i.hasNext()) {
            		AbstractInsnNode in = i.next();
            		if (in instanceof JumpInsnNode) {
              			LabelNode label = ((JumpInsnNode) in).label;
              			AbstractInsnNode target;
              		// 當 target == goto l,用 l 代替
            			while (true) {
                			target = label;
                				while (target != null && target.getOpcode() < 0) {
                  				target = target.getNext();
                				}
                				if (target != null && target.getOpcode() == GOTO) {
                  					label = ((JumpInsnNode) target).label;
              					} else {
                  					break;
								}
						}
              			// // 更新目標
            			((JumpInsnNode) in).label = label;
             		 // 在可能時,用目標指令代替跳轉
                		if (in.getOpcode() == GOTO && target != null) {
                			int op = target.getOpcode();
                			if ((op >= IRETURN && op <= RETURN) || op == ATHROW) {
                  			// replace ’in’ with clone of ’target’
								insns.set(in, target.clone(null)); 
							}
						} 
					}
        	 }
          	super.transform(mn);
      } 
 }

此代碼的工作過程如下:當找到一條跳轉指令 in 時,它的目標被存儲在 label 中。然 後 用最內層的 while 循環查找緊跟在這個標記之後出現的指令( 不代表實際指令的 AbstractInsnNode 對象,比如 FrameNodeLabelNode,其“操作碼”爲負)。只要這條 指 令是 GOTO,就用這條指令的目標代替 label,然後重複上述步驟。最後,用這個更新後的 label 值 來代替 in 的目標標記,如果 in 本身是一個 GOTO,並且其更新後的目標是一條 RETURN指令,則 in 用這個返回指令的克隆副本代替(回想一下,一個指令對象在一個指令列表中不能 出現一次以上)。

對於 3.1.5 節定義的 checkAndSetF 方法,這一轉換的效果如下

before after
ILOAD 1 ILOAD 1
IFLT label IFLT label
ILOAD 0 ALOAD 0
ILOAD 1 ILOAD 1
PUTFIELD … PUTFIELD …
GOTO end RETURN
label: label:
F_SAME F_SAME
NEW … NEW …
DUP DUP
INVOKESPECIAL … INVOKESPECIAL …
ATHROW ATHROW
end: end:
F_SAME F_SAME
RETURN RETURN

注意,儘管這個轉換改變了跳轉指令(更正式地說,是改變了控制流圖),但它不需要更新 方法的幀。事實上,在每條指令處,執行幀的狀態保持不變,而且由於沒有引用新的跳轉目標, 所以並不需要訪問新的幀。但是,可能會出現不再需要某個幀的情況。例如在上面的例子中,轉 換
後不再需要 end 標記,它後面的 F_SAME 幀和 RETURN 指令也是如此。幸好,訪問幀數超出 必需數量是完全合法的,在方法中包含未被使用的代碼(稱爲死代碼或不可及代碼)也是合法的。 因此,上述方法適配器是正確的,儘管可對其進行改進,刪除死代碼和幀。

7.2. 組件合成

到目前爲止,我們僅看到了如何創建和轉換 MethodNode 對象,卻還沒有看到與類的字節 數組表示進行鏈接。和類的情景一樣,這一鏈接過程也是通過合成核心 API 和樹 API 組件完成 的,本節就來進行解釋。

7.2.1. 介紹

除了圖 7.1 顯示的字段之外,MethodNode 類擴展了 MethodVisitor 類,還提供了兩個 accept 方法,它以一個 MethodVisitor 或一個 ClassVisitor 爲參數。accept 方法基於 MethodNode 字段值生成事件,而 MethodVisitor 方法執行逆操作,即根據接收到的事 件設 定 MethodNode 字段。

7.2.2. 模式

和類的情景一樣,有可能與核心 API 使用一個基於樹的方法轉換器,比如一個方法適配器。 用於類的兩種模式實際上對於方法也是有效的,其工作方式完全相同。基於繼承的模式如下:

public class MyMethodAdapter extends MethodNode {
	public MyMethodAdapter(int access, String name, String desc, String signature, String[] exceptions, MethodVisitor mv) {
          super(ASM4, access, name, desc, signature, exceptions);
          this.mv = mv;
        }
        @Override 
        public void visitEnd() {
         // 將你的轉換代碼放在這
			accept(mv);
		} 
	}

而基於委託的模式爲:

public class MyMethodAdapter extends MethodVisitor {
	MethodVisitor next;
	public MyMethodAdapter(int access, String name, String desc, String signature, String[] exceptions, MethodVisitor mv) {
		super(ASM4, new MethodNode(access, name, desc, signature, exceptions));
		next = mv; 
	}
	@Override 
	public void visitEnd() { 
		MethodNode mn = (MethodNode) mv;
		
		mn.accept(next);
		} 
	}

第一種模式的一種變體是直接在 ClassAdaptervisitMethod 中將它與一個匿名內部 類一起使用:

public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
	return new MethodNode(ASM4, access, name, desc, signature, exceptions) {
		@Override 
		public void visitEnd() {
			// 將你的轉換代碼放在這
			accept(cv);
		} 
	};
}

這些模式表明,可以將樹 API 僅用於方法,將核心 API 用於類。在實踐中經常使用這一策 略。

8.方法分析

本章介紹用於分析方法代碼的 ASM API,它是基於樹 API 的。首先介紹代碼分析算法,然 後以一些示例介紹相應的 ASM API。

8.1. 介紹

代碼分析是一個很大的主題,存在許多代碼分析算法。我們不可能在這裏介紹所有這些算法, 也超出了本文檔的範圍。事實上,這一節的目的只是概述 ASM 中使用的算法。關於這一主題的 更 好介紹,可以在有關編譯器的書中找到。接下來的幾節將介紹代碼分析技術的兩個重要類型, 即數據流和控制流分析:

  • 數據流分析包括:對於一個方法的每條指令,計算其執行幀的狀態。這一狀態可能採用一種多少有些抽象的方式來表示。例如,引用值可能用一個值來表示,可以每個類一 個值,可以是{null, 非 null,可爲 null}集合中的三個可能值表示,等等。
  • 控制流分析包括計算一個方法的控制流圖,並對這個圖進行分析。控制流圖中的節點爲 指令,如果指令 j 可以緊跟在 i 之後執行,則圖的有向邊將連接這兩條指令 i→j。

8.1.1. 數據流分析

有兩種類型的數據流分析可以執行:

  • 正向分析是指對於每條指令,根據執行幀在執行此指令之前的狀態,計算執行幀在這 一 指令之後的狀態。
  • 反向分析是指對於每條指令,根據執行幀在執行此指令之後的狀態,計算執行幀在這 一 指令之前的狀態。

正向數據流分析的執行是對於一個方法的每個字節代碼指令,模擬它在其執行幀上的執行, 通常包括:

  • 從棧中彈出值
  • 合併它們
  • 將結果壓入棧中

這看起來似乎就是解釋器或 Java 虛擬機做的事情,但事實上,它是完全不同的,因爲其目 標是對於所有可能出現的參數值,模擬一個方法中的所有可能執行路徑,而不是由某一組特定 方法參數值所決定的單一執行路徑。一個結果就是,對於分支指令,兩個路徑都將被模擬(而 實 際解釋器將會根據實際條件值,僅沿一條分支執行)。

另一個結果是,所處理的值實際上是由可能取值組成的集合。這些集合可能非常大,比如“所 有可 能值”,“所有整數”,“所有可能對象”或者“所有可能的 String 對象”,在這些情況下, 可以 將它們稱爲類型。它們也可能更爲準確,比如“所有正整數”,“所有介於 0 到 10 之間的整 數”, 或者“所有不爲 null 的可能對象”。要模擬指令 i 的執行,就是要對於其操作數取值集合 中的所有 組合形式,找出 i 的所有可能結果集。例如,如果整數由以下三個集合表示:P=“正整 數或 null”, N=“負整數或 null”,A=“所有整數”,要模擬 IADD 指令,就意味着當兩個操作 數均爲 P 時返回 P,當兩個操作數均爲 N 時返回 N,在所有其他情況下返回 A

最後一個後果是需要計算取值的並集:例如,與**(b ? e1 : e2)對應的可能值集是 e1 的可能值與 e2 的可能值的並集。更一般地說,每當控制流圖包含兩條或多條具有同一目的地的 邊 時,就需要這一操作。在上面的例子中,整數由三個集合 P、N 和 A 表示,可以很容易地 計算出 這些集合中兩個集合的並集:除非這兩個集合相等,否則總是A**。

8.1.2. 控制流分析

控制流分析的基礎是方法的控制流圖。舉個例子,3.1.3 節 checkAndSetF 方法的控制流圖 給出如下(圖中包含的標記類似於實際指令):
在這裏插入圖片描述
這個圖可以分解爲四個基本模塊(如圖中的矩形所示),一個基本模塊就是這樣一個指令序 列:除最後一條指令外,每個指令都恰有一個後繼者,而且除第一條外,所有其他指令都不是跳 轉的目標。

8.2. 接口和組件

用於代碼分析的 ASM APIorg.objectweb.asm.tree.analysis 包中。由包的名字 可以看出,它是基於樹 API 的。事實上,這個包提供了一個進行正向數據流分析的框架。

爲了能夠以準確度不一的取值進行各種數據流分析,數據流分析算法分爲兩部分:一種是
固定的,由框架提供,另一種是變化的,由用戶提供。更準確地說:

  • 整體數據流分析算法、將適當數量的值從棧中彈出和壓回棧中的任務僅實現一次,用於AnalyzerFrame 類中的所有內容。
  • 合併值的任何和計算值集並集的任務由用戶定義的 InterpreterValue 抽象類的子類提供。提供了幾個預定義的子類,下面幾節將進行介紹。

儘管框架的主要目的是執行數據流分析,但 Analyzer 類也可構造所分析方法的控制流圖。 爲此,可以重寫這個類的 newControlFlowEdgenewControlFlowExceptionEdge 方法, 它們默認情況下不做任何事情。其結果可用於進行控制流分析。

8.2.1. 基本數據流分析

Interpreter 類是抽象類中預定義的 Interpreter 子類之一。它利用在 BasicValue類中定義的七個值集來模擬字節代碼指令的效果:

  • UNINITIALIZED_VALUE 指“所有可能值”。
  • INT_VALUE 指“所有 int、short、byte、boolean 或 char值”。
  • FLOAT_VALUE 指“所有 float 值”。 LONG_VALUE 指“所有 long 值”。
  • DOUBLE_VALUE 指“所有 double 值”。
  • REFERENCE_VALUE 指“所有對象和數組值”。
  • RETURNADDRESS_VALUE 用於子例程(見附錄 A.2)

這個解釋器本身不是非常有用(方法幀中已經提供了這一信息,而且更爲詳細——見 3.1.5節),但它可以用作一個“空的”Interpreter 實現,以構建一個 Analyzer。這個分析器可用於檢測方法中的不可及代碼。事實上,即使是沿着跳轉指令的兩條分支,也不可能到達那些不 能由第一條指令到達的代碼。其結果是:在分析之後,無論什麼樣的 Interpreter 實現,由Analyzer.getFrames 方法返回的計算幀,對於不可到達的指令都是 null。這一特性可用於非常輕鬆地實現一個 RemoveDeadCodeAdapter 類(還有一些更高效的方法,但它們需要編 寫的代碼也更多):

public class RemoveDeadCodeAdapter extends MethodVisitor{ 
	String owner;
	MethodVisitor next;
	
	public RemoveDeadCodeAdapter(String owner, int access, String name, String desc, MethodVisitor mv) {
		super(ASM4, new MethodNode(access, name, desc, null, null)); 
		this.owner = owner;
		next = mv;
	}
	
 	@Override 
 	public void visitEnd(){ 
 	MethodNode mn = (MethodNode) mv; 		
 	Analyzer<BasicValue> a = new Analyzer<BasicValue>(new BasicInterpreter());
 	
	try {
		a.analyze(owner, mn);
		Frame<BasicValue>[] frames = a.getFrames();
 		AbstractInsnNode[] insns = mn.instructions.toArray(); 
 		for (int i = 0; i < frames.length; ++i) {
			if (frames[i] == null && !(insns[i]  instanceof LabelNode)) { 
			mn.instructions.remove(insns[i]);
			} 
		}
     } catch (AnalyzerException ignored) {}
	mn.accept(next); 
	}
}

結合 7.1.5 節的 OptimizeJumpAdapter,由跳轉優化器引入的死代碼被移除。例如, 對 checkAndSetF 方法應用這個適配器鏈將給出:
在這裏插入圖片描述
注意,死標記未被移除。這是故意的:它實際上沒有改變最終代碼,但避免刪除一個儘管 不 可及但可能會在比如 LocalVariableNode 中引用的標記。

8.2.2. 基本數據流驗證器

BasicVerifier 類擴展 BasicInterpreter 類。它使用的事件集相同,但不同於BasicInterpreter 的是,它會驗證對指令的使用是否正確。例如,它會驗證 IADD 指令的操 作數爲 INTEGER_VALUE 值(而 BasicInterpreter 只是返回結果,即 INTEGER_VALUE)。 這個類可在開發類生成器或適配器時進行調試,見 3.3 節的解釋。例如,這個類可以檢測出 ISTORE 1 ALOAD 1 序列是無效的。它可以包含在像下面這樣一個實用工具適配器中(在實踐 中,使用 CheckMethodAdapter 類要更簡單一些,可以將其配置爲使用 BasicVerifier):

public class BasicVerifierAdapter extends MethodVisitor {
        String owner;
        MethodVisitor next;
        public BasicVerifierAdapter(String owner, int access, String name, String desc, MethodVisitor mv) {
          super(ASM4, new MethodNode(access, name, desc, null, null));
          this.owner = owner;
          next = mv;
        }
        @Override 
        public void visitEnd() {
          MethodNode mn = (MethodNode) mv;
          Analyzer<BasicValue> a =
              new Analyzer<BasicValue(new BasicVerifier());
          try {
            a.analyze(owner, mn);
          } catch (AnalyzerException e) {
            throw new RuntimeException(e.getMessage());
}
          mn.accept(next);
        }
}

8.2.3. 簡單數據流驗證器

SimpleVerifier 類擴展了 BasicVerifier 類。它使用更多的集合來模擬字節代碼指令的執行:事實上,每個類都由它自己的集合表示,這個集合表示了這個類的所有可能對象。因此, 它可以檢測出更多的錯誤,比如如下情況:一個對象的可能值爲“所有 Thread 類型的對象”, 卻對這個對象調用在 String 類中定義的方法。

這個類使用 Java 反射 API,以執行與類層次結構有關的驗證和計算。然後,它將一個方 法 引用的類加載到 JVM 中。這一默認行爲可以通過重寫這個類的受保護方法來改變。
BasicVerifier 一樣,這個類也可以在開發類生成器或適配器時使用,以便更輕鬆地 找 出 Bug。但它也可以用於其他目的。下面這個轉換就是一個例子,它會刪除方法中不必要的類 型轉 換:如果這個分析器發現 CHECKCAST to 指令的操作數是“所有 from 類型的對象”值集, 如 果 tofrom 的一個超類,那 CHECKCAST 指令就是不必要的,可以刪除。這個轉換的實現 如下:

public class RemoveUnusedCastTransformer extends MethodTransformer {
        String owner;
        public RemoveUnusedCastTransformer(String owner,
            MethodTransformer mt) {
          super(mt);
          this.owner = owner;
        }
        @Override public MethodNode transform(MethodNode mn) {
          Analyzer<BasicValue> a =
              new Analyzer<BasicValue>(new SimpleVerifier());
try {
a.analyze(owner, mn);
Frame<BasicValue>[] frames = a.getFrames(); AbstractInsnNode[] insns = mn.instructions.toArray(); for (int i = 0; i < insns.length; ++i) {
              AbstractInsnNode insn = insns[i];
              if (insn.getOpcode() == CHECKCAST) {
                Frame f = frames[i];
                if (f != null && f.getStackSize() > 0) {
                  Object operand = f.getStack(f.getStackSize() - 1);
Class<?> to = getClass(((TypeInsnNode) insn).desc); Class<?> from = getClass(((BasicValue) operand).getType()); if (to.isAssignableFrom(from)) {
                    mn.instructions.remove(insn);
                  }
} }
            }
          } catch (AnalyzerException ignored) {
          }
          return mt == null ? mn : mt.transform(mn);
        }
        private static Class<?> getClass(String desc) {
          try {
            return Class.forName(desc.replace(/,.));
          } catch (ClassNotFoundException e) {
            throw new RuntimeException(e.toString());
            } }
        private static Class<?> getClass(Type t) {
          if (t.getSort() == Type.OBJECT) {
            return getClass(t.getInternalName());
          }
          return getClass(t.getDescriptor());
        }
}

但對於 Java 6 類(或者用 COMPUTE_FRAMES 升級到 Java 6 的類),用 AnalyzerAdapter 以核心 API 來完成這一任務要更簡單一些,效率要高得多:

public class RemoveUnusedCastAdapter extends MethodVisitor {
        public AnalyzerAdapter aa;
        public RemoveUnusedCastAdapter(MethodVisitor mv) {
          super(ASM4, mv);
        }
        @Override 
        public void visitTypeInsn(int opcode, String desc) {
        	if (opcode == CHECKCAST) {
            	Class<?> to = getClass(desc);
            	if (aa.stack != null && aa.stack.size() > 0) {
              		Object operand = aa.stack.get(aa.stack.size() - 1);
					if (operand instanceof String) {
						Class<?> from = getClass((String) operand); 
						if (to.isAssignableFrom(from)) {
							return; 
						}
					} 
				}
			}
        	mv.visitTypeInsn(opcode, desc);
        }
        private static Class getClass(String desc) {
          try {
          		return Class.forName(desc.replace(/,.));
          } catch (ClassNotFoundException e) {
          		throw new RuntimeException(e.toString());
          }
} }

8.2.4. 用戶定義的數據流分析

假定我們希望檢測出一些字段訪問和方法調用的對象可能是 null,比如在下面的源代碼段 中 (其中,第一行防止一些編譯器檢測 Bug,否則它可能會被認作一個“o 可能尚未初始化”錯 誤)

		Object o = null;
     	while (...) {o = ...; }
      	o.m(...); //潛在的 NullPointerException!

於是我們需要一個數據流分析,它能告訴我們,在對應於最後一行的 INVOKEVIRTUAL 指 令 處,與 o 對應的底部棧值可能爲 null。爲此,我們需要爲引用值區分三個集合:包含 null
值的 NULL 集,包含所有非 null 引用值的 NONNULL 集,以及包含所有引用值的 MAYBENULL 集。於是,我們只需要考慮 ACONST_NULLNULL 集壓入操作數棧,而所有其他在棧中壓 入引 用值的指令將壓入 NONNULL 集(換句話說,我們考慮任意字段訪問或方法調用的結果 都不是 null,如果不對程序的所有類進行全局分析,那就不可能得到更好的結果)。爲表示 NULLNONNULL 集的並集,MAYBENULL 集合是必需的。

上述規則必須在一個自定義的 Interpreter 子類中實現。完全可以從頭實現它,但也可 以 通過擴展 BasicInterpreter 類來實現它,而且這種做法要容易得多。事實上,如果我們 考慮 BasicValue.REFERENCE_VALUE 對應於 NONNULL 集,那隻需重寫模擬 ACONST_NULL 執行的方法,使它返回 NULL,還有計算並集的方法:

 class IsNullInterpreter extends BasicInterpreter {
        public final static BasicValue NULL = new BasicValue(null);
        public final static BasicValue MAYBENULL = new BasicValue(null);
        public IsNullInterpreter() {
          super(ASM4);
        }
        @Override 
        public BasicValue newOperation(AbstractInsnNode insn) {
          if (insn.getOpcode() == ACONST_NULL) {
            return NULL;
		}
          	return super.newOperation(insn);
        }
        @Override 
        public BasicValue merge(BasicValue v, BasicValue w) {
          if (isRef(v) && isRef(w) && v != w) {
            return MAYBENULL;
}
   			return super.merge(v, w);
        }
        private boolean isRef(Value v) {
          return v == REFERENCE_VALUE || v == NULL || v == MAYBENULL;
		} 
}

於是,可以很容易地利用這個 IsNullnterpreter 來檢測那些可能導致潛在 null 指針異 常的指令

public class NullDereferenceAnalyzer {
        public List<AbstractInsnNode> findNullDereferences(String owner, MethodNode mn) throws AnalyzerException {
			List<AbstractInsnNode> result = new ArrayList<AbstractInsnNode>();
			Analyzer<BasicValue> a = new Analyzer<BasicValue>(new IsNullInterpreter()); 
			a.analyze(owner, mn);
			Frame<BasicValue>[] frames = a.getFrames(); 
			AbstractInsnNode[] insns = mn.instructions.toArray(); 
			for (int i = 0; i < insns.length; ++i) {
            	AbstractInsnNode insn = insns[i];
            	if (frames[i] != null) {
              		Value v = getTarget(insn, frames[i]);
              	if (v == NULL || v == MAYBENULL) {
                	result.add(insn);
              	}
			} 
		}
          return result;
        }
        
        private static BasicValue getTarget(AbstractInsnNode insn, Frame<BasicValue> f) {
          	switch (insn.getOpcode()) {
          		case GETFIELD:
          		case ARRAYLENGTH:
         		case MONITORENTER:
          		case MONITOREXIT:
            		return getStackValue(f, 0);
          		case PUTFIELD:
            		return getStackValue(f, 1);
          		case INVOKEVIRTUAL:
          		case INVOKESPECIAL:
          		case INVOKEINTERFACE:
            		String desc = ((MethodInsnNode) insn).desc;
            		return getStackValue(f, Type.getArgumentTypes(desc).length);
          }
          return null;
       }
       
       private static BasicValue getStackValue(Frame<BasicValue> f, int index) {
          int top = f.getStackSize() - 1;
          return index <= top ? f.getStack(top - index) : null;
        }
}

findNullDereferences 方法用一個 IsNullInterpreter 分析給定方法節點。然後, 對於每條指令,檢測其引用操作數(如果有的話)的可能值集是不是 NULL 集或 NONNULL 集。
若是,則這條指令可能導致一個 null 指針異常,將它添加到此類指令的列表中,該列表由這一 方法返回。
getTarget 方法在幀 f 中返回與 insn 對象操作數相對應的 Value,如果 insn 沒有對 象 操作數,則返回 null。它的主要任務就是計算這個值相對於操作數棧頂端的偏移量,這一數量 取決於指令類型。

8.2.5. 控制流分析

控制流分析可以有許多應用。一個簡單的例子就是計算方法的“圓複雜度”。這一度量定 義 爲控制流圖的邊數減去節點數,再加上 2。例如,checkAndSetF 方法的控制流圖如 8.1.2 節所 示,它的圈複雜度爲 11-12+2=1。這個度量很好地表徵了一個方法的“複雜度” (在這個數字與 方法的平均 bug 數之間存在一種關聯)。它還給出了要“正確”測試一個方 法所需要的建議測試 情景數目。
用於計算這一度量的算法可以用 ASM 分析框架來實現(還有僅基於核心 API 的更高效方法, 只是它們需要編寫更多的代碼)。第一步是構建控制流圖。我們在本章開頭曾經說過,可以通過 重 寫 Analyzer 類的 newControlFlowEdge 方法來構建。這個類將節點表示爲 Frame 對象。
如果希望將這個圖存儲在這些對象中,則需要擴展 Frame 類:

class Node<V extends Value> extends Frame<V> {
        Set< Node<V> > successors = new HashSet< Node<V> >();
        public Node(int nLocals, int nStack) {
          super(nLocals, nStack);
        }
        public Node(Frame<? extends V> src) {
          super(src);
		} 
}

隨後,可以提供一個 Analyzer 子類,用來構建控制流圖,並用它的結果來計算邊數、節 點數,最終計算出圈複雜度:

 public class CyclomaticComplexity {
        public int getCyclomaticComplexity(String owner, MethodNode mn) throws AnalyzerException {
        	Analyzer<BasicValue> a = new Analyzer<BasicValue>(new BasicInterpreter()) {
           		protected Frame<BasicValue> newFrame(int nLocals, int nStack) {
              	return new Node<BasicValue>(nLocals, nStack);
            }
        	protected Frame<BasicValue> newFrame(Frame<? extends BasicValue> src) {
              	return new Node<BasicValue>(src);
            }
        	protected void newControlFlowEdge(int src, int dst) {
              	Node<BasicValue> s = (Node<BasicValue>) getFrames()[src];
              s.successors.add((Node<BasicValue>) getFrames()[dst]);
			} 
		};
          a.analyze(owner, mn);
          Frame<BasicValue>[] frames = a.getFrames();
          int edges = 0;
          int nodes = 0;
          for (int i = 0; i < frames.length; ++i) {
            	if (frames[i] != null) {
              	edges += ((Node<BasicValue>) frames[i]).successors.size();
              	nodes += 1;
				} 
		  }
          return edges - nodes + 2;
        }
}

9.元數據

本章介紹用於編譯 Java 類元數據(比如註釋)的樹 API。本章非常短,因爲這些元數據已 經在第 4 章介紹過了,而且在瞭解了相應的核心 API 之後,樹 API 就很簡單了。

9.1. 泛型

樹 API 沒有提供對泛型的任何支持!事實上,它用簽名表示泛型,這一點與核心 API 中一 樣,但卻沒有提供與 SignatureVisitor 對應的 SignatureNode 類,儘管這也是可能的(事 實上,至少使用幾個 Node 類來區分類型、方法和類簽名會很方便)。

9.2. 註解

註釋的樹 API 都基於 AnnotationNode 類,它的公共 API 如下:

  public class AnnotationNode extends AnnotationVisitor {
        public String desc;
        public List<Object> values;
        public AnnotationNode(String desc);
        public AnnotationNode(int api, String desc);
        ... // methods of the AnnotationVisitor interface
        public void accept(AnnotationVisitor av);
}

desc 字段包含了註釋類型,而 values 字段包含了名稱/值對,其中每個名字後面都跟 有相 關聯的值(值的表示在 Javadoc 中描述)。

可以看出,AnnotationNode 類擴展了 AnnotationVisitor 類,還提供了一個 accept
方法,它以一個這種類型的對象爲參數,比如具有這個類和方法訪問器類的 ClassNodMethodNode 類。我們前面已經看到用於類和方法的模式也可用於合成處理註釋的核心與樹 API 組件。例如,對於基於繼承的模式(見 7.2.2 節),可進行“匿名內部類”的變體,使其適用於注 釋,給出如下:

public AnnotationVisitor visitAnnotation(String desc, boolean visible) { 
	return new AnnotationNode(ASM4, desc) {
		@Override 
		public void visitEnd() {
	// put your annotation transformation code here 	
		accept(cv.visitAnnotation(desc, visible));
		} 
	};
}

9.3. 調試

作爲被編譯類來源的源文件存儲在 ClassNode 中的 sourceFile 字段中。關於源代碼行 號的信息存儲在 LineNumberNode 對象中,它的類繼承自 AbstractInsnNode。在覈心 API 中,關於行號的信息是與指令同時受訪問的,與此類似,LineNumberNode 對象是指令列表 的 一部分。最後,源局部變量的名字和類型存儲在 MethodNodelocalVariables 字 段中, 它是 LocalVariableNode 對象的一個列表。

10. 向後兼容

與核心 API 的情景一樣,在 ASM 4.0 的樹 API 中已經引入了一種新機制,用於確保未來
ASM 版本的後向兼容性。但要再次強調,僅靠 ASM 自身不能保證這一性質。它要求用戶在編寫 代 碼時遵循一些簡單的規則。本章的目標就是介紹這些規則,並大致介紹 ASMAPI 中用於確 保後向兼容的內部機制。

10.1 介紹

本節給出一些規則,在使用 ASM 樹 API 時,要想確保你的代碼在所有未來 ASM 版本中 都保 持有效(其意義見 5.1.1 節定義的約定),就必須遵循這些規則。

首先,如果使用樹 API 編寫一個類生成器,那就不需要遵循什麼規則(和核心 API 一樣)。 可以用任意構造器創建 ClassNode 和其他元素,可以使用這些類的任意方法。

另一方面,如果要用樹 API 編寫類分析器或類適配器,也就是說,如果使用 ClassNode 或其他直接或間接地通過 ClassReader.accept()填充的類似類,或者如果重寫這些類中的 一個,則必須遵循下面給出的規則。

10.2 準則

10.2.1. 基本規則

創建類節點

考慮這樣一種情景,我們創建一個 ClassNode,通過一個 ClassReader 填充它,然後 分 析或轉換它,最終根據需要用 ClassWriter 寫出結果(這一討論及相關規則同樣適用於 其他 節點類;對於由別人創建的 ClassNode,其分析或轉換在下一節討論)。在這種情況 下,僅有 一條規則:

規則 3:要用 ASM 版本 X 的樹 API 編寫類分析器或適配器,則使用以這一確切版本爲 參數 的構造器創建 ClassNode(而不是使用沒有參數的默認構造器)。

本規則的目的是在通過一個 ClassReader 填充ClassNode 時,如果遇到未知特性,則拋 出一個錯誤(根據後向兼容性約定的定義)。如果不遵循這一規則,在以後遇到未知元素時,你的分析或轉換代碼可能會失敗,也許能夠成功運行,但卻因爲沒有忽略這些未知元素而給出錯
誤 結果。換言之,如果不遵循這一規則,可能無法保證約定的最後一項條款。

如何做到呢?ASM 4.0 內部對 ClassNode 的實現如下(這裏重複使用 5.1.2 節的示例):

public class ClassNode extends ClassVisitor {
        public ClassNode() {
          	super(ASM4, null);
        }
        public ClassNode(int api) {
          	super(api, null);
        }
        ...
        public void visitSource(String source, String debug) {
          // store source and debug in local fields ...
        }
}

在 ASM 5.0 中,這一代碼變爲:

public class ClassNode extends ClassVisitor {
        ...
        public void visitSource(String source, String debug) {
          if (api < ASM5) {
            // store source and debug in local fields ...
          } else {
            visitSource(null, source, debug);
          }
        }
        public void visitSource(Sring author, String source, String debug) {
          if (api < ASM5) {
            if (author == null)
                          visitSource(source, debug);
            else
              throw new RuntimeException();
          } else {
            // store author, source and debug in local fields ...
          }
        }
        public void visitLicense(String license) {
          if (api < ASM5) throw new RuntimeException();
          // store license in local fields ...
        }
}

如果使用 ASM 4.0,那創建 **ClassNode(ASM4)**沒有什麼特別之處。但如果升級到 ASM 5.0, 但不修改代碼,那就會得到一個 ClassNode 5.0,它的 api 字段將爲 ASM4 < ASM5。於 是容易看 出,如果輸入類包含一個非 null 作者或許可屬性,那通過 ClassReader 填充 ClassNode 時將會 失敗,如約定中的定義。如果還升級你的代碼,將 api 字段改爲 ASM5,並 升級剩餘代碼,將這些新屬性考慮在內,那在填充代碼時就不會拋出錯誤。

注意,ClassNode 5.0 代碼非常類似於 ClassVisitor 5.0 代碼。這是爲了確保在定義 ClassNode 的子類時能夠擁有正確的語義(類似於 ClassVisitor 的子類——見 10.2.2 節)。

使用現有類代碼

如果你的類分析器或適配器收到別人創建的 ClassNode,那你就不能肯定在創建它時傳送給其構造器的 ASM 版本。當然可以自行檢查 api 字段,但如果發現這個版本高於你支持的版本,直接拒絕這個類可能太過保守了。事實上,這個類中可能沒有包含任何未知特性。另一方面,你 不能檢查是否存在未知特性(在我們的示例情景中,在爲 ASM 4.0 編寫代碼時,你如何判斷你 的 ClassNode 中不存在未知的 license 字段呢?因爲你在這裏還不知道未來會添加這樣一個 字 段)。於是設計了 ClassNode.check() 方法來解決這個問題。這就引出了以下規則:

規則 4:要用 ASM 版本 X 的樹 API 編寫一個類分析器或適配器,使用別人創建的 ClassNode,在以任何方式使用這個 ClassNode 之前,都要以這個確切版本號爲參數,調 用 它的 check() 方法。

其目的與規則 3 相同:如果不遵循這一規則,可能無法保證約定的最後一項條款。如何做 到 的呢?這個檢查方法在 ASM 4.0 內部的實現如下:


      public class ClassNode extends ClassVisitor {
        ...
        public void check(int api) {
          // nothing to do
} }

在 ASM 5.0 中,這一代碼變爲:

    public class ClassNode extends ClassVisitor {
        ...
        public void check(int api) {
          if (api < ASM5 && (author != null || license != null)) {
            throw new RuntimeException();
          }
} }

如果你的代碼是爲 ASM 4.0 編寫的,而且如果得到一個 ClassNode 4.0,它的 api 字段 將爲 ASM4,這樣不會有問題,check也不做任何事情。但如果你得到一個 ClassNode5.0,如 果這個節點實際上包含了非 null authorlicense,也就是說, 它包含了 ASM 4.0 中未知 的新特性,那 check(ASM4)方法將會失敗。

注意:如果你自己創建 ClassNode,也可以使用這一規則。那就不需要遵循規則 3,也就是說,不需要在 ClassNode 構造器中指明 ASM 版本。這一檢查將在 check 方法中進行(但在填充 ClassNode 時,這 種做法的效率要低於在之前進行檢查)。

10.2.2. 繼承規則

如果希望提供 ClassNode 的子類或者其他類似節點類,那麼規則 1 和 2 都是適用的。
注意, 在一個 MethodNode 匿名子類的一個常用特例中,visitEnd() 方法被重寫:


      class MyClassVisitor extends ClassVisitor {
        ...
        public MethodVisitor visitMethod(...) {
			final MethodVisitor mv = super.visitMethod(...);
         	 if (mv != null) {
				return new MethodNode(ASM4) {
			 public void visitEnd() {
                // perform a transformation
                accept(mv);
              }
			}
		}
		return mv; }
}

那就自動適用規則 2(匿名類不能被重寫,儘管沒有明確將它聲明爲 final 的)。你只需要 遵循 規則 3,也就是說,在 MehtodNode 構造器中指定 ASM 版本(或者遵循規則 4,也 就是在執行 轉換之前調用 check(ASM4))

10.2.3. 其他包

asm.utilasm.commons 中的類都有兩個構造函數變體:一個有 ASM 版本參數,一個沒有。

如果只是希望像 asm.util 中的 ASMifier、TextifierCheckXxx Adapter 類 或者 asm.commons 包中的任意類一樣,加以實例化和應用,那可以用沒有 ASM 版本參數 的構造器 來實例化它們。也可以使用帶有 ASM 版本參數的構造器,那就會不必要地將這些 組件限制於特 定的 ASM 版本(而使用無參數構造器相當於在說“使用最新的 ASM 版 本”)。這就是爲什麼使 用 ASM 版本參數的構造器被聲明爲 protected。

另一方面,如果希望重寫 asm.util 中的 ASMifier、Textifier 或 CheckXxx Adapter類或者 asm.commons 包中的任意類,那適用規則 1 和 2。具體來說,你的構造器必須以你 希望 用作參數的 ASM 版本來調用 super(…)。

最後,如果希望使用或重寫 asm.tree.analysis 中的 Interpreter 類或其子類, 必須 做出同樣的區分。還要注意,在使用這個分析包之前,創建一個 MethodNode 或者從別 人那裏 獲取一個,那在將這一代碼傳送給 Analyzer 之前必須使用規則 3 和 4。

A.附錄135

A.1. 字節碼指令

本節對字節代碼指令進行簡要描述。如需全面描述,請參閱 Java 虛擬機規範。

約定:a 和 b 表示 int, float, long 或 double 值(比如,它們對於 IADD 表示 int,而 對於 LADD 則表示 long),op 表示對象引用,v 表示任意值(或者,對於棧指令,表示大小 爲 1 的值),w 表示 longdoubleijn 表示 int 值。

局部變量

Instruction Stack before Stack after
ILOAD, LLOAD, FLOAD, DLOAD var … , a
ALOAD var … , o
ISTORE, LSTORE, FSTORE, DSTORE var … , a
ASTORE var … , o
IINC var incr

Instruction Stack before Stack after
POP … , v
POP2 … , v1 , v2
… , w
DUP … , v … ,v,v
DUP2 … , v1 , v2 … ,v1 ,v2 ,v1 ,v2
… , w … ,w,w
SWAP … , v1 , v2 … , v2 , v1
DUP_X1 … , v1 , v2 … ,v2 ,v1 ,v2
DUP_X2 … ,v1 ,v2 ,v3 … ,v3 ,v1 ,v2 ,v3
… ,w,v … ,v,w,v
DUP2_X1 … ,v1 ,v2 ,v3 … ,v2 ,v3 ,v1 ,v2 ,v3
… ,v,w … ,w,v,w
DUP2_X2 … ,v1 ,v2 ,v3 ,v4 … ,v3 ,v4 ,v1 ,v2 ,v3 ,v4
… ,w,v1 ,v2 … ,v1 ,v2 ,w,v1 ,v2
… ,v1 ,v2 ,w … ,w,v1 ,v2 ,w
… , w1 , w2 … ,w2 ,w1 ,w2

常量

Instruction Stack before Stack after
ICONST_n (−1 ≤ n ≤ 5) … , n
LCONST_n (0 ≤ n ≤ 1) … , nL
FCONST_n (0 ≤ n ≤ 2) … , nF
DCONST_n (0 ≤ n ≤ 1) … , nD
BIPUSH b, −128 ≤ b < 127 … , b
SIPUSH s, −32768 ≤ s < 32767 … , s
LDC cst (int, float, long, double, String or Type) … , cst
ACONST_NULL … , null

算術與邏輯

IADD, LADD, FADD, DADD … ,a,b … ,a+b
ISUB, LSUB, FSUB, DSUB … ,a,b … ,a-b
IMUL, LMUL, FMUL, DMUL … ,a,b … ,a*b
IDIV, LDIV, FDIV, DDIV … ,a,b … ,a/b
IREM, LREM, FREM, DREM … ,a,b … ,a%b
INEG, LNEG, FNEG, DNEG … , a … , -a
ISHL, LSHL … ,a,n … ,a<‌<n
ISHR, LSHR … ,a,n … ,a>\‌>n
IUSHR, LUSHR … ,a,n … , a >\‌>\‌> n
IAND, LAND … ,a,b … ,a&b
IOR, LOR … ,a,b … ,a
IXOR, LXOR … ,a,b … ,a^b
LCMP … ,a,b … ,a==b? 0: (a<b? -1: 1)
FCMPL, FCMPG … ,a,b … ,a==b? 0: (a<b? -1: 1)
DCMPL, DCMPG … ,a,b … ,a==b? 0: (a<b? -1: 1)

類型轉化

I2B … , i … , (byte) i
I2C … , i … , (char) i
I2S … , i … , (short) i
L2I, F2I, D2I … , a … , (int) a
I2L, F2L, D2L … , a … , (long) a
I2F, L2F, D2F … , a … , (float) a
I2D, L2D, F2D … , a … , (double) a
CHECKCAST class … , o … , (class) o

對象字段和方法

NEW class … , new class
GETFIELD c f t … , o … , o.f
PUTFIELD c f t … ,o,v
GETSTATIC c f t … , c.f
PUTSTATIC c f t … , v
INVOKEVIRTUAL c m t … ,o,v1 ,… ,vn … , o.m(v1, … vn)
INVOKESPECIAL c m t … ,o,v1 ,… ,vn … , o.m(v1, … vn)
INVOKESTATIC c m t … ,v1 ,… ,vn … , c.m(v1, … vn)
INVOKEINTERFACE c m t … ,o,v1 ,… ,vn … , o.m(v1, … vn)
INVOKEDYNAMIC m t bsm … ,o,v1 ,… ,vn … , o.m(v1, … vn)
INSTANCEOF class … , o … , o instanceof class
MONITORENTER … , o
MONITOREXIT … , o

數組

NEWARRAY type (for any primitive type) … , n … , new type[n]
ANEWARRAY class … , n … , new class[n]
MULTIANEWARRAY […[t n … ,i1 ,… ,in … , new t[i1]…[in]…
BALOAD, CALOAD, SALOAD … ,o,i … , o[i]
IALOAD, LALOAD, FALOAD, DALOAD … ,o,i … , o[i]
AALOAD … ,o,i … , o[i]
BASTORE, CASTORE, SASTORE … ,o,i,j
IASTORE, LASTORE, FASTORE, DASTORE … ,o,i,a
AASTORE … ,o,i,p
ARRAYLENGTH … , o … , o.length

跳轉

IFEQ … , i
IFNE … , i
IFLT … , i
IFGE … , i
IFGT … , i
IFLE … , i
IF_ICMPEQ … ,i,j
IF_ICMPNE … ,i,j
IF_ICMPLT … ,i,j
IF_ICMPGE … ,i,j
IF_ICMPGT … ,i,j
IF_ICMPLE … ,i,j
IF_ACMPEQ … ,o,p
IF_ACMPNE … ,o,p
IFNULL … , o
IFNONNULL … , o
GOTO
TABLESWITCH … , i
LOOKUPSWITCH … , i

返回

IRETURN, LRETURN, FRETURN, DRETURN … , a
ARETURN … , o
RETURN
ATHROW … , o

A2. 子程序

除了上一節給出的字節代碼指令,版本號低於或等於 V1_5 的類還可以包含 JSR 和 RET 指令,用於子例程(JSR 表示 Jump to SubRoutine,即跳轉至子例程,RET 表示 RETurn from subroutine,,即從子例程返回)。版本高於或等於 V1_6 的類不能包含這些指令(移除它們就是爲 了 Java 6 中引入的新驗證器體系結構;因爲它們不是嚴格必需的,所以纔可能刪除它們)。

JSR 指令以一個標記爲參數,無條件跳轉到這個標記。但在跳轉之前,它會在操作數棧中 壓 入一個返回地址,它是緊跟在 JSR 之後的指令的索引。這個返回地址只能由諸如 POP、 DUP 或 SWAP 之類的棧指令、ASTORE 指令和 RET 指令處理。

RET 指令以一個局部變量索引爲參數。它加載包含在這個槽中的返回地址,並無條件跳轉 至相應的指令。由於返回地址可以有幾個可能值,所以 RET 指令可以返回到幾個可能指令。

讓我們用一個例子來說明。考慮以下代碼:

	JSR sub 
	JSR sub 
	RETURN
sub: ASTORE 1 
	IINC 0 1 
	RET 1

第一條指令將第二條指令的索引作爲返回地址壓入棧中,並跳轉到 ASTORE 指令。這個指 令 將返回地址存儲局部變量 1 中。然後,局部變量 0 增 1。最後,RET 指令載入在局部變量 1 中包含的返回地址,並跳轉到相應的指令,即第二條指令。

這個第二指令又是一個 JSR 指令:它將第三條指令的索引作爲返回地址壓入棧中,並跳轉 到 ASTORE 指令。當再次到達 RET 指令時,返回地址現在對應於 RETURN 指令,所以執行過程跳轉到這個 RETURN,並停止。

sub 標記之後的指令定義了一個所謂的子例程。它有點像“方法”,可以從一個正常方法的 不同地方“調用”。在 Java 6 之前,子例程用於編譯 Java 中的 finally 塊。但事實上,子例程 並非必不可少的:實際上,有可能用相應子例程的主體來代替每個 JSR 指令。這種內聯生成了 重複代碼,但刪除了 JSR 和 RET 指令。對於上面的例子,結果非常簡單:

    IINC 0 1
    IINC 0 1
    RETURN

ASMorg.objectweb.asm.commons 包中提供了一個 JSRInlinerAdapter 類,它可 以 自動執行這一轉換。可以用它來刪除 JSR 和 RET 指令,以簡化代碼分析,或者將類從 1.5 或 更低版本轉換爲 1.6 或更高版本。

A.3. 屬性

2.1.1 節曾經解釋過,有可能將任意屬性關聯到類、字段和方法。在引入新特性時,這種可 擴展機制對於擴展類文件格式非常有用。例如,它已經被用於擴展這一格式,以支持註釋、泛 型、 棧映射幀等。這一機制還可由用戶使用,而不只是由 Sun 公司使用,但自從在 Java 5 中 引入注 釋之後,註釋的使用就比屬性容易得多。也就是說,如果你真的需要使用自己的屬性, 或者必須 管理由別人定義的非標準屬性,可以在 ASM 用 Attribute 類完成。

默認情況下,ClassReader 類會爲它找到的每個標準屬性創建一個 Attribute 實例,並以這個實例爲參數,調用 visitAttribute 方法(至於是 ClassVisitor、FieldVisitor, 還是 MethodVisitor 類的該方法,則取決於上下文)。這個實例中包含了屬性的原始內容,其形式 爲私有字節數組。在訪問這種未知屬性時,ClassWriter 類就是將這個原始字節數組複製到它 構建的類中。這一默認行爲只有在使用 2.2.4 節介紹的優化時纔是安全的(除了提高性能外,這 是使用該優化的另一原因)。沒有這一選項,原內容可能會與類編寫器創建的新常量池不一 致, 從而導致類文件被損壞。

默認情況下,非標準屬性會以它在已轉換類中的形式被複制,它的內容對 ASM 和用戶來說 是完全不透明的。如果需要訪問這一內容,必須首先定義一個 Attribute 子類,能夠對原內容 進行解碼並重新編碼。還必須在 ClassReader.accept 方法中傳送這個類的一個原型實例,使 這個類可以解碼這一類型的屬性。讓我們用一個例子來說明這一點。下面的類可用於運行一個設 想 的“註釋”特性,它的原始內容是一個 short 值,引用存儲在常量池中的一個 UTF8 字符串:

class CommentAttribute extends Attribute {
        private String comment;
        public CommentAttribute(final String comment){
          	super("Comment");
          	this.comment = comment;
        }
        public String getComment() {
          	return comment;
		} 
		@Override
        public boolean isUnknown() {
          	return false;
		}
		@Override
		protected Attribute read(ClassReader cr, int off, int len, char[] buf, int codeOff, Label[] labels) {
          	return new CommentAttribute(cr.readUTF8(off, buf));
		}
		@Override
        protected ByteVector write(ClassWriter cw, byte[] code, int len, int maxStack, int maxLocals) {
          	return new ByteVector().putShort(cw.newUTF8(comment));
        }
}

最重要的方法是 read 和 write 方法。read 方法對這一類型的屬性的原始內容進行解碼, write 方法執行逆操作。注意,read 方法必須返回一個新的屬性實例。爲了在讀取一個類時實 現這種屬性的解碼,必須使用:

      ClassReader cr = ...;
      ClassVisitor cv = ...;
      cr.accept(cv, new Attribute[] { new CommentAttribute("") }, 0);

這個“註釋”屬性將被識別,併爲它們中的每一個都創建一個 CommentAttribute實 例 (而未知屬性仍將用 Attribute 實例表示)。

A.4. 準則

現在回憶一下爲確保你的代碼能與較早的 ASM 版本保持後向兼容性而必須遵循的規則
(見 第 5 章和第 10 章)。

  • 規則 1:要爲 ASM X 編寫一個 ClassVisitor 子類,就以這個版本號爲參數,調用ClassVisitor 構造器,在這個版本的 ClassVisitor 類中,絕對不要重寫或調用被棄用的 方法(或者將在之後版本引入的方法)。
  • 規則 2:不要使用訪問器的繼承,而要使用委託(即訪問器鏈)。一種好的做法是讓 你 的訪問器類在默認情況下成爲 final 的,以確保這一特性。
  • 規則 3:要用 ASM 版本 X 的樹 API 編寫類分析器或適配器,則使用以這一確切版本爲 參數 的構造器創建 ClassNode(而不是使用沒有參數的默認構造器)。
  • 規則 4:要用 ASM 版本 X 的樹 API 編寫一個類分析器或適配器,使用別人創建的 ClassNode,在以任何方式使用這個 ClassNode 之前,都要以這個確切版本號爲參數,調 用 它的 check() 方法。

規則 1 和 2 還適用於 ClassNode、MethodNode 等的子類,asm.tree.analysis 中 Interpreter 及其子類的子類,asm.util 中 ASMifier、Texifier 或 CheckXxx Adapter 類的子類,asm.commons 包中任意類的子類。最後,規則 2 有兩個例外:

  • 如果能夠完全由自己控制繼承鏈,並同時發佈層次結構中的所有類,那就可以使用訪問 器的繼承。然後必須確保層次結構中的所有類都是爲同一 ASM 版本編寫的。仍然要讓層次結構的葉類是 final 的。
  • 如果除了葉類之外,沒有其他類重寫任何訪問方法(例如,如果只是爲了引入方便的方法而在 ClassVisitor 和具體訪問類之間使用了中間類),那就可以使用“訪問器”的繼承。仍然要讓層次結構的葉類是 final 的(除非它們也沒有重寫任何訪問方法;在 這種情況下,提供一個以 ASM 版本爲參數的構造器,使子類可以指定它們是爲哪個版本編寫的)。

A.5. 性能

下圖給出核心與樹 API、ClassWriter 選項和分析框架的相對性能(越短越快):
在這裏插入圖片描述
引用時間 100 對應於直接鏈接到 ClassWriterClassReader。“加計時器”和“移除 序列”測試對應於 AddTimerAdapterRemoveGetFieldPutFieldAdapter(斜體表示 使 用 2.2.4 節所述的優化,粗體表示使用樹 API)。總轉換時間分解爲三個部分:類分析(下)、 類轉換或分析(中間)和類的寫入(上)。對於每個測試,測量值都是分析、轉換和寫入一個字 節 數組所需要的時間,也就是從磁盤加載類並將它們加載到 JVM 所需要的時間未考慮在內。爲獲得這些結果,將每個測試對於 JDK 7 rt.jar 上的 18600 多個類運行 10 次,並採用最佳運
行時獲得的性能。
快速分析一下這些結果表明:

  • 90%的轉換時間用於類分析和寫入。
  • “複製常量池”優化可提速 15-20%。
  • 基於樹的轉換要比基於訪問器的慢大約 25%。
  • COMPUTE_FRAMES 選項耗時很多⇒進行增量幀更新。
  • 分析包的成本非常高!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章