說明:本博文是博主學習 Instrumentation 歷程的總結,整合了學習過程當中參考的關於Instrumentation 的教程,並加入博主本身的看法和實例。html
參考連接:java
JDK源碼-java.lang.instrument-第一部分-源碼學習數組
利用 Java 代碼,即 java.lang.instrument 作動態 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能從本地代碼中解放出來,使之能夠用 Java 代碼的方式解決問題。使用 Instrumentation,開發者能夠構建一個獨立於應用程序的代理程序(Agent),用來監測和協助運行在 JVM 上的程序,甚至可以替換和修改某些類的定義。有了這樣的功能,開發者就能夠實現更爲靈活的運行時虛擬機監控和 Java 類操做了,這樣的特性實際上提供了一種虛擬機級別支持的 AOP 實現方式,使得開發者無需對 JDK 作任何升級和改動,就能夠實現某些 AOP 的功能了。markdown
在 Java SE 6 裏面,instrumentation 包被賦予了更強大的功能:啓動後的 instrument、本地代碼(native code)instrument,以及動態改變 classpath 等等。這些改變,意味着 Java 具備了更強的動態控制、解釋能力,它使得 Java 語言變得更加靈活多變。app
「java.lang.instrument」包的具體實現,依賴於 JVMTI。JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虛擬機提供的,爲 JVM 相關的工具提供的本地編程接口集合。JVMTI 是從 Java SE 5 開始引入,整合和取代了之前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已經消失了。JVMTI 提供了一套」代理」程序機制,能夠支持第三方工具程序以代理的方式鏈接和訪問 JVM,並利用 JVMTI 提供的豐富的編程接口,完成不少跟 JVM 相關的功能eclipse
學習 Java Instrumentation,首先應該瞭解 JVMTI 的基本知識,此處推薦 IBM 的一篇文章 JVMTI 和 Agent 實現 ,文章寫得至關貼切。編程語言
代理監控JVM運行的JAVA程序,對字節碼修改ide
//轉換類文件的代理接口 public interface ClassFileTransformer { //protectionDomain - 要定義或重定義的類的保護域 //classfileBuffer - 類文件格式的輸入字節緩衝區(不得修改) byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException; }
###Instrumentation(接口)
函數
Java SE 6新特性, 獲取 Instrumentation 接口的實例有兩種方式: 1. 當 JVM 以指示一個代理類的方式啓動時,將傳遞給代理類的 premain 方法一個 Instrumentation 實例。 2. 當 JVM 提供某種機制在 JVM 啓動以後某一時刻啓動代理時,將傳遞給代理代碼的 agentmain 方法一個 Instrumentation 實例
源碼解釋:
//提供檢測 Java 編程語言代碼所需的服務。檢測是向方法中添加字節碼 //以蒐集各類工具所使用的數據 public interface Instrumentation { /** * 註冊提供的轉換器。 */ void addTransformer(ClassFileTransformer transformer, boolean canRetransform); /** * 註冊提供的轉換器。 */ void addTransformer(ClassFileTransformer transformer); /** * 註銷提供的轉換器 */ boolean removeTransformer(ClassFileTransformer transformer); /** * 返回當前 JVM 配置是否支持類的重轉換。 */ boolean isRetransformClassesSupported(); /** * 重轉換提供的類集 */ void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; /** * 返回當前 JVM 配置是否支持類的重定義 */ boolean isRedefineClassesSupported(); /** * 使用提供的類文件重定義提供的類集 */ void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException; /** * 肯定一個類是否能夠被 retransformation 或 redefinition 修改。 */ boolean isModifiableClass(Class<?> theClass); /** * 返回 JVM 當前加載的全部類的數組 */ Class[] getAllLoadedClasses(); /** * 返回全部初始化加載器是 loader 的類的數組。 */ Class[] getInitiatedClasses(ClassLoader loader); /** * 返回指定對象使用的特定於實現的近似存儲量。 */ long getObjectSize(Object objectToSize); /** * 指定 JAR 文件,檢測類由引導類加載器定義 */ void appendToBootstrapClassLoaderSearch(JarFile jarfile); /** * 指定 JAR 文件,檢測類由系統類加載器定義。 */ void appendToSystemClassLoaderSearch(JarFile jarfile); /** * 返回當前 JVM 配置是否支持設置本機方法前綴。 */ boolean isNativeMethodPrefixSupported(); /** * 經過容許重試,將前綴應用到名稱,此方法修改本機方法解析的失敗處理 */ void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix); }
ClassFileTransformer.transform 的實現拋出該異常。拋出此異常的緣由或者因爲初始類文件字節無效,或者因爲之前應用的轉換損壞了字節
在沒法修改指定類之一時,由 Instrumentation.redefineClasses 的實現拋出此異常。
public final class ClassDefinition { /** * 自身class */ private final Class mClass; /** * 本地class文件 */ private final byte[] mClassFile; /** * 使用提供的類和類文件字節建立一個新的 ClassDefinition 綁定 */ public ClassDefinition( Class<?> theClass, byte[] theClassFile) { if (theClass == null || theClassFile == null) { throw new NullPointerException(); } mClass = theClass; mClassFile = theClassFile; } /** * 返回該類。 */ public Class<?> getDefinitionClass() { return mClass; } /** * 返回包含新的類文件的 byte 數組。 */ public byte[] getDefinitionClassFile() { return mClassFile; }
}
功能:經過代理,在main函數運行前或後動態的改變類的定義和其餘處理操做。
具體包括:premain ,agentmain,動態改變 classpath,本地方法prefix等等,下面進行詳細介紹:
開發者可讓 Instrumentation 代理在 main 函數運行前執行:
package wqz.zoom.test; public class TransClass { public void sayHello() { System.out.println("Hello!!!"); }
在此類作加載時,進行處理。
package wqz.zoom.test; import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.NotFoundException; public class Transformer implements ClassFileTransformer{ @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { // TODO Auto-generated method stub System.out.println("transform()"); if(className.equals("wqz/zoom/test/TransClass")) { ClassPool classPool = ClassPool.getDefault(); try { CtClass class1 = classPool.get(className.replaceAll("/", ".")); CtMethod ctMethod = class1.getDeclaredMethod("sayHello"); if(!ctMethod.isEmpty()) { ctMethod.insertBefore("System.out.println(\"before hello!!!\");"); } return class1.toBytecode(); } catch (NotFoundException | CannotCompileException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return null; } }
這個類實現了 ClassFileTransformer 接口。ClassFileTransformer 當中規定的 transform 方法則完成類定義的替換轉換。
此處使用 javassist 技術對字節碼進行處理,對TransClass類的sayHello方法體前插入一行代碼
ctMethod.insertBefore("System.out.println(\"before hello!!!\");");
javassist 是功能十分強大的字節碼處理工具,其使用參考 javassist使用簡介。
須要將javassist的jar包導入到形目中。
Premain 代碼中:
inst.addTransformer(new Transformer());
addTransformer 方法並無指明要轉換哪一個類。轉換髮生在 premain 函數執行以後,main 函數執行以前,這時每裝載一個類,transform 方法就會執行一次,看看是否須要轉換,因此,在 transform(Transformer 類中)方法中,程序用 ClassName.equals("wqz/zoom/test/TransClass") 來判斷當前的類是否須要轉換。此處須要注意className的形式。
編寫一個 Java 類,包含以下兩個方法當中的任何一個
public static void premain(String agentArgs, Instrumentation inst); //[1] public static void premain(String agentArgs); //[2]
其中,[1] 的優先級比 [2] 高,將會被優先執行([1] 和 [2] 同時存在時,[2] 被忽略)。
在這個 premain 函數中,開發者能夠進行對類的各類操做。
agentArgs 是 premain 函數獲得的程序參數,隨同 「– javaagent
」一塊兒傳入。與 main 函數不一樣的是,這個參數是一個字符串而不是一個字符串數組,若是程序參數有多個,程序將自行解析這個字符串。
Inst 是一個 java.lang.instrument.Instrumentation 的實例,由 JVM 自動傳入。java.lang.instrument.Instrumentation 是 instrument 包中定義的一個接口,也是這個包的核心部分,集中了其中幾乎全部的功能方法,例如類定義的轉換和操做等等。
package wqz.zoom.test; import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; public class Premain { public static void premain(String agentArgs,Instrumentation inst) throws ClassNotFoundException,UnmodifiableClassException{ inst.addTransformer(new Transformer()); System.out.println("premain ok!"); } }
package wqz.zoom.test; public class TestMainInJar { public static void main(String[] args) { System.out.println("TestMainInJar main()"); new TransClass().sayHello(); } }
premain函數會在此main函數執行前執行。
instrumentation應用須要打包執行(eclipse/cmd 兩種打 jar 包的方式,自行百度)
將wqz.zoom.test 包 打包成 test.jar, 並將javassist的jar包放於同一目錄
使用壓縮工具打開test.jar,編輯 MANIFEST.MF並保存
添加以下配置:
Class-Path: javassist-3.15.0-GA.jar Premain-Class: wqz.zoom.test.Premain
運行jar:
java -javaagent:test.jar -cp test.jar wqz.zoom.test.TestMainInJar
運行結果:
public class Premain { public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException { ClassDefinition def = new ClassDefinition(TransClass.class,bytes);//bytes 爲TranClass的字節碼數組(修改前或修改後) inst.redefineClasses(new ClassDefinition[] { def }); System.out.println("success"); } }
ClassDefinition def = new ClassDefinition(TransClass.class,bytes);//bytes 爲TranClass的字節碼數組(修改前或修改後)
此字節數組參數可與 javassist 配合使用,傳入javassist修改後的數組。
例:若是要對一個已經加載的類進行重定義,從原理上講須要提供已經修改過的字節碼,這個修改過的字節碼就來源於bytes(此 bytes 能夠由javassist獲取原ctclass,在此基礎上進行編輯與修改,獲取修改後的bytes,傳入ClassDefinition)。
【注意事項】詳細閱讀 jdk文檔!!!
1. 僅當 Can-Retransform-Classes
清單屬性在代理 JAR 文件中設置爲 true
(如包規範所述),且 JVM 支持此性能時,才支持重轉換。
inst.isRedefineClassesSupported();//返回當前 JVM 配置是否支持類的重定義
2. 若是重定義的方法有活動的堆棧幀,那麼這些活動的幀將繼續運行原方法的字節碼。將在新的調用上使用此重定義的方法。
3. 重定義可能會更改方法體、常量池和屬性。重定義不得添加、移除、重命名字段或方法。
虛擬機啓動後的動態 instrument,開發者能夠在 main 函數開始執行之後,再啓動本身的 Instrumentation 程序,對應premain的一樣有方法:
public static void agentmain (String agentArgs, Instrumentation inst); // [1] public static void agentmain (String agentArgs); //[2]
用法和premain用法基本相同,jar清單文件MANIFEST.MF 區別Premain-Class 變爲 Agent-Class
常與 retransformClasses()方法配合使用。使用前認真閱讀就jdk文檔
在 Java SE 6 中,新的 Native Instrumentation 提出了一個新的 native code 的解析方式,做爲原有的 native method 的解析方式的一個補充,來很好地解決了一些問題。這就是在新版本的 java.lang.instrument 包裏,咱們擁有了對 native 代碼的 instrument 方式 —— 設置 prefix
博主在應用此種方式給更改本地方法前綴時走了很多彎路,現在仍沒有搞明白(T-T),但並非沒法可取,博主發現如下實現方式:
使用重定義技術實現(使用javassist重定義本地方法,調用代理類的本地方法,在代理類的本地方法中回調須要的本地方法)!!!