天天看點

Javassist入門1. Reading and writing bytecode2. ClassPool3. Class loader

Javassist是一個用于處理Java位元組碼的類庫。

百度百科:
Javassist是一個開源的分析、編輯和建立Java位元組碼的類庫。是由東京工業大學的數學和計算機科學系的 Shigeru Chiba (千葉 滋)所建立的。它已加入了開放源代碼JBoss 應用伺服器項目,通過使用Javassist對位元組碼操作為JBoss實作動态"AOP"架構。

原文位址

*以下内容大部分為機翻。如有不适請自行調整~~~~*

1. Reading and writing bytecode

Javassist是一個用于處理Java位元組碼的類庫。 Java位元組碼存儲在稱為類檔案的二進制檔案中。每個類檔案包含一個Java類或接口。

Javassist.CtClass類是類檔案的抽象表示。 CtClass(編譯時類)對象是用于處理類檔案的句柄。以下程式是一個非常簡單的例子:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();
           

該程式首先獲得一個ClassPool對象,該對象使用Javassist控制位元組碼修改。ClassPool對象是表示類檔案的CtClass對象的容器。它根據需要讀取類檔案以構造CtClass對象,并記錄構造的對象以響應以後的通路。要修改類的定義,使用者必須首先從ClassPool對象擷取對表示該類的CtClass對象的引用。ClassPool中的get()用于此目的。在上面顯示的程式的情況下,表示類test.Rectangle的CtClass對象是從ClassPool對象獲得的,并且它被指派給變量cc。getDefault()傳回的ClassPool預設的系統搜尋路徑的對象。

從實作的角度來看,ClassPool是CtClass對象的哈希表,它使用類名作為鍵。ClassPool中的get()搜尋此哈希表以查找與指定鍵關聯的CtClass對象。如果找不到這樣的CtClass對象,則get()讀取一個類檔案以構造一個新的CtClass對象,該對象記錄在哈希表中,然後作為get()的結果值傳回。

可以修改從ClassPool對象擷取的CtClass對象(稍後将介紹如何修改CtClass的詳細資訊)。在上面的示例中,對其進行了修改,以便将test.Rectangle的超類更改為類test.Point。

當最終調用CtClass()中的writeFile()時,此更改将反映在原始類檔案中。

writeFile()将CtClass對象轉換為類檔案并将其寫入本地磁盤。 Javassist還提供了一種直接擷取修改後的位元組碼的方法。要擷取位元組碼,請調用Bytecode():

byte[] b = cc.toBytecode();
           

您也可以直接加載CtClass:

Class clazz = cc.toClass();
           

toClass()請求目前線程的上下文類加載器加載由CtClass表示的類檔案。它傳回一個表示加載類的java.lang.Class對象。有關詳細資訊,請參閱下面的此部分(3.1 The toClass method in CtClass)。

1.1 Defining a new class

要從頭開始定義新類,必須在ClassPool上調用makeClass()。

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");
           

該程式定義了一個不包含成員的類Point。 Point的成員方法可以使用在CtNewMethod中聲明的工廠方法建立,并使用CtClass中的addMethod()附加到Point。

makeClass()無法建立新接口;ClassPool中的makeInterface()可以做到。可以使用CtNewMethod中的abstractMethod()建立接口中的成員方法。注意,接口方法是一種抽象方法。

1.2 Frozen classes

如果CtClass對象通過writeFile(),toClass()或toBytecode()轉換為類檔案,Javassist将當機該CtClass對象。将不允許對該CtClass對象進行進一步修改。這是為了在開發人員嘗試修改已加載的類檔案時警告開發人員,因為JVM不允許重新加載類。

當機的CtClass也可以解凍,以便允許修改類定義。

例如:

CtClasss cc = ...;
    :
cc.writeFile();
cc.defrost();
cc.setSuperclass(...);    // OK since the class is not frozen.
           

調用defrost() 後,可以再次修改CtClass對象。

如果ClassPool.doPruning設定為true,那麼當Javassist當機該對象時,Javassist将修剪CtClass對象中的資料結構。為了減少記憶體消耗,修剪會丢棄該對象中不必要的屬性(attribute_info結構)。

例如,丢棄Code_attribute結構(方法體)。是以,在修剪CtClass對象之後,除方法名稱,簽名和注釋外,方法的位元組碼不可通路。已修剪的CtClass對象将無法再次解凍。ClassPool.doPruning的預設值為false。

要禁止修剪特定的CtClass,必須事先在該對象上調用stopPruning():

CtClasss cc = ...;
cc.stopPruning(true);
    :
cc.writeFile();                             // convert to a class file.
// cc is not pruned.
           

CtClass對象cc未被修剪。是以,在調用writeFile()之後可以解凍。

注意:在調試時,您可能希望暫時停止修剪和當機,并将修改後的類檔案寫入磁盤驅動器。debugWriteFile()是一種友善的方法。它停止修剪,寫一個類檔案,解凍它,然後再次修剪(如果它最初打開)。

1.3 Class search path

預設的ClassPool通過靜态方法ClassPool.getDefault()獲得,與底層JVM(Java虛拟機)具有的相同路徑。如果程式在諸如JBoss和Tomcat之類的Web應用程式伺服器上運作,則ClassPool對象可能無法找到使用者類,因為這樣的Web應用程式伺服器使用多個類加載器以及系統類加載器。在這種情況下,必須在ClassPool中注冊其他類路徑。假設該池引用ClassPool對象:

pool.insertClassPath(new ClassClassPath(this.getClass()));
           

上面的注冊用于加載此引用的對象的類的類路徑。您可以使用任何Class對象作為參數而不是this.getClass(),該Class對象用于表示加載的類的路徑已注冊。

你可以将目錄名稱注冊為類搜尋路徑。例如,以下代碼将目錄/usr/local/javalib添加到搜尋路徑:

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath("/usr/local/javalib");
           

使用者可以添加的搜尋路徑不僅是目錄,還包括URL:

ClassPool pool = ClassPool.getDefault();
ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist.");
pool.insertClassPath(cp);
           

該程式将 “http://www.javassist.org:80/java/” 添加到類搜尋路徑中。此URL僅用于搜尋包org.javassist中的類。例如,要加載類org.javassist.test.Main,其類檔案将從以下位置擷取:http://www.javassist.org:80/java/org/javassist/test/Main.class

此外,可以直接向ClassPool對象提供一個位元組數組,并從該數組構造一個CtClass對象。

為此,請使用ByteArrayClassPath。

例如:

ClassPool cp = ClassPool.getDefault();
byte[] b = a byte array;
String name = class name;
cp.insertClassPath(new ByteArrayClassPath(name, b));
CtClass cc = cp.get(name);
           

獲得的CtClass對象表示由b指定的類檔案定義的類。如果調用get()并且get(name)的名字與name指定的類名相同,則ClassPool從給定的ByteArrayClassPath讀取類檔案。

如果你不知道該類的全名稱,則可以在ClassPool中使用makeClass()

ClassPool cp = ClassPool.getDefault();
InputStream ins = an input stream for reading a class file;
CtClass cc = cp.makeClass(ins);
           

makeClass()會傳回從給定輸入流構造的CtClass對象。通過使用makeClass()可以快速的将類檔案提供給ClassPool對象。如果搜尋路徑内包含較大的jar檔案,這可能會提高性能。由于ClassPool對象根據需要讀取類檔案,是以它可能會重複搜尋整個jar檔案中的每個類檔案。 由makeClass()構造的CtClass儲存在ClassPool對象中,是以不會再讀取類檔案,是以makeClass()可用于優化此搜尋。

使用者可以擴充類搜尋路徑,他們可以定義一個實作ClassPath接口的新類,并将該類的執行個體提供給ClassPool中的insertClassPath()。這允許非标準資源包含在搜尋路徑中。

2. ClassPool

ClassPool對象是CtClass對象的容器。建立CtClass對象後,它将永遠記錄在ClassPool中。這是因為編譯器需要通路CtClass對象,是在該CtClass代表的源代碼編譯之後。

例如,假設将一個新方法getter()添加到表示Point類的CtClass對象中。稍後,程式嘗試編譯源代碼,包括在Point中調用getter()的方法,并使用編譯的代碼作為方法的主體,将其添加到另一個類Line。如果表示Point的CtClass對象丢失,則編譯器無法将方法調用編譯為getter()。請注意,原始類定義不包括getter()。是以,要正确編譯這樣的方法調用,ClassPool必須始終包含程式執行的所有CtClass執行個體。

2.1 Avoid out of memory

如果CtClass對象的數量變得非常大,那麼ClassPool的這種規範可能會導緻巨大的記憶體消耗(這很少發生,因為Javassist試圖以各種方式(Frozen classes)減少記憶體消耗)。要避免此問題,可以從ClassPool中主動的删除不必要的CtClass對象。如果在CtClass對象上調用detach(),則會從ClassPool中删除該CtClass對象。

例如:

CtClass cc = ... ;
cc.writeFile();
cc.detach();
           

你不可以在調用detach()後,在此調用任何該CtClass對象上的方法。但是,您可以在ClassPool上調用get()來擷取CtClass的新執行個體表示相同的類,如果調用get()方法,ClassPool會再次讀取一個類檔案并重新建立一個CtClass對象,該對象由get()傳回。

另一個想法是用新的ClassPool替換ClassPool并丢棄舊的ClassPool,如果舊的ClassPool是垃圾收集的,那麼ClassPool中包含的CtClass對象也是垃圾收集的,要建立ClassPool的新執行個體,請執行以下代碼段:

ClassPool cp = new ClassPool(true);
// if needed, append an extra search path by appendClassPath()
           

這将建立一個ClassPool對象,其行為與ClassPool.getDefault()傳回的預設ClassPool相同。請注意,為友善起見,ClassPool.getDefault()是一個單獨的工廠方法。它以與上面所示相同的方式建立一個ClassPool對象,盡管它是一個ClassPool的單例。getDefault()傳回的ClassPool對象并沒有什麼特殊, getDefault()隻是一種簡便的方法。

請注意,新的ClassPool(true)是一個友善的構造函數,它構造一個ClassPool對象并将系統搜尋路徑添加給它。調用該構造函數等效于以下代碼:

ClassPool cp = new ClassPool();
cp.appendSystemPath(); 
// or append another path by appendClassPath()
           

2.2 Cascaded ClassPools

如果程式在Web應用程式伺服器上運作,則可能需要建立多個ClassPool執行個體;應為每個類加載器(即容器)建立一個ClassPool執行個體。程式建立ClassPool對象不應該調用 getDefault()而是通過構造ClassPool。

多個ClassPool對象可以像java.lang.ClassLoader一樣級聯。例如:

ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.insertClassPath("./classes");
           

如果調用了child.get(),則子類ClassPool首先委托給父ClassPool。如果父ClassPool無法找到類檔案,則子ClassPool會嘗試在./classes目錄下查找類檔案。

如果child.childFirstLookup為true,則子類ClassPool會在委派給父ClassPool之前嘗試查找類檔案。

例如:

ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
// the same class path as the default one.
child.appendSystemPath();     
// changes the behavior of the child.
child.childFirstLookup = true;    
           

2.3 Changing a class name for defining a new class

可以将新類定義為現有類的副本。可以通過以下方法:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.setName("Pair");
           

程式首先擷取Point類的CtClass對象。然後它調用setName()為該CtClass對象賦予一個新的名稱Pair。在此調用之後,由該CtClass對象表示的類定義中出現的所有類名都将從Point更改為Pair,該類定義的其餘部分不會改變。

請注意,CtClass中的setName()更改了ClassPool對象中的記錄。從實作的角度來看,ClassPool對象是CtClass對象的哈希表,setName()更改與哈希表中的CtClass對象關聯的key值。

key從原始類名更改為新類名。

是以,如果稍後再次在ClassPool對象上調用get(“Point”),則它永遠不會傳回變量cc引用的CtClass對象。ClassPool對象再次讀取一個類檔案Point.class,它為類Point構造一個新的CtClass對象,這是因為與Point名稱關聯的CtClass對象不再存在了。

請參閱以下内容:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");

// cc1 is identical to cc.
CtClass cc1 = pool.get("Point");   
cc.setName("Pair");

// cc2 is identical to cc.
CtClass cc2 = pool.get("Pair");   

 // cc3 is not identical to cc.
CtClass cc3 = pool.get("Point");  
           

cc1和cc2指的是與cc相同的CtClass執行個體,而cc3則不是。請注意,在執行cc.setName(“Pair”)之後,cc和cc1引用的CtClass對象表示Pair類。

ClassPool對象用于維護類和CtClass對象之間的一對一映射。除非建立了兩個獨立的ClassPool,否則Javassist永遠不會允許兩個不同的CtClass對象表示同一個類。這是程式轉換的一個重要的特征。

要建立ClassPool.getDefault()傳回的ClassPool預設執行個體的另一個副本,請執行以下代碼片段(此代碼已在上面顯示):

ClassPool cp = new ClassPool(true);
           

如果您有兩個ClassPool對象,則可以從每個ClassPool中擷取表示同一類檔案的不同CtClass對象。你可以修改這些CtClass對象以生成該類的不同版本。

2.4 Renaming a frozen class for defining a new class

一旦通過writeFile()或toBytecode()将CtClass對象轉換為類檔案,Javassist就會拒絕對該CtClass對象進行進一步修改。是以,在将表示Point類的CtClass對象轉換為類檔案之後,不能将Pair類定義為Point的副本,因為在Point上執行setName()會被拒絕。

以下代碼段錯誤示範:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();

 // wrong since writeFile() has been called.
cc.setName("Pair");   
           

要避免此限制,您應該在ClassPool中調用getAndRename()。

例如:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
CtClass cc2 = pool.getAndRename("Point", "Pair");
           

如果調用getAndRename(),則ClassPool首先讀取Point.class以建立表示Point類的新CtClass對象。并且在将CtClass對象記錄在哈希表中之前,會将CtClass對象從Point重命名為Pair。是以,在表示Point類的CtClass對象上調用writeFile()或toBytecode()之後,可以執行getAndRename()。

3. Class loader

如果事先知道必須修改哪些類,則修改類的最簡單方法如下:

  1. 通過調用ClassPool.get()擷取CtClass對象
  2. 修改它
  3. 在該CtClass對象上調用writeFile()或toBytecode()以擷取修改後的類檔案

如果在加載時确定是否修改了類,則使用者必須使Javassist與類加載器協作。Javassist可以與類加載器一起使用,以便可以在加載時修改位元組碼。Javassist的使用者可以定義自己的類加載器版本,也可以使用Javassist提供的類加載器。

3.1 The toClass method in CtClass

CtClass提供了一個友善的方法toClass(),來請求目前線程的上下文類加載器加載由CtClass對象表示的類。要調用此方法,調用者必須具有适當的權限;否則,可能會抛出SecurityException。

以下程式顯示了如何使用toClass():

public class Hello {
    public void say() {
        System.out.println("Hello");
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        ClassPool cp = ClassPool.getDefault();
        CtClass cc = cp.get("Hello");
        CtMethod m = cc.getDeclaredMethod("say");
        m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
        Class c = cc.toClass();
        Hello h = (Hello)c.newInstance();
        h.say();
    }
}
           

Test.main()在Hello中的say()方法體中插入對println()的調用。然後它構造一個修改過的Hello類的執行個體,并在該執行個體上調用say()。

請注意,上面的程式取決于在調用toClass()之前Hello類不會被調用,否則,JVM将在toClass()請求之前加載原始的Hello類拒絕加載修改後的Hello類。是以加載修改後的Hello類将失敗(抛出LinkageError)。

例如,如果Test中的main()是這樣的:

public static void main(String[] args) throws Exception {
    Hello orig = new Hello();
    ClassPool cp = ClassPool.getDefault();
    CtClass cc = cp.get("Hello");
        :
}
           

然後在main的第一行加載原始的Hello類,并且對toClass()的調用抛出異常,因為類加載器不能同時加載兩個不同版本的Hello類。

如果程式在某些應用程式伺服器(如JBoss和Tomcat)上運作,則toClass()使用的上下文類加載器可能不合适。在這種情況下,你會看到異常ClassCastException。要避免此異常,必須顯式為toClass()提供适當的類加載器。

例如,如果bean是您的會話bean對象,那麼以下代碼:

CtClass cc = ...;
Class c = cc.toClass(bean.getClass().getClassLoader());
           

如果想要運作,你應該給toClass()加載你的程式的類加載器(在上面的例子中,bean對象的類)。

提供toClass()是為了友善。如果需要更複雜的功能,則應編寫自己的類加載器。

3.2 Class loading in Java

在Java中,多個類加載器可以共存,每個類加載器都可以建立自己的名稱空間。不同的類加載器可以加載具有相同類名的不同類檔案。加載的兩個類被視為不同的類。此功能使我們能夠在單個JVM上運作多個應用程式,即使這些程式包含具有相同名稱的不同類。

注意:JVM不允許動态重新加載類。一旦類加載器加載了一個類,它就無法在運作時重新加載該類的修改版本。是以,在JVM加載類之後,無法更改類的定義,但是,JPDA(Java平台調試器體系結構)提供了重新加載類的有限能力。見3.6。

如果同一個類檔案由兩個不同的類加載器加載,則JVM會生成兩個具有相同名稱和定義的不同類。這兩個類被視為不同的類。由于這兩個類不相同,是以一個類的執行個體不能配置設定給另一個類的變量。兩個類之間的轉換操作失敗并抛出ClassCastException。

例如,以下代碼段會引發異常:

MyClassLoader myLoader = new MyClassLoader();
Class clazz = myLoader.loadClass("Box");
Object obj = clazz.newInstance();
Box b = (Box)obj;  
// this always throws ClassCastException.
           

Box類由兩個類加載器加載。假設類加載器CL加載包含此代碼片段的類。由于此代碼段引用了MyClassLoader,Class,Object和Box,是以CL還會加載這些類(除非它委托給另一個類加載器)。是以,變量b的類型是CL加載的Box類。另一方面,myLoader也加載Box類。對象obj是myLoader加載的Box類的執行個體。是以,最後一個語句總是抛出一個ClassCastException,因為obj的類是一個不同版本的Box類,而不是用作變量b的類型。

多個類加載器形成樹結構。除引導加載程式之外的每個類加載器都有一個父類加載器,它通常加載了該子類加載器的類。由于可以沿着類加載器的這個層次結構委托加載類的請求,是以可以通過不請求類加載的類加載器加載類。是以,已經請求加載類C的類加載器可能與實際加載類C的加載器不同。為了區分,我們将前加載器稱為C的發起者,并将後者加載器稱為C的實際加載器。

此外,如果類加載器CL請求加載類C(C的發起者)委托給父類加載器PL,那麼類加載器CL永遠不會被請求加載類C定義中引用的任何類。CL不是這些類的發起者。相反,父類加載器PL成為它們的啟動器,并要求加載它們。C類的定義所引用的類由C的實際加載器加載。

要了解這種行為,讓我們考慮以下示例。

public class Point {    // loaded by PL
    private int x, y;
    public int getX() { return x; }
        :
}
// the initiator is L but the real loader is PL
public class Box {      
    private Point upperLeft, size;
    public int getBaseX() { return upperLeft.x; }
        :
}
 // loaded by a class loader L
public class Window {   
    private Box box;
    public int getBaseX() { return box.getBaseX(); }
}
           

假設一個類Window由一個類加載器L加載。發起者和Window的真實加載器都是L.由于Window的定義是指Box,是以JVM将請求L加載Box。這裡,假設L将此任務委托給父類加載器PL。Box的發起者是L,但真正的加載器是PL。在這種情況下,Point的發起者不是L而是PL,因為它與Box的真實加載器相同。是以,永遠不會要求L加載Point。

接下來,讓我們考慮一個略微修改的示例。

public class Point {
    private int x, y;
    public int getX() { return x; }
        :
}
 // the initiator is L but the real loader is PL
public class Box {     
    private Point upperLeft, size;
    public Point getSize() { return size; }
        :
}
 // loaded by a class loader L
public class Window {   
    private Box box;
    public boolean widthIs(int w) {
        Point p = box.getSize();
        return w == p.getX();
    }
}
           

現在,Window的定義也指Point。在這種情況下,如果請求加載Point,則類加載器L也必須委托給PL。你必須避免讓兩個類加載器加倍加載同一個類。兩個裝載機中的一個必須委托給另一個。

如果L在加載Point時沒有委托給PL,則widthIs()會抛出ClassCastException。由于Box的真實加載器是PL,是以Box中引用的Point也由PL加載。是以,getSize()的結果值是由PL加載的Point的執行個體,而widthIs()中的變量p的類型是由L加載的Point。JVM将它們視為不同的類型,是以由于類型不比對而引發異常。

這種行為有點不友善但有必要。如果聲明如下:

Point p = box.getSize();
           

沒有抛出異常,那麼編寫Window的程式員可以改變Point對象的封裝。例如,字段x在由PL加載的Point中是私有的。但是,如果L使用以下定義加載Point,則Window類可以直接通路x的值:

public class Point {
    public int x, y;    // not private
    public int getX() { return x; }
        :
}
           

有關Java中類加載器的更多詳細資訊,以下文章将有所幫助:

Sheng Liang and Gilad Bracha, "Dynamic Class Loading in the Java Virtual Machine", 
ACM OOPSLA'98, pp.36-44, 1998.
           

3.3 Using javassist.Loader

Javassist提供了一個類加載器javassist.Loader。此類加載器使用javassist.ClassPool對象來讀取類檔案。

例如,javassist.Loader可用于加載使用Javassist修改的特定類。

import javassist.*;
import test.Rectangle;

public class Main {
  public static void main(String[] args) throws Throwable {
     ClassPool pool = ClassPool.getDefault();
     Loader cl = new Loader(pool);

     CtClass ct = pool.get("test.Rectangle");
     ct.setSuperclass(pool.get("test.Point"));

     Class c = cl.loadClass("test.Rectangle");
     Object rect = c.newInstance();
         :
  }
}
           

該程式修改了一個類test.Rectangle。test.Rectangle的超類設定為test.Point類。然後,該程式加載修改後的類,并建立test.Rectangle類的新執行個體。

如果使用者希望在加載時按需修改類,則使用者可以向javassist.Loader添加事件監聽器。當類加載器加載類時,會通知添加的事件監聽器。事件監聽器類必須實作以下接口:

public interface Translator {
    public void start(ClassPool pool)
        throws NotFoundException, CannotCompileException;
    public void onLoad(ClassPool pool, String classname)
        throws NotFoundException, CannotCompileException;
}
           

當javassist.Loader中的addTranslator()将此事件偵聽器添加到javassist.Loader對象時,将調用方法start(),在javassist.Loader加載類之前調用onLoad()方法。 onLoad()可以修改加載類的定義。

例如,以下事件監聽器在加載之前将所有類更改為公共類。

public class MyTranslator implements Translator {
    void start(ClassPool pool)
        throws NotFoundException, CannotCompileException {}
    void onLoad(ClassPool pool, String classname)
        throws NotFoundException, CannotCompileException
    {
        CtClass cc = pool.get(classname);
        cc.setModifiers(Modifier.PUBLIC);
    }
}
           

請注意,onLoad()不必調用toBytecode()或writeFile(),因為javassist.Loader調用這些方法來擷取類檔案。

要使用MyTranslator對象運作應用程式類MyApp,請按如下方式編寫主類:

import javassist.*;

public class Main2 {
  public static void main(String[] args) throws Throwable {
     Translator t = new MyTranslator();
     ClassPool pool = ClassPool.getDefault();
     Loader cl = new Loader();
     cl.addTranslator(pool, t);
     cl.run("MyApp", args);
  }
}
           

要運作此程式,請執行:

% java Main2 arg1 arg2...
           

MyApp和其他應用程式類由MyTranslator翻譯。

請注意,像MyApp這樣的應用程式類無法通路諸如Main2,MyTranslator和ClassPool之類的加載器類,因為它們由不同的加載器加載。應用程式類由javassist.Loader加載,而加載器類(如Main2)由預設的Java類加載器加載。

javassist.Loader搜尋classes的順序與java.lang.ClassLoader不同。ClassLoader首先将加載操作委托給父類加載器,然後僅在父類加載器找不到它們時才嘗試加載類。另一方面,javassist.Loader嘗試在委托父類加載器之前加載類。

它僅在以下情況下委托:

  • 通過在ClassPool對象上調用get()找不到類
  • 通過使用由父類加載器加載的delegateLoadingOf()來指定的類。

此搜尋順序允許Javassist加載修改的類,但是,如果由于某種原因無法找到修改的類,它會委托給父類加載器,一旦類由父類加載器加載,該類中引用的其他類也将由父類加載器加載,是以它們永遠不會被修改。回想一下,C類中引用的所有類都由C的實際加載器加載。如果您的程式加載修改後的類失敗,那麼你應該确認該類的所有類是否已由javassist.Loader加載。

3.4 Writing a class loader

使用Javassist的簡單類加載器如下:

import javassist.*;

public class SampleLoader extends ClassLoader {
    /* Call MyApp.main().
     */
    public static void main(String[] args) throws Throwable {
        SampleLoader s = new SampleLoader();
        Class c = s.loadClass("MyApp");
        c.getDeclaredMethod("main", new Class[] { String[].class })
         .invoke(null, new Object[] { args });
    }

    private ClassPool pool;

    public SampleLoader() throws NotFoundException {
        pool = new ClassPool();
        pool.insertClassPath("./class"); // MyApp.class must be there.
    }

    /* Finds a specified class.
     * The bytecode for that class can be modified.
     */
    protected Class findClass(String name) throws ClassNotFoundException {
        try {
            CtClass cc = pool.get(name);
            // modify the CtClass object here
            byte[] b = cc.toBytecode();
            return defineClass(name, b, 0, b.length);
        } catch (NotFoundException e) {
            throw new ClassNotFoundException();
        } catch (IOException e) {
            throw new ClassNotFoundException();
        } catch (CannotCompileException e) {
            throw new ClassNotFoundException();
        }
    }
}
           

MyApp類是一個應用程式。要執行此程式,首先将類檔案放在./class目錄下,該目錄不能包含在類搜尋路徑中。否則,MyApp.class将由預設的系統類加載器加載,該加載器是SampleLoader的父加載器。目錄名./class由構造函數中的insertClassPath()指定。如果需要,您可以選擇其他名稱而不是./class。

如果你想。然後執行以下操作:

% java SampleLoader
           

類加載器加載類MyApp(./class/MyApp.class)并使用指令行參數調用MyApp.main()。

這是使用Javassist的最簡單方法。但是,如果編寫更複雜的類加載器,則可能需要詳細了解Java的類加載機制。例如,上面的程式将MyApp類放在與SampleLoader類所屬的名稱空間分開的名稱空間中,因為這兩個類由不同的類加載器加載。是以,MyApp類無法直接通路SampleLoader類。

3.5 Modifying a system class

系統類(如java.lang.String)不能由系統類加載器以外的類加載器加載。是以,上面顯示的SampleLoader或javassist.Loader無法在加載時修改系統類。

如果您的應用程式需要這樣做,則必須對系統類進行靜态修改。例如,以下程式向java.lang.String添加一個新字段hiddenValue:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("java.lang.String");
CtField f = new CtField(CtClass.intType, "hiddenValue", cc);
f.setModifiers(Modifier.PUBLIC);
cc.addField(f);
cc.writeFile(".");
           

該程式生成一個檔案“./java/lang/String.class”。

要使用此經過修改的String類運作程式MyApp,請執行以下操作:

% java -Xbootclasspath/p:. MyApp arg1 arg2...
           

假設MyApp的定義如下:

public class MyApp {
    public static void main(String[] args) throws Exception {
        System.out.println(String.class.getField("hiddenValue").getName());
    }
}
           

如果正确加載了修改後的String類,MyApp将列印hiddenValue。

注意:不應部署使用此技術來覆寫rt.jar中的系統類的應用程式,因為這樣做會違反Java 2 Runtime Environment二進制代碼許可證。

3.6 Reloading a class at runtime

如果在啟用JPDA(Java平台調試器體系結構)的情況下啟動JVM,則可以動态地重新加載類。在JVM加載類之後,可以解除安裝舊版本的類定義,并且可以再次重新加載新版本。也就是說,可以在運作時動态修改該類的定義。但是,新類定義必須與舊類定義相容。JVM不允許兩個版本之間的架構更改。他們有相同的方法和領域。

Javassist提供了一個友善的類,用于在運作時重新加載類。有關更多資訊,請參閱javassist.tools.HotSwapper的API文檔。

------------ 未完 -------------