天天看點

轉 : java asm庫的原理與使用方法(一)

原部落格位址:

https://blog.csdn.net/xysmiracle/article/details/38293795

在之前android的性能監測sdk項目中用到過asm庫,在這裡記錄一下基本原理和用法;

ASM庫/工具  http://asm.ow2.org/

ASM是一款基于java位元組碼層面的代碼分析和修改工具;無需提供源代碼即可對應用嵌入所需debug代碼,用于應用API性能分析,代碼優化和代碼混淆等工作。ASM的目标是生成,轉換和分析已編譯的java class檔案,可使用ASM工具讀/寫/轉換JVM指令集。

ASM工具提供兩種方式來産生和轉換已編譯的class檔案,它們分别是基于事件和基于對象的表示模型。其中,基于事件的表示模型使用一個有序的事件序清單示一個class檔案,class檔案中的每一個元素使用一個事件來表示,比如class的頭部,變量,方法聲明,JVM指令都有相對應的事件表示,ASM使用自帶的事件解析器能将每一個class檔案解析成一個事件序列。而基于對象的表示模型則使用對象樹結構來解析每一個class檔案。

轉 : java asm庫的原理與使用方法(一)

基于事件模型的ASM工具使用生産者-消費者模型轉換/産生一個class檔案。其轉換過程中涉及到自定義的事件生産者,自定義的事件過濾器和自定義的事件消費者這三種元件。其中使用classReader來解析每一個class檔案中的事件元素,使用自定義的各種基于方法/變量/聲明/類注釋的元素擴充卡來過濾和修改class事件元序列中的相應事件對象,最後使用ClassWriter來重新将更新後的class事件序列轉換成class位元組碼供JVM加載執行。整個生産/轉換class檔案的過程如下圖所示,起點和終點分别是CLASSREADER(CLASS檔案解析器)和ClassWriter(class事件序列轉換到class位元組碼),中間的過程由若幹個自定義的事件過濾器組成。

1.class檔案的結構

class檔案保持固定的結構資訊,而且保留了幾乎所有的源代碼檔案中的符号。一個class檔案整體結構由幾個區域組成,一個區域用來描述類的modifier,name,父類,接口和注釋。一個區域用來描述類中變量的modfier,名字,類型和注釋。一個區域用來描述類中方法和構造函數的modifier,名字參數類型,傳回類型,注釋等資訊,當然也包含已編譯成java位元組碼指令序列的方法具體内容。還有一個作為class檔案的靜态池區域,用來儲存所有的數字,字元串,類型的常量,這些常量隻被定義過一次且被其他class中區域所引用。class檔案與源代碼檔案的關系:一個java檔案最後會被編譯成N(1 <= N)個class檔案。

下圖展示了一個class檔案的總體概貌

轉 : java asm庫的原理與使用方法(一)

圖 2.1 class檔案的概覽(*表示0個或者多個)

2. class檔案的内部命名

原java類型與class檔案内部類型對應關系 

轉 : java asm庫的原理與使用方法(一)

圖2.2 java類型的描述 

原java方法聲明與class檔案内部方法聲明的對應關系

轉 : java asm庫的原理與使用方法(一)

圖2.3方法描述舉例

3.ASM工具的接口群組件

ASM工具生産和轉換class檔案内容的所有工作都是基于ClassVisitor這個抽象類進行的。ClassVisitor抽象類中的每一個方法會對應到class檔案的相應區域,每個方法負責處理class檔案相應區域的位元組碼内容。下圖展示了ClassVisitor抽象類的成員函數。

  1. public abstract class ClassVisitor {

  2. public ClassVisitor(int api);

  3. public ClassVisitor(int api, ClassVisitor cv);

  4. public void visit(int version, int access, String name, String signature, String superName, String[] interfaces);

  5. public void visitSource(String source, String debug);

  6. public void visitOuterClass(String owner, String name, String desc);
  7. AnnotationVisitor visitAnnotation(String desc, boolean visible);
  8. public void visitAttribute(Attribute attr);

  9. public void visitInnerClass(String name, String outerName, String innerName, int access);

  10. public FieldVisitor visitField(int access, String name, String desc,
  11. String signature, Object value);

  12. public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions);
  13. void visitEnd();
  14. }

某些區域方法直接完成該區域位元組碼内容的處理并傳回空,某些複雜class區域的方法需傳回更加細節的XXXVisitor對象,此XXXVisitor對象将負責處理此class區域的更加細節的位元組碼内容,開發者可以編寫繼承自XXXVisitor抽象類的自定義類,在成員函數中實作對細節位元組碼操作的邏輯代碼。比如,visitField方法用來負責class檔案中變量區域的位元組碼内容修改,該區域又可細分出多種屬性資料對象(注釋,參數值),這裡需要編寫繼承自fieldVisitor抽象類的自定義類完成這些細分資料對象的位元組碼内容操作。下圖為FieldVisitor的抽象類内容。

  1. public abstract class FieldVisitor {

  2. public FieldVisitor(int api);

  3. public FieldVisitor(int api, FieldVisitor fv);

  4. public AnnotationVisitor visitAnnotation(String desc, boolean visible);
  5. public void visitAttribute(Attribute attr);

  6. public void visitEnd();
  7. }

另外,在處理整個class檔案處理過程中,classVistor抽象類中的方法通路需滿足下面的次序。

visitvisitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*

(visitInnerClass | visitField | visitMethod )*
visitEnd

基于 ClassVistor api 的通路方式, ASM 工具提供了三種核心元件用來實作 class 的産生和轉換工作。 ClassReader 負責解析 class 檔案位元組碼數組,然後将相應區域的内容對象傳遞給 classVistor 執行個體中相應的 visitXXX 方法, ClassReader 可以看作是一個事件生産者。 ClassWriter 繼承自 ClassVistor 抽象類,負責将對象化的 class 檔案内容重構成一個二進制格式的 class 位元組碼檔案, ClassWriter 可以看作是一個事件消費者。繼承自 ClassVistor 抽象類的自定義類負責 class 檔案各個區域内容的修改和生成,它可以看作是一個事件過濾器,一次生産消費過程中這樣的事件過濾器可以有 N 個( 0<=N )。

1、周遊CLASS位元組碼類資訊

面的例子用來列印class位元組碼内容的類資訊,這裡以java.lang.runnable為例。

test.java:

  1. public class ClassPrinter extends ClassVisitor {
  2. public ClassPrinter() {
  3. super(ASM4);
  4. }
  5. public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
  6. System.out.println(name + " extends " + superName + " {");
  7. }
  8. public void visitSource(String source, String debug) {}
  9. public void visitOuterClass(String owner, String name, String desc) {}
  10. public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
  11. return null;
  12. }
  13. public void visitAttribute(Attribute attr) {}
  14. public void visitInnerClass(String name, String outerName, String innerName, int access) {}
  15. public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
  16. System.out.println( " " + desc + " " + name);
  17. return null;
  18. }
  19. public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
  20. System.out.println( " " + name + desc);
  21. return null;
  22. }
  23. public void visitEnd() {
  24. System.out.println( "}");
  25. }
  26. }
  27. //ClassReader作為位元組碼生産者,ClassPrinter作為位元組碼消費者
  28. ClassPrinter cp = new ClassPrinter();
  29. ClassReader cr = new ClassReader( "java.lang.Runnable");
  30. cr.accept(cp, );

輸出:

  1. java/lang/Runnable extends java/lang/Object {
  2. run()V
  3. }

2 、生産自定義類對應的 class 位元組碼内容

目标生産出以下自定義接口:

  1. package pkg;
  2. public interface Comparable extends Mesurable {
  3. int LESS = - ;
  4. int EQUAL = ;
  5. int GREATER = ;
  6. int compareTo(Object o);
  7. }

test.java内容:

  1. ClassWriter cw = new ClassWriter( );
  2. cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, "pkg/Comparable", null, "java/lang/Object", new String[] { "pkg/Mesurable" });
  3. cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I", null, new Integer(- )).visitEnd();
  4. cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I", null, new Integer( )).visitEnd();
  5. cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I", null, new Integer( )).visitEnd(); cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo", "(Ljava/lang/Object;)I", null, null).visitEnd(); cw.visitEnd();
  6. byte[] b = cw.toByteArray();

3、動态加載2生産出的class位元組碼并執行個體化該類

使用繼承自ClassLoader的類,并重寫defineClass方法;

test.java:

  1. //第一種方法:通過ClassLoader的defineClass動态加載位元組碼
  2. class MyClassLoader extends ClassLoader {
  3. public Class defineClass(String name, byte[] b) {
  4. return defineClass(name, b, , b.length);
  5. }
  6. }
  7. //直接調用方法
  8. Class c = myClassLoader.defineClass( "pkg.Comparable", b);

使用繼承自ClassLoader的類,并重寫findClass内部類;

test.java:

  1. class StubClassLoader extends ClassLoader {
  2. @Override
  3. protected Class findClass(String name) throws ClassNotFoundException {
  4. if (name.endsWith( "_Stub")) {
  5. ClassWriter cw = new ClassWriter( );
  6. ...
  7. byte[] b = cw.toByteArray();
  8. return defineClass(name, b, , b.length);
  9. }
  10. return super.findClass(name);
  11. }
  12. }

4、轉換class位元組碼内容中類的成員(變量,方法,注釋等)

4.1、ClassReader生産者生産的class位元組碼bytes可以被ClassWriter直接消費,比如:

  1. byte[] b1 = ...;
  2. ClassWriter cw = new ClassWriter( );
  3. ClassReader cr = new ClassReader(b1);
  4. cr.accept(cw, );
  5. byte[] b2 = cw.toByteArray(); //這裡的b2與b1表示同一個類且值一樣

4.2、ClassReader生産者生産的class位元組碼bytes可以先被繼承自ClassVisitor的自定義類過濾,最後被ClassWriter消費,比如:

  1. byte[] b1 = ...;
  2. ClassWriter cw = new ClassWriter( );
  3. // cv forwards all events to cw
  4. ClassVisitor cv = new ChangeVersionAdapter (cw) { };
  5. ClassReader cr = new ClassReader(b1);
  6. cr.accept(cv, );
  7. byte[] b2 = cw.toByteArray(); //這裡的b2與b1表示同一個類但值不一樣

具體架構圖如下如所示,

轉 : java asm庫的原理與使用方法(一)

圖2.6類位元組碼轉換鍊(bytes->reader->adapter->writer->bytes)

這裡的ChangeVersionAdapter繼承自ClassVisitor,它沒做其他過濾,僅僅重寫了visit方法,過濾出類中的方法并指定方法的版本号(v1.5)。

  1. public class ChangeVersionAdapter extends ClassVisitor {
  2. public ChangeVersionAdapter(ClassVisitor cv) {
  3. super(ASM4, cv);
  4. }
  5. @Override
  6. public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
  7. cv.visit(V1_5, access, name, signature, superName, interfaces);
  8. }
  9. }

整個位元組碼轉換過程的時序圖如下所示。

轉 : java asm庫的原理與使用方法(一)

圖2.7類位元組碼使用ChangeVersionAdapter過濾轉換過程的時序圖

通過重寫ClassVisitor的visit方法并修改cv.visit方法中的其他參數,你還能做其他有趣的事情。比如,你能給這個class增加一個接口,改變這個class的名字等。

4.3、如何使用轉換過的class類位元組碼

前面的小節提到,轉換後的classb2能夠儲存到磁盤中或者通過ClassLoader動态加載。通過這種動态加載的方式隻能在本地使用這個ClassLoader加載的單個類,無法滿足同時加載多個類的需求。如果你想要轉換所有運作時的類位元組碼并能夠加載使用這些轉換後的類位元組碼檔案(被systemClassLoader加載),你需要使用java.lang.instrument包中的ClassFileTransformer類。實作繼承自ClassFileTransformer的自定義類并重寫transform方法,在該方法中實作對所有系統class檔案的周遊(這裡的周遊指對各個class檔案在位元組碼層面的轉換)。另外這裡的ClassFileTransformer必須在main方法之前完成class檔案的轉換,且必須與main方法在同一個JVM中運作,這樣才能被system classLoader加載。

那麼怎麼實作位元組碼轉換發生在main方法執行之前?這裡涉及到java agent的概念,java代理(agent) 作為main方法前的一個攔截器 (interceptor),也就是在main方法執行之前,執行agent的代碼。agent的代碼與你的main方法在同一個JVM中運作,并被同一個system classloader裝載,被同一的安全政策 (security policy) 和上下文 (context) 所管理。

怎樣寫一個java agent? 隻需要實作premain這個方法

public static void premain(String agentArgs,Instrumentation inst)

下面的例子展示了如何實作對所有運作時class位元組碼進行轉換并加載的方法。

  1. public static void premain(String agentArgs, Instrumentation inst) { //該方法在main方法前運作
  2. inst.addTransformer( new ClassFileTransformer() {
  3. public byte[] transform(ClassLoader l, String name, Class c, ProtectionDomain d, byte[] b) throws IllegalClassFormatException {
  4. ClassReader cr = new ClassReader(b);
  5. ClassWriter cw = new ClassWriter(cr, );
  6. ClassVisitor cv = new ChangeVersionAdapter(cw);
  7. cr.accept(cv, );
  8. return cw.toByteArray();
  9. }
  10. });
  11. }

5、删除class中的方法或者變量的做法

通過在visitMethod或者visitField方法中傳回null的方式就能實作删除class中的方法或者變量的需求。如下執行個體,通過傳入名字和描述删除class中的相應方法。

  1. public class RemoveMethodAdapter extends ClassVisitor {
  2. private String mName;
  3. private String mDesc;
  4. public RemoveMethodAdapter(ClassVisitor cv, String mName, String mDesc) {
  5. super(ASM4, cv);
  6. this.mName = mName;
  7. this.mDesc = mDesc;
  8. }
  9. @Override
  10. public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
  11. if (name.equals(mName) && desc.equals(mDesc)) {
  12. // do not delegate to next visitor -> this removes the method
  13. return null;
  14. }
  15. return cv.visitMethod(access, name, desc, signature, exceptions);
  16. }
  17. }

6、增加class中的方法或者變量的做法

在visitEnd中增加想要增加的方法或者變量,執行個體如下。

  1. public class AddFieldAdapter extends ClassVisitor {
  2. private int fAcc;
  3. private String fName;
  4. private String fDesc;
  5. private boolean isFieldPresent;
  6. public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName, String fDesc) {
  7. super(ASM4, cv);
  8. this.fAcc = fAcc;
  9. this.fName = fName;
  10. this.fDesc = fDesc;
  11. }
  12. @Override
  13. public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
  14. if (name.equals(fName)) {
  15. isFieldPresent = true;
  16. }
  17. return cv.visitField(access, name, desc, signature, value);
  18. }
  19. @Override
  20. public void visitEnd() {
  21. if (!isFieldPresent) {
  22. FieldVisitor fv = cv.visitField(fAcc, fName, fDesc, null, null);
  23. if (fv != null) {
  24. fv.visitEnd();
  25. }
  26. }
  27. cv.visitEnd();
  28. }
  29. }

7、classVisitor過濾(轉換)鍊

可在一次 生産—過濾--消費 過程中,使用多個繼承自classVisitor的自定義過濾類對class進行過濾(轉換)。

轉 : java asm庫的原理與使用方法(一)

圖2.8過濾鍊

下面的執行個體中通過傳入多個自定義的過濾器,實作過濾鍊的功能。

  1. public class MultiClassAdapter extends ClassVisitor { protected ClassVisitor[] cvs;
  2. public MultiClassAdapter(ClassVisitor[] cvs) {
  3. super(ASM4);
  4. this.cvs = cvs;
  5. }
  6. @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
  7. for (ClassVisitor cv : cvs) {
  8. cv.visit(version, access, name, signature, superName, interfaces);
  9. }
  10. }
  11. ...
  12. }

8、最後通過一個例子來梳理一下asm工具的使用方法

例子中用到了agentmain,先補充一下相關知識。

agentmain方式

premain時JavaSE5開始就提供的代理方式,給了開發者諸多驚喜,不過也有些須不變,由于其必須在指令行指定代理jar,并且代理類必須在main方法前啟動。是以,要求開發者在應用前就必須确認代理的處理邏輯和參數内容等等,在有些場合下,這是比較苦難的。比如正常的生産環境下,一般不會開啟代理功能,但是在發生問題時,我們不希望停止應用就能夠動态的去修改一些類的行為,以幫助排查問題,這在應用啟動前是無法确定的。 為解決運作時啟動代理類的問題,JavaSE6開始,提供了在應用程式的VM啟動後在動态添加代理的方式,即agentmain方式。 與permain類似,agent方式同樣需要提供一個agentjar,并且這個jar需要滿足:

1.    在manifest中指定Agent-Class屬性,值為代理類全路徑

2.    代理類需要提供public static voidagentmain(String args, Instrumentation inst)或public static void agentmain(Stringargs)方法。并且再二者同時存在時以前者優先。args和inst和premain中的一緻。

不過如此設計的再運作時進行代理有個問題——如何在應用程式啟動之後再開啟代理程式呢?JDK6中提供了JavaTools API,其中AttachAPI可以滿足這個需求。

Attach API中的VirtualMachine代表一個運作中的VM。其提供了loadAgent()方法,可以在運作時動态加載一個代理jar。具體需要參考《Attach API》(http://docs.oracle.com/javase/6/docs/jdk/api/attach/spec/com/sun/tools/attach/VirtualMachine.html

轉自:http://blog.sina.com.cn/s/blog_605f5b4f01010i3b.html

完整的例子如下,在java.lang.ProcessBuilder執行個體調用start方法時eclipse的console中列印出"java.lang.ProcessBuilder執行個體的start方法被觸發"。

代理類RewriterAgent.java:

  1. public class RewriterAgent {
  2. public static void agentmain(String agentArgs, Instrumentation instrumentation){ //代理類入口函數
  3. premain(agentArgs, instrumentation);
  4. }
  5. public static void premain(String agentArgs, Instrumentation instrumentation){ //代理類入口函數Java SE6
  6. try{
  7. DexClassTransformer dexClassTransformer = new DexClassTransformer(); //繼承自ClassFileTransformer的自定義類,多class檔案的加載
  8. instrumentation.addTransformer(dexClassTransformer, true);
  9. }
  10. catch(Throwable ex){
  11. System.out.println( "Agent startup error");
  12. throw new RuntimeException(ex);
  13. }
  14. }
  15. private static final class DexClassTransformer implements ClassFileTransformer{
  16. private Log log;
  17. private final Map classVisitors;
  18. public byte[] transform(ClassLoader classLoader, String className, Class clazz, ProtectionDomain protectionDomain, byte bytes[]) throws IllegalClassFormatException{
  19. ClassVisitorFactory factory = (ClassVisitorFactory)classVisitors.get(className); //根據給定className比對相應class的工廠對象
  20. if(factory != null){
  21. if(clazz != null && !factory.isRetransformOkay()){
  22. return null;
  23. }
  24. try{
  25. ClassReader cr = new ClassReader(bytes); //生産者
  26. ClassWriter cw = new PatchedClassWriter( , classLoader); //消費者 自定義的ClassWriter
  27. ClassAdapter adapter = factory.create(cw); //過濾器
  28. cr.accept(adapter, );
  29. return cw.toByteArray();
  30. }
  31. catch(SkipException ex) { }
  32. catch(Exception ex){
  33. throw ex;
  34. }
  35. }
  36. return null;
  37. }
  38. public DexClassTransformer() throws URISyntaxException{
  39. classVisitors = new HashMap<String, ClassVisitorFactory>(){ //将需要轉換的類放到map中,Map中為不同的類指定相應的工廠對象
  40. private static final long serialVersionUID = ;
  41. {
  42. put( "java/lang/ProcessBuilder", new ClassVisitorFactory( true){ //實作ClassVisitorFactory并重寫create方法
  43. public ClassAdapter create(ClassVisitor cv){
  44. return RewriterAgent.createProcessBuilderClassAdapter(cv);
  45. }
  46. });
  47. }
  48. };
  49. }
  50. }
  51. private static ClassAdapter createProcessBuilderClassAdapter(ClassVisitor cw, Log log){
  52. return new ClassAdapter(cw){
  53. public MethodVisitor visitMethod(int access, String name, String desc, String signature, String exceptions[]){
  54. MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
  55. if( "start".equals(name)){
  56. System.out.println( "java.lang.ProcessBuilder執行個體的start方法被觸發");
  57. }
  58. return mv;
  59. }
  60. };
  61. }
  62. }

自定義的ClassWriter:

  1. class PatchedClassWriter extends ClassWriter{
  2. private final ClassLoader classLoader;
  3. public PatchedClassWriter(int flags, ClassLoader classLoader){
  4. super(flags);
  5. this.classLoader = classLoader;
  6. }
  7. protected String getCommonSuperClass(String type1, String type2){ //傳回共同的父類
  8. Class c;
  9. Class d;
  10. try{
  11. c = Class.forName(type1.replace( '/', '.'), true, classLoader);
  12. d = Class.forName(type2.replace( '/', '.'), true, classLoader);
  13. }
  14. catch(Exception e){
  15. throw new RuntimeException(e.toString());
  16. }
  17. if(c.isAssignableFrom(d))
  18. return type1;
  19. if(d.isAssignableFrom(c))
  20. return type2;
  21. if(c.isInterface() || d.isInterface())
  22. return "java/lang/Object";
  23. do
  24. c = c.getSuperclass();
  25. while(!c.isAssignableFrom(d));
  26. return c.getName().replace( '.', '/');
  27. }
  28. }

測試類AttachTest:

  1. public class AttachTest {
  2. public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
  3. VirtualMachine vm = VirtualMachine.attach(args[ ]); //args[0]傳入的是jvm的pid号
  4. vm.loadAgent( "F:\\workspace\\rewriterAgent.jar"); //rewriterAgent.jar是RewriterAgent.java導出的Jar包名
  5. }