文章詳情頁
ASM2.0字節碼框架介紹
瀏覽:110日期:2024-07-19 16:47:51
內容: 摘要:Java的特性如動態類加載和反射使其成為動態語言。然而在許多時候,反射是不夠的,而且開發人員需要從非Java源程序中生成字節碼,如腳本語言Groovy和BeanShell,或者源數據如ORM配置。當使用已經存在的類時,特別是當沒有源程序時,就需要使用一些工具來做如分析類或方法的依賴性以便生成測試度量,或者來檢查是否存在問題或反模式。Java5中增加了一些新特性,如如注解和范型,這會影響字節碼結果因而需要字節碼處理工具特別注意以便保持良好的性能。本文會通過一個最小且最快的Java字節碼處理框架來演示。版權聲明:任何獲得Matrix授權的網站,轉載時請務必保留以下作者信息和鏈接作者:Eugene Kuleshov;xMatrix(作者的blog:http://blog.matrix.org.cn/page/xMatrix)原文:http://www.onjava.com/pub/a/onjava/2005/08/17/asm3.html譯文:http://www.matrix.org.cn/resource/article/44/44220_ASM+Bytecode+Framework.html關鍵字:ASM;Bytecode;Framework框架結構ASM字節碼處理框架是用Java開發的而且使用基于訪問者模式生成字節碼及驅動類到字節碼的轉換。這允許開發人員避免直接處理方法字節碼中的類常量池及偏移,因此為開發人員隱藏了字節碼的復雜性并且相對于其他類似工具如BCEL, SERP, or Javassist提供了更好的性能。ASM分為幾個包更方便靈活地構建。包結構圖如圖1。 Figure 1. Arrangement of ASM packages·Core包提供了讀/寫/轉換字節碼的API而且是其他包的基礎。這個包已經足夠生成Java字節碼而且能夠實現大部分的字節碼轉換。·Tree包提供了Java字節碼的內存內表示。·Analysis包為存儲在來自Tree包結構中的Java方法字節碼提供了基礎的數據流分析和類型檢查算法。·Commons包(ASM2.0增加)提供了幾個通用的字節碼轉換和簡化字節碼生成的適配器。·Util包包含幾個助手類和簡單的字節碼較驗器來方便開發和測試。·XML包提供了與XML文件相互轉換的字節碼結構適配器,及兼容SAX而且允許使用XSLT來定義字節碼轉換方式的適配器。后面幾節會給出ASM框架中Core包的介紹。為了更好地理解這個包的組織結構,你最好有一些在JVM規范中定義的字節碼結構的基礎了解。下面是較高級別的類文件格式圖([*]標識重復的結構) [1]-------------------------------------------+ | Header and Constant Stack | +--------------------------------------------+ | [*] Class Attributes | [2]------------+------------------------------+ | [*] Fields | Field Name, Descriptor, etc | | +------------------------------+ | | [*] Field Attributes | [3]------------+------------------------------+ | [*] Methods | Method Name, Descriptor, etc | | +------------------------------| | | Method max stack and locals | | |------------------------------| | | [*] Method Code table | | |------------------------------| | | [*] Method Exception table | | |------------------------------| | | [*] Method Code Attributes | | +------------------------------| | | [*] Method Attributes | +-------------+------------------------------+需要注意的一些地方:·所有使用在類結構中的描述符,字符串和其他常量都存儲在類文件開始的常量堆棧中,來自其他結構的引用是基于堆棧的序號。·每一個類必須包含頭部(包括類名,父類,接口等)和常量堆棧。而其他元素如字段列表/方法列表/屬性列表都是可選的。·每一個方法段包含相同的頭信息和最大最小局部變量數的信息,這些是用來校驗字節碼的。對非抽象和非原生方法,還包含一個方法指令表,一個異常表及代碼屬性。此外,還可能有其他的方法屬性。·類的每一個屬性,成員/方法/方法代碼都有自己的名字,具體細節可參考JVM規范的類文件格式部分。這些屬性代表字節碼的各種信息,如源文件名/內部類/標識(用來存儲泛型)/行號/局部變量表和注解。JVM規范也允許定義自定義的屬性來包含更多的信息但標準實現的VM不會識別。注:Java5注解實際上已經廢棄了那些自定義屬性,因為注解在主義上允許你表達更多的東西。·方法代碼表包含JVM的指令列表。一些指令(就像異常/行號/局部變量表)使用代碼表中的偏移值并且所有這些偏移的值可能需要在指令從方法代碼表中增刪時相應調整。如你所見,字節碼轉換并不容易。但是,ASM框架減少了潛在的結構復雜性并且提供簡化的API允許所有字節碼信息的訪問和復雜的轉換?;谑录淖止澊a處理Core包使用推方案(類似訪問者模式,在SAX API就使用了這種模式處理XML)來遍歷復雜的字節碼結構。ASM定義了幾個接口,如ClassVisitor,FieldVisitor,MethodVisitor和AnnotationVisitor。AnnotationVisitor是一個特殊的接口允許你表達層次的注解結構。下面的幾幅圖顯示這些接口是如何相互交互及配合使用實現字節碼轉換和從字節碼獲取信息。Core包邏輯上可憐分為兩大部分:1、 字節碼生產者,如ClassReader或者按正確順序調用了上面的訪問者類的方法的自定義類。2、 字節碼消費者,如輸出器(ClassWriter, FieldWriter, MethodWriter, and AnnotationWriter),適配器(ClassAdapter and MethodAdapter)或者其他實現了訪問者接口的類。圖2給出了通用生產者/消費者交互過程的時序圖。Figure 2. Sequence diagram for producer-consumer interaction在這個交互過程中,客戶端應用首先創建了ClassReader并調用accept()方法(以ClassVisitor實例作為參數)。然后ClassReader解析類并對每一個字節碼斷發送“visit事務給ClassVisitor。對循環的上下文,如成員/方法/注解,ClassVisitor可以創建繼續撲克相應接口(FieldVisitor, MethodVisitor, or AnnotationVisitor)的子訪問者并返回給生產者。如果生產者接收到一個空值,他簡單地忽略類的那部分(如在由訪問者驅動的“延遲加載特性時就不需要解析相應的字節碼部分);否則相應的子上下文事件就傳遞給子訪問者實例。當子上下文結束時,生產者調用visitEnd()方法然后移到下一部分。字節碼消費者可以通過手工傳遞事件給下一個鏈中的訪問者或者使用來自傳遞所有訪問方法給內部的訪問者的ClassAdapter/ MethodAdapter的訪問者通過“響應鏈模式連接起來。這些代理者一方面字節碼的消費者方面另一方面也作為字節碼的生產者。他們在實現特定的字節碼轉換時可以修改原始的代理方式:1、 訪問調用代理可以在刪除類成員/方法/方法指令時被忽略。2、 訪問調用參數可以在重命名類/方法/類型時被修改。3、 新訪問調用可以在引入新成員/方法/注入新代碼到現存代碼時被增加。ClassWriter訪問者可以終結整個處理鏈,他也是最終字節碼的生成者。例如: ClassWriter cw = new ClassWriter(computeMax); ClassVisitor cc = new CheckClassAdapter(cw); ClassVisitor tv = new TraceClassVisitor(cc, new PrintWriter(System.out)); ClassVisitor cv = new TransformingClassAdapter(tv); ClassReader cr = new ClassReader(bytecode); cr.accept(cv, skipDebug); byte[] newBytecode = cw.toByteArray();在上面的代碼中,實現了自定義的類轉換并且將結果人作為參數傳給TraceClassVisitor的構造函數。TraceClassVisitor打印轉換的類并傳遞給CheckClassAdapter(這是用來作簡單的字節校驗后傳遞給ClassWriter)。大部分的訪問方法接收簡單的參數如int,boolean和String。在所有的方法中String參數是字節碼中常量的引用,ASM使用與JVM一致的方式。例如,所有類名都應該定義在內部格式中。成員和方法描述符應該跟JVM表示一致。泛型信息的表示也類似。這種方式避免了在沒有轉換時不必要的計算。為了便于構建和解析這樣的描述,系統提供了包含一些靜態方法的Type類:·String getMethodDescriptor(Type returnType, Type[] argumentTypes)·String getInternalName(Class c)·String getDescriptor(Class c)·String getMethodDescriptor(Method m)·Type getType(String typeDescriptor)·Type getType(Class c)·Type getReturnType(String methodDescriptor)·Type getReturnType(Method m)·Type[] getArgumentTypes(String methodDescriptor)·Type[] getArgumentTypes(Method m)注意這些描述符使用了“簡單表示,這意味著不包含泛型信息。泛型信息實際上作為一個單獨的字節屬性存儲,但ASM專門對待這個屬性并且在相應訪問方法中傳遞泛型標識串作為參數。這個標識串的值也是參照JVM規范,與Java代碼中的泛型定義唯一映射,并且為工具增加獲取額外細節的機會。ASM提供了與其他訪問者類似的SignatureVisitor, SignatureReader, and SignatureWriter類,如圖3所示。 Figure 3. Sequence diagram for Signature classesUtil包中包含了TraceSignatureVisitor,已經實現了SignatureVisitor而且可以將一個標識值轉換成Java的泛型定義。下面的例子將一個方法標識轉換為Java方法定義。 TraceSignatureVisitor v = new TraceSignatureVisitor(access); SignatureReader r = new SignatureReader(sign); r.accept(v); String genericDecl = v.getDeclaration(); String genericReturn = v.getReturnType(); String genericExceptions = v.getExceptions(); String methodDecl = genericReturn + ' ' + methodName + genericDecl; if(genericExceptions!=null) { methodDecl += ' throws ' + genericExceptions; }到目前為止,我們已經討論了ASM框架的基本設計方式及類結構處理。但最有趣的部分是ASM如何處理方法代碼。訪問方法代碼在ASM中,方法定義是由ClassVisitor.visitMethod()來表示,剩下的方法字節碼則由MethodVisitor中的許多訪問方法來表示。這些方法按照下面的順序來調用,“*表示重復的方法而“?表示方法只能被調用一次。此外,visit...Insn 和visitLabel方法必須按照訪問代碼的字節碼指令順序調用,而visitTryCatchBlock, visitLocalVariable和visitLineNumber方法必須在標簽作為參數傳遞被訪問后才能調用。注意visitEnd方法必須在方法處理完成后被調用。雖然ClassReader已經做了這一步,但在使用自定義字節碼生產者時要注意一點。還要注意如果一個方法包含字節碼(也就是說方法是非抽象或非源生的),那么visitCode必須在第一個visit...Insn調用前被調用,而visitMaxs必須在最后一個visit...Insn調用后被調用。每一個visitIincInsn, visitLdcInsn, visitMultiANewArrayInsn, visitLookupSwitchInsn, and visitTableSwitchInsn方法唯一對應一個字節碼指令。剩下的visit...Insn方法對應多個字節碼指令,他們的操作碼作為第一個方法參數被傳入。所有這些操作碼常量被定義在Opcodes接口中。這種方式對字節碼的解析和格式化非常有效率。不幸的是,這給開發人員生成非法代碼的可能,因為ClassWriter不會校驗這些限制。但是,還是有一個CheckClassAdapter可以被用來在開發期間測試生成的代碼。另一個機會是對所有字節碼生成或轉換可以修改方法代碼的偏移并且在方法代碼中增刪額外的指令時應該自動調整。這對所有的跳轉偽指令的參數都兼容的,就如try-catch塊,行號和局部變量定義及一些特殊屬性一樣。但是,ASM為開發人員隱藏了這些復雜性。為了定義方法字節碼中的位置且不需要使用絕對偏移值,需要傳遞一個唯一的標簽類的實例給visitLabel方法。其他MethodVisitor方法如visitJumpInsn, visitLookupSwitchInsn, visitTableSwitchInsn, visitTryCatchBlock, visitLocalVariable, and visitLineNumber可以使用這些標簽實例在visitLabel調用之前,就像實例后在方法后被調用。上面的內容看起來很復雜,好像需要很深奧的字節碼指令知識。但是在編譯的類上使用ASMifierClassVisitor就可以讓你知道如何用ASM生成給定的字節碼。此外,在兩個編譯的類上(一個原始的和另一個應用特定的轉換)使用然后進行比較就可以給出什么樣的ASM調用應該被使用在轉換器上。這個過程在幾篇文章中已經詳細解釋了(可以參看最后的資源部分)。目前已經有了Eclipse使用的插件了,如圖4,提供了從Java源生成ASM代碼及比較ASMifier輸出的良好支持,還包含了上下文字節碼的參考。 Figure 4. Eclipse ASM plugin (Click on the picture to see a full-size image)用ASM的訪問者來跟蹤類的依賴已經有一些文章介紹了如何用ASM生成字節碼。現在,我們來看一下如何用ASM分析已有的類。我們來做一個有趣的應用來獲取給定的.jar文件中使用的外部類和包。簡單起見,這個例子僅獲取外部的依賴而不會取依賴的類型(如父類/方法參數/局部變量類型等)。僅為分析,我們不會創建那些注解/成員/方法的子訪問者實例。所有使用的訪問者(包括類和標識訪問者)都在一個類中實現:public class DependencyVisitor implements AnnotationVisitor, SignatureVisitor, ClassVisitor, FieldVisitor, MethodVisitor {...在這個例子中,我們會跟蹤包之間的依賴,因此私有類必須包含包名: private String getGroupKey(String name) { int n = name.lastIndexOf('/'); if(n>-1) name = name.substring(0, n); packages.add(name); return name; }為了獲取依賴關系,訪問者接口如ClassVisitor, AnnotationVisitor, FieldVisitor, and MethodVisitor應該選擇性地集成方法的參數。幾個常見的樣例如下: private void addName(String name) { if(name==null) return; String p = getGroupKey(name); if(current.containsKey(p)) { current.put(p, current.get(p)+1); } else { current.put(p, 1); } }在這個例子中,current是依賴的當前包。另一個例子是類型描述符(注解/枚舉/成員類型/newarray指令的參數等);如Ljava/lang/String;, J, and [[[I。這些可以用Type.getType( desc)來獲取內部格式的類名: private void addDesc(String desc) { addType(Type.getType(desc)); } private void addType(Type t) { switch(t.getSort()) { case Type.ARRAY: addType(t.getElementType()); break; case Type.OBJECT: addName(t.getClassName().replace('.','/')); break; } }在方法定義中的方法描述法及激活指令中的描述參數類型及返回類型??梢酝ㄟ^Type.getReturnType(methodDescriptor) 和Type.getArgumentTypes(methodDescriptor)來解析并取得參數和返回類型。 private void addMethodDesc(String desc) { addType(Type.getReturnType(desc)); Type[] types = Type.getArgumentTypes(desc); for(int i = 0; i < types.length; i++) { addType(types[ i]); } }而使用在許多“訪問方法中的用來定義Java5泛型信息的標識參數是個特例。如果存在,這個參數重寫描述符參數并包含編碼后的泛型信息??梢员挥肧ignatureReader來解析這個值。所以我們可以實現SignatureVisitor來被每一個標識工件來調用。 private void addSignature(String sign) { if(sign!=null) { new SignatureReader(sign).accept(this); } } private void addTypeSignature(String sign) { if(sign!=null) { new SignatureReader(sign).acceptType(this); } }實現ClassVisitor接口的方法,如such as visit(), visitField(), visitMethod(), and visitAnnotation()就可以獲取在父類/接口/成員類型/方法參數/返回值/異常上的依賴信息,就如注解一樣。例如: public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { String p = getGroupKey(name); current = groups.get(p); if(current==null) { current = new HashMap(); groups.put(p, current); } if(signature==null) { addName(superName); addNames(interfaces); } else { addSignature(signature); } } public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { if(signature==null) { addDesc(desc); } else { addTypeSignature(signature); } if(value instanceof Type) { addType((Type) value); } return this; } public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { if(signature==null) { addMethodDesc(desc); } else { addSignature(signature); } addNames(exceptions); return this; } public AnnotationVisitor visitAnnotation( String desc, boolean visible) { addDesc(desc); return this; }實現MethodVisitor接口的方法就可以獲取關于參數注解類型和使用在可以使用對象引用的字節碼指令上的依賴: public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { addDesc(desc); return this; } /** * Visits a type instruction * NEW, ANEWARRAY, CHECKCAST or INSTANCEOF. */ public void visitTypeInsn(int opcode, String desc) { if(desc.charAt(0)=='[') { addDesc(desc); } else { addName(desc); } } /** * Visits a field instruction * GETSTATIC, PUTSTATIC, GETFIELD or PUTFIELD. */ public void visitFieldInsn(int opcode, String owner, String name, String desc) { addName(owner); addDesc(desc); } /** * Visits a method instruction INVOKEVIRTUAL, * INVOKESPECIAL, INVOKESTATIC or * INVOKEINTERFACE. */ public void visitMethodInsn(int opcode, String owner, String name, String desc) { addName(owner); addMethodDesc(desc); } /** * Visits a LDC instruction. */ public void visitLdcInsn(Object cst) { if(cst instanceof Type) { addType((Type) cst); } } /** * Visits a MULTIANEWARRAY instruction. */ public void visitMultiANewArrayInsn( String desc, int dims) { addDesc(desc); } /** * Visits a try catch block. */ public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { addName(type); }現在我們可以用DependencyVisitor來獲取整個.jar文件的依賴關系了。例如: DependencyVisitor v = new DependencyVisitor(); ZipFile f = new ZipFile(jarName); Enumeration<? extends ZipEntry> en = f.entries(); while(en.hasMoreElements()) { ZipEntry e = en.nextElement(); String name = e.getName(); if(name.endsWith('.class')) { ClassReader cr = new ClassReader(f.getInputStream(e)); cr.accept(v, false); } }可以用很多不同的方式來表示得到的信息。一種方式是構建依賴樹并計算相關數據或者創建可視化的東西。例如,圖5顯示了ant1.6.5 jar包中的依賴關系的可視化表現,這是我使用一些簡單的Java2D代碼寫的。下面的圖在水平軸上顯示包,在垂直軸上顯示依賴。陰影部分表示包被多次引用。 Figure 5. Dependencies in ant.1.6.5.jar, as discovered with ASM這個工具的全部代碼會被包含在下一個ASM發布中。你可以從ASM CVS獲取。ASM1.x后的改變如果你沒有使用ASM1.x可以略過這個段。2.0中主要的結構變化是所有J2SE5.0的特性都被內建到訪問者/過濾器的事件流中。因此新的API允許你用更輕便和自然的方式來處理泛型和注解。不需要顯式創建注解屬性實例,因為在事件流中已經包含了泛型和注解數據。例如,在1.x,ClassVisitor接口如下使用: CodeVisitor visitMethod(int access, String name, String desc, String[] exceptions, Attribute attrs);This has been split into several methods in ASM 2.0:在2.0中已經分為多個方法: MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) AnnotationVisitor visitAnnotation(String desc, boolean visible) void visitAttribute(Attribute attr)在1.x API中,為了定義泛型信息,你必須創建SignatureAttribute的實例,而定義注解你需要RuntimeInvisibleAnnotations, RuntimeInvisibleParameterAnnotations, RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations, and AnnotationDefault的實例。然后你可以將這些實例放在相應的訪問方法的attrs參數中。在2.0,增加了新的標識參數來表示泛型信息。新的AnnotationVisitor接口被用來處理所有的注解。不再需要創建attrs集合了,而且注解數據是強類型的。然而在移植現有代碼時,特別是在“適配器類被使用時,必須注意確保所有來自適配器的方法需要重寫來適應新的標識,因為編譯器不用對這種情況給出警告。ASM2.0還有些其他的改變。1、增加了新的接口FieldVisitor 和AnnotationVisitor2、CodeVisitor合并到MethodVisitor中了。3、在MethodVisitor中增加了visitCode()方法簡化檢測首個指令。4、Constants接口重構為Opcodes。5、所有來自attrs包的屬性被包含到ASM的事件模型中。6、TreeClassAdapter and TreeCodeAdapter被包含到ClassNode and MethodNode中。7、增加LabelNode類使指令集合的元素成為AbstractInsnNode的通用類型。通常,建議使用如JDiff這樣的工具來比較兩個版本之間的區別。小結ASM2.0為開發人員屏蔽了字節碼的復雜性,因而使開發人員更有效在字節碼級別上使用Java的特性。這個框架不僅允許你轉換和生成字節碼,而且可以從現有的類中取得具體的信息。他的API繼續改善,現在已經包含了J2SE5.0中的泛型和注解。接下來,還會增加Mustang(J2SE6)中的新特性。資源·Java Virtual Machine Specification Java虛擬機規范•·'“修訂的類文件格式(JVM規范的第4章)。包含J2SE5.0中支持的JSR-14/JSR-175/JSR-201中要求的修改及其他小的更正和調整。·“使用ASM工具集來處理字節碼·“使用ASM工具集來創建和讀寫J2SE5.0注解·字節碼指令(BCI)。Eugene Kuleshov是一個獨立咨詢師,擁有超過15年的軟件設計開發經驗。 Java, java, J2SE, j2se, J2EE, j2ee, J2ME, j2me, ejb, ejb3, JBOSS, jboss, spring, hibernate, jdo, struts, webwork, ajax, AJAX, mysql, MySQL, Oracle, Weblogic, Websphere, scjp, scjd 摘要:Java的特性如動態類加載和反射使其成為動態語言。然而在許多時候,反射是不夠的,而且開發人員需要從非Java源程序中生成字節碼,如腳本語言Groovy和BeanShell,或者源數據如ORM配置。當使用已經存在的類時,特別是當沒有源程序時,就需要使用一些工具來做如分析類或方法的依賴性以便生成測試度量,?
上一條:通過元數據驗證對象下一條:在NetBean5.0中加載Struts1.1
相關文章:
排行榜
