Java 编程的动态性,第 5 部分: 动态转换类--转载
在第 4 部分“?用 Javassist 進行類轉換”中,您學習了如何使用 Javassist 框架來轉換編譯器生成的 Java 類文件,同時寫回修改過的類文件。這種類文件轉換步驟對于做出持久變更是很理想的,但是如果想要在每次執行應用程序時做出不同的變更,這種方法就不一定很方便。對于這種暫時的變更,采用在您實際啟動應用程序時起作用的方法要好得多。
JVM 體系結構為我們提供了這樣做的便利途徑――通過使用 classloader 實現。通過使用 classloader 掛鉤(hook),您可以攔截將類加載到 JVM 中的過程,并在實際加載這些類之前轉換它們。為了說明這個過程是如何工作的,我將首先展示類加載過程的直接攔截,然后展示 Javassist 如何提供了一種可在您的應用程序中使用的便利捷徑。在整個過程中,我將利用取自本系列以前文章中的代碼片斷。
加載區域
運行 Java 應用程序的通常方式是作為參數向 JVM 指定主類。這對于標準操作沒有什么問題,但是它沒有提供及時攔截類加載過程的任何途徑,而這種攔截對大多數程序來說是很有用的。正如我在第 1 部分“?類和類裝入”中所討論的,許多類甚至在主類還沒有開始執行之前就已經加載了。要攔截這些類的加載,您需要在程序的執行過程中進行某種程度的重定向。
幸運的是,模擬 JVM 在運行應用程序的主類時所做的工作是相當容易的。您所需做的就是使用反射(這是在不得?第 2 部分?中介紹的)來首先找到指定類中的靜態?main()?方法,然后使用預期的命令行參數來調用它。清單 1 提供了完成這個任務的示例代碼(為簡單起見,我省略了導入和異常處理語句):
清單 1. Java 應用程序運行器
public class Run {public static void main(String[] args) {if (args.length >= 1) {try {// load the target class to be runClass clas = Run.class.getClassLoader().loadClass(args[0]);// invoke "main" method of target classClass[] ptypes =new Class[] { args.getClass() };Method main =clas.getDeclaredMethod("main", ptypes);String[] pargs = new String[args.length-1];System.arraycopy(args, 1, pargs, 0, pargs.length);main.invoke(null, new Object[] { pargs });} catch ...}} else {System.out.println("Usage: Run main-class args...");}} }要使用這個類來運行 Java 應用程序,只需將它指定為?java?命令的目標類,后面跟著應用程序的主類和想要傳遞給應用程序的其他任何參數。換句話說,如果用于運行 Java 應用程序的命令為:
java test.Test arg1 arg2 arg3您相應地要通過如下命令使用?Run?類來運行應用程序:
java Run test.Test arg1 arg2 arg3攔截類加載
就其本身而言,清單 1 中短小的?Run?類不是非常有用。為了實現攔截類加載過程的目標,我們需要采取進一步的動作,對應用程序類定義和使用我們自己的 classloader。
正如我們在第 1 部分中討論的,classloader 使用一個樹狀層次結構。每個 classloader(JVM 用于核心 Java 類的根 classloader 除外)都具有一個父 classloader。Classloader 應該在獨自加載類之前檢查它們的父 classloader,以防止當某個層次結構中的多個 classloader 加載同一個類時可能引發的沖突。首先檢查父 classloader 的過程稱為?委托――classloader 將加載類的責任委托給最接近根的 classloader,后者能夠訪問要加載類的信息。
當?清單 1?中的?Run?程序開始執行時,它已經被 JVM 默認的 System classloader(您定義的 classpath 所指定的那一個)加載了。為了符合類加載的委托規則,我們需要對相同的父 classloader 使用完全相同的 classpath 信息和委托,從而使我們的 classloader 成為 System classloader 的真正替代者。幸運的是,JVM 當前用于 System classloader 實現的?java.net.URLClassLoader?類提供了一種檢索 classpath 信息的容易途徑,它使用了?getURLs()?方法。為了編寫 classloader,我們只需從?java.net.URLClassLoader?派生子類,并初始化基類以使用相同的 classpath 和父 classloader 作為加載主類的 System classloader。清單 2 提供了這種方法的具體實現:
清單 2. 一個詳細的 classloader
public class VerboseLoader extends URLClassLoader {protected VerboseLoader(URL[] urls, ClassLoader parent) {super(urls, parent);}public Class loadClass(String name)throws ClassNotFoundException {System.out.println("loadClass: " + name);return super.loadClass(name);}protected Class findClass(String name)throws ClassNotFoundException {Class clas = super.findClass(name);System.out.println("findclass: loaded " + name +" from this loader");return clas;}public static void main(String[] args) {if (args.length >= 1) {try {// get paths to be used for loadingClassLoader base =ClassLoader.getSystemClassLoader();URL[] urls;if (base instanceof URLClassLoader) {urls = ((URLClassLoader)base).getURLs();} else {urls = new URL[]{ new File(".").toURI().toURL() };}// list the paths actually being usedSystem.out.println("Loading from paths:");for (int i = 0; i < urls.length; i++) {System.out.println(" " + urls[i]);}// load target class using custom class loaderVerboseLoader loader =new VerboseLoader(urls, base.getParent());Class clas = loader.loadClass(args[0]);// invoke "main" method of target classClass[] ptypes =new Class[] { args.getClass() };Method main =clas.getDeclaredMethod("main", ptypes);String[] pargs = new String[args.length-1];System.arraycopy(args, 1, pargs, 0, pargs.length);Thread.currentThread().setContextClassLoader(loader);main.invoke(null, new Object[] { pargs });} catch ...}} else {System.out.println("Usage: VerboseLoader main-class args...");}} }我們已從?java.net.URLClassLoader?派生了我們自己的?VerboseLoader?類,它列出正在被加載的所有類,同時指出哪些類是由這個 loader 實例(而不是委托父 classloader)加載的。這里同樣為簡潔起見而省略了導入和異常處理語句。
VerboseLoader?類中的前兩個方法?loadClass()?和?findClass()?重載了標準的 classloader 方法。?loadClass()?方法分別針對 classloader 請求的每個類作了調用。在此例中,我們僅讓它向控制臺打印一條消息,然后調用它的基類版本來執行實際的處理。基類方法實現了標準 classloader 委托行為,即首先檢查父 classloader 是否能夠加載所請求的類,并且僅在父 classloader 無法加載該類時,才嘗試使用受保護的findClass()?方法來直接加載該類。對于?findClass()?的?VerboseLoader?實現,我們首先調用重載的基類實現,然后在調用成功(在沒有拋出異常的情況下返回)時打印一條消息。
VerboseLoader?的?main()?方法或者從用于包含類的 loader 中獲得 classpath URL 的列表,或者在與不屬于?URLClassLoader?的實例的 loader 一起使用的情況下,簡單地使用當前目錄作為唯一的 classpath 條目。不管采用哪種方式,它都會列出實際正在使用的路徑,然后創建VerboseLoader?類的一個實例,并使用該實例來加載命令行上指定的目標類。該邏輯的其余部分(即查找和調用目標類的?main()?方法)與?清單 1?中的?Run?代碼相同。
清單 3 顯示了?VerboseLoader?命令行和輸出的一個例子,它用于調用清單 1 中的?Run?應用程序:
清單 3. 清單 2 中的程序的例子輸出
[dennis]$ java VerboseLoader Run Loading from paths:file:/home/dennis/writing/articles/devworks/dynamic/code5/ loadClass: Run loadClass: java.lang.Object findclass: loaded Run from this loader loadClass: java.lang.Throwable loadClass: java.lang.reflect.InvocationTargetException loadClass: java.lang.IllegalAccessException loadClass: java.lang.IllegalArgumentException loadClass: java.lang.NoSuchMethodException loadClass: java.lang.ClassNotFoundException loadClass: java.lang.NoClassDefFoundError loadClass: java.lang.Class loadClass: java.lang.String loadClass: java.lang.System loadClass: java.io.PrintStream Usage: Run main-class args...在此例中,唯一直接由?VerboseLoader?加載的類是?Run?類。?Run?使用的其他所有類都是核心 Java 類,它們是通過父 classloader 使用委托來加載的。這其中的大多數(如果不是全部的話)核心類實際上都會在?VerboseLoader?應用程序本身的啟動期間加載,因此父 classloader 將只返回一個指向先前創建的?java.lang.Class?實例的引用。
Javassist 攔截
清單 2?中的?VerboseClassloader?展示了攔截類加載的基本過程。為了在加載時修改類,我們可以更進一步,向?findClass()?方法添加代碼,把二進制類文件當作資源來訪問,然后使用該二進制數據。Javassist 實際上包括了直接完成此類攔截的代碼,因此與其進一步擴充這個例子,我們不如看看如何使用 Javassist 實現。
使用 Javassist 來攔截類加載的過程要依賴我們在?第 4 部分?中使用的相同?javassist.ClassPool?類。在該文中,我們通過名稱直接從ClassPool?請求類,以?javassist.CtClass?實例的形式取回該類的 Javassist 表示。然而,那并不是使用?ClassPool?的唯一方式――Javassist 還以?javassist.Loader?類的形式,提供一個使用?ClassPool?作為其類數據源的 classloader。
為了允許您在加載類時操作它們,?ClassPool?使用了一個 Observer 模式。您可以向?ClassPool?的構造函數傳遞預期的觀察者接口(observer interface)的一個實例?javassist.Translator?。每當從?ClassPool?請求一個新的類,它都調用觀察者的?onWrite()?方法,這個方法能夠在ClassPool?交付類之前修改該類的表示。
javassist.Loader?類包括一個便利的?run()?方法,它加載目標類,并且使用所提供的參數數組來調用該類的?main()?方法(就像在?清單 1?中一樣)。清單 4 展示了如何使用 Javassist 類和這個方法來加載和運行目標應用程序類。這個例子中簡單的?javassist.Translator?觀察者實現僅只是打印一條關于正在被請求的類的消息。
清單 4. Javassist 應用程序運行器
public class JavassistRun {public static void main(String[] args) {if (args.length >= 1) {try {// set up class loader with translatorTranslator xlat = new VerboseTranslator();ClassPool pool = ClassPool.getDefault(xlat);Loader loader = new Loader(pool);// invoke "main" method of target classString[] pargs = new String[args.length-1];System.arraycopy(args, 1, pargs, 0, pargs.length);loader.run(args[0], pargs);} catch ...}} else {System.out.println("Usage: JavassistRun main-class args...");}}public static class VerboseTranslator implements Translator{public void start(ClassPool pool) {}public void onWrite(ClassPool pool, String cname) {System.out.println("onWrite called for " + cname);}} }下面是?JavassistRun?命令行和輸出的一個例子,其中使用它來調用?清單 1?中的?Run?應用程序。
[dennis]$java -cp .:javassist.jar JavassistRun Run onWrite called for Run Usage: Run main-class args...回頁首
運行時定時
我們在?第 4 部分中分析過的方法定時修改對于隔離性能問題來說可能一個很有用的工具,但它的確需要一個更靈活的接口。在該文中,我們只是將類和方法名稱作為參數傳遞給程序,程序加載二進制類文件,添加定時代碼,然后寫回該類。對于本文,我們將把代碼轉換為使用加載時修改方法,并將它轉換為可支持模式匹配,用以指定要定時的類和方法。
在加載類時更改代碼以處理這種修改是很容易的。在清單 4 中的?javassist.Translator?代碼的基礎上,當正在寫出的類名稱與目標類名稱匹配時,我們可以僅從?onWrite()?調用用于添加定時信息的方法。清單 5 展示了這一點(沒有包含?addTiming()?的全部細節――請參閱第 4 部分以了解這些細節)。
清單 5. 在加載時添加定時代碼
public class TranslateTiming {private static void addTiming(CtClass clas, String mname)throws NotFoundException, CannotCompileException {...}public static void main(String[] args) {if (args.length >= 3) {try {// set up class loader with translatorTranslator xlat =new SimpleTranslator(args[0], args[1]);ClassPool pool = ClassPool.getDefault(xlat);Loader loader = new Loader(pool);// invoke "main" method of target classString[] pargs = new String[args.length-3];System.arraycopy(args, 3, pargs, 0, pargs.length);loader.run(args[2], pargs);} catch (Throwable ex) {ex.printStackTrace();}} else {System.out.println("Usage: TranslateTiming" +" class-name method-mname main-class args...");}}public static class SimpleTranslator implements Translator{private String m_className;private String m_methodName;public SimpleTranslator(String cname, String mname) {m_className = cname;m_methodName = mname;}public void start(ClassPool pool) {}public void onWrite(ClassPool pool, String cname)throws NotFoundException, CannotCompileException {if (cname.equals(m_className)) {CtClass clas = pool.get(cname);addTiming(clas, m_methodName);}}} }模式方法
如清單 5 所示,除了使方法定時代碼在加載時工作外,在指定要定時的方法時增加靈活性也是很理想的。我最初使用 Java 1.4java.util.regex?包中的正則表達式匹配支持來實現這點,然后意識到它并沒有真正帶來我想要的那種靈活性。問題在于,用于選擇要修改的類和方法的有意義的模式種類無法很好地適應正則表達式模型。
那么哪種模式對于選擇類和方法?有意義呢?我想要的是在模式中使用類和方法的任何幾個特征的能力,包括實際的類和方法名稱、返回類型,以及調用參數類型。另一方面,我不需要對名稱和類型進行真正靈活的比較――簡單的相等比較就能處理我感興趣的大多數情況,而對該比較添加基本的通配符就能處理其余的所有情況了。處理這種情況的最容易方法是使模式看起來像標準的 Java 方法聲明,另外再進行一些擴展。
關于這種方法的例子,下面是幾個與?test.StringBuilder?類的?String buildString(int)?方法相匹配的模式:
java.lang.String test.StringBuilder.buildString(int) test.StringBuilder.buildString(int) *buildString(int) *buildString這些模式的通用模式首先是一個可選的返回類型(具有精確的文本),然后是組合起來的類和方法名稱模式(具有“*”通配字符),最后是參數類型列表(具有精確的文本)。如果提供了返回類型,必須使用一個空格將它與方法名稱匹配相隔離,而參數列表則跟在方法名稱匹配后面。為了使參數匹配更靈活,我通過兩種方式來設置它。如果所給的參數是圓括號括起的列表,它們必須精確匹配方法參數。如果它們是使用方括號(“[]”)來括起的,所列出的類型全都必須作為匹配方法的參數來提供,不過該方法可以按任何順序使用它們,并且還可以使用附加的參數。因此?*buildString(int, java.lang.String)?將匹配其名稱以“buildString”結尾的任何方法,并且這些方法精確地按順序接受一個?int?類型和一個?String?類型的參數。?*buildString[int,java.lang.String]?將匹配具有相同名稱的方法,但是這些方法接受兩個?或更多的?參數,其中一個是?int?類型,另一個是?java.lang.String?類型。
清單 6 給出了我編寫來處理這些模式的?javassist.Translator?子類的簡略版本。實際的匹配代碼與本文并不真正相關,不過如果您想要查看它或親自使用它,我已將它包括在了下載文件中(請參閱?參考資料)。使用這個?TimingTranslator?的主程序類是?BatchTiming?,它也包括在下載文件中。
清單 6. 模式匹配轉換程序
public class TimingTranslator implements Translator {public TimingTranslator(String pattern) {// build matching structures for supplied pattern...}private boolean matchType(CtMethod meth) {...}private boolean matchParameters(CtMethod meth) {...}private boolean matchName(CtMethod meth) {...}private void addTiming(CtMethod meth) {...}public void start(ClassPool pool) {}public void onWrite(ClassPool pool, String cname)throws NotFoundException, CannotCompileException {// loop through all methods declared in classCtClass clas = pool.get(cname);CtMethod[] meths = clas.getDeclaredMethods();for (int i = 0; i < meths.length; i++) {// check if method matches full patternCtMethod meth = meths[i];if (matchType(meth) &&matchParameters(meth) && matchName(meth)) {// handle the actual timing modificationaddTiming(meth);}}} }回頁首
后續內容
在上兩篇文章中,您已經看到了如何使用 Javassist 來處理基本的轉換。對于下一篇文章,我們將探討這個框架的高級特性,這些特性提供用于編輯字節代碼的查找和替換技術。這些特性使得對程序行為做出系統性的變更很容易,其中包括諸如攔截所有方法調用或所有字段訪問這樣的變更。它們是理解為什么 Javassist 是 Java 程序中提供面向方面支持的卓越框架的關鍵。請下個月再回來看看如何能夠使用 Javassist 來揭示應用程序中的方面(aspect)
原文:http://www.ibm.com/developerworks/cn/java/j-dyn0203/index.html
轉載于:https://www.cnblogs.com/davidwang456/p/4035714.html
總結
以上是生活随笔為你收集整理的Java 编程的动态性,第 5 部分: 动态转换类--转载的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 编程的动态性, 第4部分: 用
- 下一篇: Java 编程的动态性,第 6 部分: