動態編譯 java ASM入門

概述

ASM 是java字節碼操作框架。
由於ASM性能好的原因,所以在動態編譯上往往比Javassist上使用的更加廣泛。

之前已經寫過了Javassist實現動態編譯的demo,對動態編譯不瞭解的讀者可以看下:動態編譯入門(gradle Transform Demo)

本文在前面demo的基礎上,將Javassist的實現改爲了ASM。
因此對於gradle 插件等重複的點就不多加描述了,本文主要講解下ASM的使用。

Demo 概述


本demo通過ASM,實現在方法中動態插入代碼的功能。
主要代碼如下:

public class PluginTestClass {

  public void init() {
    System.out.println("PluginTestClass init");
    //此處將會使用動態編譯插入代碼
    //PluginTestClass.testPrint();
  }

  public static void testPrint(String s) {
    System.out.println(s);
  }
}
public class MainActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    PluginTestClass pluginTestClass=new PluginTestClass();
    pluginTestClass.init();
  }
}

Demo結果

2020-04-05 10:26:53.708 11336-11336/com.example.transformtest I/System.out: PluginTestClass init
2020-04-05 10:26:53.708 11336-11336/com.example.transformtest I/System.out: 我是插入的代碼

“代碼插入”實現流程

1、自己實現了TestFileUtils.java這個類來遞歸遍歷文件夾下的所有文件。
2、遍歷所有文件找到PluginTestClass這個類。
3、使用ClassVisitor 和MethodVisitor 分別來找到init方法和修改init方法。

MethodVisitor中的visitMaxs方法用於返回最大的操作數棧和局部變量表,這兩項往往會根據插入的代碼而改變。(此demo中插入的代碼不會導致這兩者改變,因此此demo中沒有修改)
筆者自己單獨寫了一篇文章用於講解着兩個概念,對其有興趣的讀者可以看下:
完全理解 java操作數棧和局部變量表

TestTransform.java

public class TestTransform extends Transform {

  //用於指明本Transform的名字,也是代表該Transform的task的名字
  @Override public String getName() {
    return "TestTransform";
  }

  //用於指明Transform的輸入類型,可以作爲輸入過濾的手段。
  @Override public Set<QualifiedContent.ContentType> getInputTypes() {
    return TransformManager.CONTENT_CLASS;
  }

  //用於指明Transform的作用域
  @Override public Set<? super QualifiedContent.Scope> getScopes() {
    return TransformManager.SCOPE_FULL_PROJECT;
  }

  //是否增量編譯
  @Override public boolean isIncremental() {
    return false;
  }

  @Override public void transform(TransformInvocation invocation) {
    System.out.println("TestTransform transform");
    for (TransformInput input : invocation.getInputs()) {
      //遍歷jar文件 對jar不操作,但是要輸出到out路徑
      input.getJarInputs().parallelStream().forEach(jarInput -> {
        File src = jarInput.getFile();
        System.out.println("input.getJarInputs fielName:" + src.getName());
        File dst = invocation.getOutputProvider().getContentLocation(
            jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(),
            Format.JAR);
        try {
          FileUtils.copyFile(src, dst);
        } catch (IOException e) {
          throw new RuntimeException(e);
        }
      });
      //遍歷文件,在遍歷過程中
      input.getDirectoryInputs().parallelStream().forEach(directoryInput -> {
        File src = directoryInput.getFile();
        System.out.println("input.getDirectoryInputs fielName:" + src.getName());
        File dst = invocation.getOutputProvider().getContentLocation(
            directoryInput.getName(), directoryInput.getContentTypes(),
            directoryInput.getScopes(), Format.DIRECTORY);
        try {
          scanFilesAndInsertCode(src);
          FileUtils.copyDirectory(src, dst);
        } catch (Exception e) {
          System.out.println(e.getMessage());
        }
      });
    }
  }

  private void scanFilesAndInsertCode(File file) throws Exception {
    TestFileUtils.scanFileInDir(file,
        new TestFileUtils.ScanFileCallback() {
          @Override public void action(File file) {
            if (file.getAbsolutePath().contains("PluginTestClass")) {
              insertTestCode(file);
            }
          }
        });
  }

  private void insertTestCode(File file) {
    try {
      //讀取class文件並且插入代碼
      InputStream inputStream = new FileInputStream(file);
      ClassReader cr = new ClassReader(inputStream);
      ClassWriter cw = new ClassWriter(cr, 0);
      ClassVisitor cv = new MyClassVisitor(Opcodes.ASM5, cw);
      cr.accept(cv, ClassReader.EXPAND_FRAMES);

      //生成新的class文件
      FileOutputStream fileOutputStream = new FileOutputStream(file);
      fileOutputStream.write(cw.toByteArray());
      fileOutputStream.close();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  private static class MyClassVisitor extends ClassVisitor {

    MyClassVisitor(int api, ClassVisitor cv) {
      super(api, cv);
    }

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

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
        String signature, String[] exceptions) {
      MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
      //generate code into this method
      if (name.equals("init")) {
        mv = new MyMethodVisitor(Opcodes.ASM5, mv);
      }
      return mv;
    }
  }

  private static class MyMethodVisitor extends MethodVisitor {

    MyMethodVisitor(int api, MethodVisitor mv) {
      super(api, mv);
    }

    @Override
    public void visitInsn(int opcode) {
      if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
        //添加方法
        mv.visitLdcInsn("我是插入的代碼");
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/example/testplugin/PluginTestClass",
            "testPrint",
            "(Ljava/lang/String;)V", false);
      }
      super.visitInsn(opcode);
    }

    @Override public void visitMaxs(int maxStack, int maxLocals) {
      super.visitMaxs(maxStack, maxLocals);
    }
  }
}

TestFileUtils.java

public class TestFileUtils {
  public static void scanFileInDir(File rootFile, ScanFileCallback callback) {
    if (rootFile == null) {
      return;
    }
    if (rootFile.isDirectory()) {
      if (rootFile.listFiles() == null) {
        return;
      }
      for (File file : rootFile.listFiles()) {
        scanFileInDir(file, callback);
      }
    } else {
      callback.action(rootFile);
    }
  }

  public interface ScanFileCallback {
    void action(File file);
  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章