1. 程式人生 > >java位元組碼增強技術實現過程

java位元組碼增強技術實現過程

什麼是Instrumentation?

查閱java api可知,

軟體包 java.lang.instrument 的描述 

提供允許 Java 程式語言代理監測執行在 JVM 上的程式的服務。監測的機制是對方法的位元組碼的修改。

包規範

在啟動 JVM 時,通過指示代理類及其代理選項 啟動一個代理程式。

該代理類必須實現公共的靜態premain 方法,該方法原理上類似於 main 應用程式入口點:

public static void premain(String agentArgs, Instrumentation inst);

JVM 被初始化之後,每個premain 方法將按照指定代理的順序被呼叫。然後,呼叫實際的應用程式 main

方法。每個 premain 方法必須按順序返回,以便啟動序列能夠繼續。代理類將由載入包含應用程式main 方法的類的同一類載入器載入。premain 方法將在與應用程式 main 方法相同的安全性和類載入器規則下執行。不存在代理premain 方法可以執行的建模限制。應用程式 main 可以執行的任何事情(包括生成執行緒)從 premain 來看都是合法的。

每個代理程式通過agentArgs 引數傳遞其代理選項。代理選項作為單個字串傳遞,任何其他分析應由代理程式本身執行。

如果該代理程式不能被解析(例如,由於無法載入代理類,或由於代理類沒有一致的premain 方法),則 JVM 將中止。如果 premain

方法丟擲未捕獲的異常,則 JVM 將中止。

啟動命令列介面

在帶有命令列介面的 JVM 上,通過向 JVM 命令列新增此開關來指定代理程式:
-javaagent:jarpath[=options]
jarpath 是指向代理程式 JAR 檔案的路徑。options 是代理選項。此開關可以在同一命令列上多次使用,從而建立多個代理程式。多個代理程式可以使用同一jarpath。代理 JAR 檔案必須符合 JAR 檔案規範。下面的清單屬性是針對代理 JAR 檔案定義的:
Premain-Class
代理類。即包含 premain 方法的類。此屬性是必需的,如果它不存在,JVM 將中止。注:這是類名,而不是檔名或路徑。
Boot-Class-Path
由引導類載入器搜尋的路徑列表。路徑表示目錄或庫(在許多平臺上通常作為 jar 或 zip 庫被引用)。查詢類的特定於平臺的機制出現故障之後,引導類載入器會搜尋這些路徑。按列出的順序搜尋路徑。列表中的路徑由一個或多個空格分開。路徑使用分層 URI 的路徑元件的語法。如果該路徑以斜槓字元(“/”)開頭,則為絕對路徑,否則為相對路徑。相對路徑根據代理 JAR 檔案的絕對路徑解析。忽略格式不正確的路徑和不存在的路徑。此屬性是可選的。
Can-Redefine-Classes
布林值(truefalse,與大小寫無關)。能夠重定義此代理所需的類。值如果不是 true,則被認為是 false。此屬性是可選的,預設值為false
代理 JAR 檔案附加到類路徑之後。 

Java Instrumentation指的是可以用獨立於應用程式之外的代理(agent)程式來監測和協助執行在JVM上的應用程式。這種監測和協助包括但不限於獲取JVM執行時狀態,替換和修改類定義等。 java SE5中使用JVM TI替代了JVM PI和JVM DI。提供一套代理機制,支援獨立於JVM應用程式之外的程式以代理的方式連線和訪問JVM。Java.lang.instrument是在JVM TI的基礎上提供的Java版本的實現。 Instrumentation提供的主要功能是修改jvm中類的行為。 Java SE6中由兩種應用Instrumentation的方式,premain(命令列)和agentmain(執行時)

premain

在Java SE5時代,Instrument只提供了premain一種方式,即在真正的應用程式(包含main方法的程式)main方法啟動前啟動一個代理程式。例如使用如下命令:

java -javaagent:agent_jar_path[=options] java_app_name

可以在啟動名為java_app_name的應用之前啟動一個agent_jar_path指定位置的agent jar。 實現這樣一個agent jar包,必須滿足兩個條件:

  1. 在這個jar包的manifest檔案中包含Premain-Class屬性,並且改屬性的值為代理類全路徑名。
  2. 代理類必須提供一個public static void premain(String args, Instrumentation inst)或 public static void premain(String args) 方法。

當在命令列啟動該代理jar時,VM會根據manifest中指定的代理類,使用於main類相同的系統類載入器(即ClassLoader.getSystemClassLoader()獲得的載入器)載入代理類。在執行main方法前執行premain()方法。如果premain(String args, Instrumentation inst)和premain(String args)同時存在時,優先使用前者。其中方法引數args即命令中的options,型別為String(注意不是String[]),因此如果需要多個引數,需要在方法中自己處理(比如用";"分割多個引數之類);inst是執行時由VM自動傳入的Instrumentation例項,可以用於獲取VM資訊。

premain例項-列印所有的方法呼叫

下面實現一個列印程式執行過程中所有方法呼叫的功能,這個功能可以通過AOP其他方式實現,這裡只是嘗試使用Instrumentation進行ClassFile的位元組碼轉換實現:

構造agent類

premain方式的agent類必須提供premain方法,程式碼如下:

package test;

import java.lang.instrument.Instrumentation;

public class Agent {

    public static void premain(String args, Instrumentation inst){
        System.out.println("Hi, I'm agent!");
        inst.addTransformer(new TestTransformer());
    }
}

premain有兩個引數,args為自定義傳入的代理類引數,inst為VM自動傳入的Instrumentation例項。 premain方法的內容很簡單,除了標準輸出外,只有

inst.addTransformer(new TestTransformer());

這行程式碼的意思是向inst中新增一個類的轉換器。用於轉換類的行為。

構造Transformer

下面來實現上述過程中的TestTransformer來完成列印呼叫方法的類定義轉換。

package test;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;

public class TestTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader arg0, String arg1, Class<?> arg2,
            ProtectionDomain arg3, byte[] arg4)
            throws IllegalClassFormatException {
        ClassReader cr = new ClassReader(arg4);
        ClassNode cn = new ClassNode();
        cr.accept(cn, 0);
        for (Object obj : cn.methods) {
            MethodNode md = (MethodNode) obj;
            if ("<init>".endsWith(md.name) || "<clinit>".equals(md.name)) {
                continue;
            }
            InsnList insns = md.instructions;
            InsnList il = new InsnList();
            il.add(new FieldInsnNode(Opcodes.GETSTATIC, "java/lang/System",
                    "out", "Ljava/io/PrintStream;"));
            il.add(new LdcInsnNode("Enter method-> " + cn.name+"."+md.name));
            il.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL,
                    "java/io/PrintStream", "println", "(Ljava/lang/String;)V"));
            insns.insert(il);
            md.maxStack += 3;

        }
        ClassWriter cw = new ClassWriter(0);
        cn.accept(cw);
        return cw.toByteArray();
    }

}

TestTransformer實現了ClassFileTransformer介面,該介面只有一個transform方法,引數傳入包括該類的類載入器,類名,原位元組碼位元組流等,返回被轉換後的位元組碼位元組流。 TestTransformer主要使用ASM實現在所有的類定義的方法中,在方法開始出添加了一段列印該類名和方法名的位元組碼。在轉換完成後返回新的位元組碼位元組流。詳細的ASM使用請參考ASM手冊。

設定MANIFEST.MF

設定MANIFEST.MF檔案中的屬性,檔案內容如下:

Manifest-Version: 1.0
Premain-Class: test.Agent
Created-By: 1.6.0_29

測試

程式碼編寫完成後將程式碼編譯打成agent.jar。 編寫測試程式碼:

public class TestAgent {

    public static void main(String[] args) {
        TestAgent ta = new TestAgent();
        ta.test();
    }

    public void test() {
        System.out.println("I'm TestAgent");
    }

}

從命令列執行該類,並設定agent.jar

java -javaagent:agent.jar TestAgent

將打印出程式執行過程中實際執行過的所有方法名:

Hi, I'm agent!
Enter method-> test/TestAgent.main
Enter method-> test/TestAgent.test
I'm TestAgent
Enter method-> java/util/IdentityHashMap$KeySet.iterator
Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.hasNext
Enter method-> java/util/IdentityHashMap$KeyIterator.next
Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.nextIndex
Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.hasNext
Enter method-> java/util/IdentityHashMap$KeySet.iterator
Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.hasNext
Enter method-> java/util/IdentityHashMap$KeyIterator.next
Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.nextIndex
Enter method-> com/apple/java/Usage$3.run
。。。

從輸出中可以看出,程式首先執行的是代理類中的premain方法(不過代理類自身不會被自己轉換,所以不能打印出代理類的方法名),然後是應用程式中的main方法。