java字節碼忍者禁術

Java語言自己是由Java語言規格說明(JLS)所定義的,而Java虛擬機的可執行字節碼則是由一個徹底獨立的標準,即Java虛擬機規格說明(一般也被稱爲VMSpec)所定義的。java

JVM字節碼是經過javac對Java源代碼文件進行編譯後生成的,生成的字節碼與本來的Java語言存在着很大的不一樣。比方說,在Java語言中爲人熟知的一些高級特性,在編譯過程當中會被移除,在字節碼中徹底不見蹤跡。程序員

這方面最明顯的一個例子莫過於Java中的各類循環關鍵字了(for、while等等),這些關鍵字在編譯過程當中會被消除,並替換爲字節碼中的分支指令。這就意味着在字節碼中,每一個方法內部的流程控制只包含if語句與jump指令(用於循環)。數組

在閱讀本文前,我假設讀者對於字節碼已經有了基本的瞭解。若是你須要瞭解一些基本的背景知識,請參考《Java程序員修煉之道》(Well-Grounded Java Developer)一書(做者爲Evans與Verburg,由Manning於 2012年出版),或是來自於RebelLabs的這篇報告(下載PDF須要註冊)。數據結構

讓咱們來看一下這個示例,它對於還不熟悉的JVM字節碼的新手來講極可能會感到困惑。該示例使用了javap工具,它本質上是一個Java字節碼的反彙編工具,在下載的JDK或JRE中能夠找到它。在這個示例中,咱們將討論一個簡單的類,它實現了Callable接口:jvm

public class ExampleCallable implements Callable {
    public Double call() {
        return 3.1415;
    }}

咱們能夠經過對javap工具進行最簡單形式的使用,對這個類進行反彙編後獲得如下結果:ide

$ javap kathik/java/bytecode_examples/ExampleCallable.classCompiled from "ExampleCallable.java"public class kathik.java.bytecode_examples.ExampleCallable 
       implements java.util.concurrent.Callable {
  public kathik.java.bytecode_examples.ExampleCallable();
  public java.lang.Double call();
  public java.lang.Object call() throws java.lang.Exception;}

這個反彙編後的結果看上去彷佛是錯誤的,畢竟咱們只寫一個call方法,而不是兩個。並且即便咱們嘗試手工建立這兩個方法,javac也會提示,代碼中有兩個具備相同名稱和參數的方法,它們僅有返回類型的不一樣,所以這段代碼是沒法編譯的。然而,這個類確確實實是由上面那個真實的、有效的Java源文件所生成的。函數

這個示例可以清晰地代表在使用Java中廣爲人知的一種限制:不可對返回類型進行重載,其實這只是Java語言的一種限制,而不是JVM字符碼自己的強制要求。javac確實會在代碼中插入一些不存在於原始的類文件中的內容,若是你爲此感到擔心,那大可放心,由於這種事每時每刻都在發生!每一位Java程序員最早學到的一個知識點就是:「若是你不提供一個構造函數,那麼編譯器會爲你自動添加一個簡單的構造函數」。在javap的輸出中,你也能看到其中有一個構造函數存在,而它並不存在於咱們的代碼中。工具

這些額外的方法從某種程度上代表,語言規格說明的需求比VM規格說明中的細節更爲嚴格。若是咱們可以直接編寫字節碼,就能夠實現許多「不可能」實現的功能,而這種字節碼雖然是合法的,卻沒有任何一個Java編譯器可以生成它們。學習

舉例來講,咱們能夠建立出徹底不含構造函數的類。Java語言規格說明中要求每一個類至少要包含一個構造函數,而若是咱們在代碼中沒有加入構造函數,javac會自動加入一個簡單的void構造函數。可是,若是咱們可以直接編寫字節碼,咱們徹底能夠忽略構造函數。這種類是沒法實例化的,即便經過反射也不行。測試

咱們的最後一個例子已經接近成功了,但仍是差一口氣。在字節碼中,咱們能夠編寫一個方法,它將試圖調用一個其它類中定義的私有方法。這段字節碼是有效的,但若是任何程序打算加載它,它將沒法正確地進行連接。這是由於在類型加載器中(classloader)的校驗器會檢測出這個方法調用的訪問控制限制,而且拒絕這個非法訪問。

介紹ASM

若是咱們打算在建立的代碼中實現這些超越Java語言的行爲,那就須要徹底手動建立這樣的一個類文件。因爲這個類文件的格式是兩進制的,所以能夠選擇使用某種類庫,它可以讓咱們對某個抽象的數據結構進行操做,隨後將其轉換爲字節碼,並經過流方式將其寫入磁盤。

具有這種功能的類庫有多個選擇,但在本文中咱們將關注於ASM。這是一個很是常見的類庫,在Java 8分發包中有一個之內部API的形式提供的版本(其內容稍有不一樣)。對於用戶代碼來講,咱們選擇使用通用的開源類庫,而不是JDK中提供的版本,畢竟咱們不該當依賴於內部API來實現所需的功能。

ASM的核心功能在於,它提供了一種API,雖然它看上去有些神祕莫測(有時也會顯得有些粗糙),但可以以一種直接的方式反映出字節碼的數據結構。

咱們看到的Java運行時是由多年以前的各類設計決策所產生的結果,而在後續各個版本的類文件格式中,咱們可以清晰地看到各類新增的內容。

ASM致力於儘可能使構建的類文件接近於真實形態,所以它的基礎API會分解爲一系列相對簡單的方法片斷(而這些片斷正是用於建模的二進制所關注的)。

若是程序員打算徹底手動編寫類文件,就必需理解類文件的總體結構,而這種結構是會隨時改變的。幸運的是,ASM可以處理多個不一樣Java版本中的類文件格式之間的細微差異,而Java平臺自己對於可兼容性的高要求也側面幫助了咱們。

一個類文件依次包含如下內容:

  • 某個特殊的數字(在傳統的Unix平臺上,Java中的特殊數字是這個歷史悠久的、人見人愛的0xCAFEBABE)

  • 正在使用中的類文件格式版本號

  • 常量

  • 訪問控制標記(例如類的訪問範圍是public、protected仍是package等等)

  • 該類的類型名稱

  • 該類的超類

  • 該類所實現的接口

  • 該類擁有的字段(處於超類中的字段上方)

  • 該類擁有的方法(處於超類中的方法上方)

  • 屬性(類級別的註解)

能夠用下面這個方法幫助你記憶JVM類文件中的主要部分:

ASM中提供了兩個API,其中最簡單的那個依賴於訪問者模式。在常見的形式中,ASM只包含最簡單的字段以及ClassWrite類(當已經熟悉了ASM的使用和直接操做字節碼的方式以後,許多開發者會發現CheckClassAdapter是一個很實用的起點,做爲一個ClassVisitor,它對代碼進行檢查的方式,與Java的類加載子系統中的校驗器的工做方式很是想像。)

能夠查看ASM OSChina 網頁

讓咱們看幾個簡單的類生成的例子,它們都是按照常規的模式建立的:

  • 啓動一個ClassVisitor(在咱們的示例中就是一個ClassWriter)

  • 寫入頭信息

  • 生成必要的方法和構造函數

  • 將ClassVisitor轉換爲字節數組,並寫入輸出

示例

public class Simple implements ClassGenerator {
 // Helpful constants private static final String GEN_CLASS_NAME = "GetterSetter";
 private static final String GEN_CLASS_STR = PKG_STR + GEN_CLASS_NAME;

 @Override public byte[] generateClass() {
   ClassWriter cw = new ClassWriter(0);
   CheckClassAdapter cv = new CheckClassAdapter(cw);
   // Visit the class header   cv.visit(V1_7, ACC_PUBLIC, GEN_CLASS_STR, null, J_L_O, new String[0]);
   generateGetterSetter(cv);
   generateCtor(cv);
   cv.visitEnd();
   return cw.toByteArray();
 }

 private void generateGetterSetter(ClassVisitor cv) {
   // Create the private field myInt of type int. Effectively:   // private int myInt;   cv.visitField(ACC_PRIVATE, "myInt", "I", null, 1).visitEnd();

   // Create a public getter method   // public int getMyInt();   MethodVisitor getterVisitor = 
      cv.visitMethod(ACC_PUBLIC, "getMyInt", "()I", null, null);
   // Get ready to start writing out the bytecode for the method   getterVisitor.visitCode();
   // Write ALOAD_0 bytecode (push the this reference onto stack)   getterVisitor.visitVarInsn(ALOAD, 0);
   // Write the GETFIELD instruction, which uses the instance on   // the stack (& consumes it) and puts the current value of the   // field onto the top of the stack   getterVisitor.visitFieldInsn(GETFIELD, GEN_CLASS_STR, "myInt", "I");
   // Write IRETURN instruction - this returns an int to caller.   // To be valid bytecode, stack must have only one thing on it   // (which must be an int) when the method returns   getterVisitor.visitInsn(IRETURN);
   // Indicate the maximum stack depth and local variables this   // method requires   getterVisitor.visitMaxs(1, 1);
   // Mark that we've reached the end of writing out the method   getterVisitor.visitEnd();

   // Create a setter   // public void setMyInt(int i);   MethodVisitor setterVisitor = 
       cv.visitMethod(ACC_PUBLIC, "setMyInt", "(I)V", null, null);
   setterVisitor.visitCode();
   // Load this onto the stack   setterVisitor.visitVarInsn(ALOAD, 0);
   // Load the method parameter (which is an int) onto the stack   setterVisitor.visitVarInsn(ILOAD, 1);
   // Write the PUTFIELD instruction, which takes the top two 
   // entries on the execution stack (the object instance and   // the int that was passed as a parameter) and set the field 
   // myInt to be the value of the int on top of the stack. 
   // Consumes the top two entries from the stack   setterVisitor.visitFieldInsn(PUTFIELD, GEN_CLASS_STR, "myInt", "I");
   setterVisitor.visitInsn(RETURN);
   setterVisitor.visitMaxs(2, 2);
   setterVisitor.visitEnd();
 }

 private void generateCtor(ClassVisitor cv) {
   // Constructor bodies are methods with special name 
   MethodVisitor mv = 
       cv.visitMethod(ACC_PUBLIC, INST_CTOR, VOID_SIG, null, null);
   mv.visitCode();
   mv.visitVarInsn(ALOAD, 0);
   // Invoke the superclass constructor (we are basically 
   // mimicing the behaviour of the default constructor 
   // inserted by javac)   // Invoking the superclass constructor consumes the entry on the top   // of the stack.   mv.visitMethodInsn(INVOKESPECIAL, J_L_O, INST_CTOR, VOID_SIG);
   // The void return instruction   mv.visitInsn(RETURN);
   mv.visitMaxs(2, 2);
   mv.visitEnd();
 }

 @Override public String getGenClassName() {
   return GEN_CLASS_NAME;
 }}

這段代碼使用了一個簡單的接口,用一個單一的方法生成類的字節,一個輔助方法以返回生成的類名,以及一些實用的常量:

interface ClassGenerator {public byte[] generateClass();public String getGenClassName();// Helpful constantspublic static final String PKG_STR = "kathik/java/bytecode_examples/";public static final String INST_CTOR = "";public static final String CL_INST_CTOR = "";public static final String J_L_O = "java/lang/Object";public static final String VOID_SIG = "()V";}

爲了駕馭生成的類,咱們須要使用一個harness類,它叫作Main。Main類提供了一個簡單的類加載器,而且提供了一種反射式的方式對生成類中的方法進行回調。爲了簡便起見,咱們將生成的類定入Maven的目標文件夾的正確位置,讓IDE中的classpath可以順利地找到它:

public class Main {public static void main(String[] args) {
   Main m = new Main();
   ClassGenerator cg = new Simple();
   byte[] b = cg.generateClass();
   try {
     Files.write(Paths.get("target/classes/" + PKG_STR +
       cg.getGenClassName() + ".class"), b, StandardOpenOption.CREATE);
   } catch (IOException ex) {
     Logger.getLogger(Simple.class.getName()).log(Level.SEVERE, null, ex);
   }
   m.callReflexive(cg.getGenClassName(), "getMyInt");}

下面的類提供了一種方法,可以對受保護的defineClass()進行訪問,這樣一來咱們就可以將一個字節數組轉換爲某個類對象,以便在反射中使用。

private static class SimpleClassLoader extends ClassLoader {
 public Class simpleDefineClass(byte[] clazzBytes) {
   return defineClass(null, clazzBytes, 0, clazzBytes.length);
 }}private void callReflexive(String typeName, String methodName) {
 byte[] buffy = null;
 try {
   buffy = Files.readAllBytes(Paths.get("target/classes/" + PKG_STR +
     typeName + ".class"));
   if (buffy != null) {
     SimpleClassLoader myCl = new SimpleClassLoader();
     Class newClz = myCl.simpleDefineClass(buffy);
     Object o = newClz.newInstance();
     Method m = newClz.getMethod(methodName, new Class[0]);
     if (o != null && m != null) {
       Object res = m.invoke(o, new Object[0]);
       System.out.println("Result: " + res);
     }
   }
 } catch (IOException | InstantiationException | IllegalAccessException | 
         NoSuchMethodException | SecurityException | 
         IllegalArgumentException | InvocationTargetException ex) {
   Logger.getLogger(Simple.class.getName()).log(Level.SEVERE, null, ex);
 }}

有了這個類之後,咱們只要經過細微的改動,就能夠方便地測試各類不一樣的類生成器,以此對字節碼生成器的各個方面進行探索。

實現無構造函數的類的方式也很類似。舉例來講,如下這種方式能夠在生成的類中僅包含一個靜態字段,以及它的getter和setter(生成器不會調用generateCtor()方法):

private void generateStaticGetterSetter(ClassVisitor cv) {// Generate the static field  cv.visitField(ACC_PRIVATE | ACC_STATIC, "myStaticInt", "I", null,
     1).visitEnd();

  MethodVisitor getterVisitor = cv.visitMethod(ACC_PUBLIC | ACC_STATIC, 
                                         "getMyInt", "()I", null, null);
  getterVisitor.visitCode();
  getterVisitor.visitFieldInsn(GETSTATIC, GEN_CLASS_STR, "myStaticInt", "I");

  getterVisitor.visitInsn(IRETURN);
  getterVisitor.visitMaxs(1, 1);
  getterVisitor.visitEnd();

  MethodVisitor setterVisitor = cv.visitMethod(ACC_PUBLIC | ACC_STATIC, "setMyInt", 
                                         "(I)V", null, null);
  setterVisitor.visitCode();
  setterVisitor.visitVarInsn(ILOAD, 0);
  setterVisitor.visitFieldInsn(PUTSTATIC, GEN_CLASS_STR, "myStaticInt", "I");}setterVisitor.visitInsn(RETURN);setterVisitor.visitMaxs(2,2);setterVisitor.visitEnd();

請留意一下該方法在生成時使用了ACC_STATIC標記,此外還請注意方法的參數是位於本地變量列表中的最前面的(這裏使用的ILOAD 0 模式暗示了這一點 —— 而在生成實例方法時,此處應該改成ILOAD 1,這是由於實例方法中的「this」引用存儲在本地變量表中的偏移量爲0)。

經過使用javap,咱們就可以確認在生成的類中確實不包括任何構造函數:

$ javap -c kathik/java/bytecode_examples/StaticOnly.class public class kathik.StaticOnly {public static int getMyInt(); Code:0: getstatic    #11                // Field myStaticInt:I3: ireturnpublic static void setMyInt(int); Code:0: iload_01: putstatic    #11                // Field myStaticInt:I4: return}

使用生成的類

目前爲止,咱們是使用反射的方式調用咱們經過ASM所生成的類的。這有助於保持這個示例的自包含性,但在不少狀況下,咱們但願可以將這些代碼生成在常規的Java文件中。要實現這一點很是簡單。如下示例將生成的類保存在Maven的目標目錄下,寫法很簡單:

$ cd target/classes
$ jar cvf gen-asm.jar kathik/java/bytecode_examples/GetterSetter.class kathik/java/bytecode_examples/StaticOnly.class$ mv gen-asm.jar ../../lib/gen-asm.jar

這樣一來咱們就獲得了一個JAR文件,能夠做爲依賴項在其它代碼中使用。比方說,咱們能夠這樣使用這個GetterSetter類:

import kathik.java.bytecode_examples.GetterSetter;public class UseGenCodeExamples {
 public static void main(String[] args) {
   UseGenCodeExamples ugcx = new UseGenCodeExamples();
   ugcx.run();
 }

 private void run() {
   GetterSetter gs = new GetterSetter();
   gs.setMyInt(42);
   System.out.println(gs.getMyInt());
 }}

這段代碼在IDE中是沒法經過編譯的(由於GetterSetter類沒有配置在classpath中)。但若是咱們直接使用命令行,而且在classpath中指向正確的依賴,就能夠正確地運行了:

$ cd ../../src/main/java/$ javac -cp ../../../lib/gen-asm.jar kathik/java/bytecode_examples/withgen/UseGenCodeExamples.java
$ java -cp .:../../../lib/gen-asm.jar kathik.java.bytecode_examples.withgen.UseGenCodeExamples42

結論

在本文中,咱們經過使用ASM類庫中所提供的簡單API,學習了徹底手動生成類文件的基礎知識。咱們也爲讀者展現了Java語言和字節碼有哪些不一樣的要求,而且瞭解到Java中的某些規則其實只是語言自己的規範,而不是運行時所強制的要求。咱們還看到,一個正確編寫的手工類文件能夠直接在語言中使用,與經過javac生成的文件沒有區別。這一點也是Java與其它非Java語言,例如Groovy或Scala進行互操做的基礎。

這方面的應用還有許多高級技巧,經過本文的學習,讀者應該已經掌握了基本的知識,而且可以進一步深刻研究JVM的運行時,以及如何對它進行各類操做的技術。

相關文章
相關標籤/搜索