同一個java類由不同的classloader加載問題


最近在測試項目代碼中遇到同一個類由不同的classloader加載后出現的問題:

  1. 類A中有一個字段a,它的類型為X
  2. 類B中有一個字段b,它的類型也為X
  3. 類A由classLoaderA所加載,類B由classLoaderB所加載
  4. 執行賦值語句A.a = B.b,由於這兩個類型均為X,可以執行,但是有一個要求,這個要求就是在A中所裝載類X的裝載器必須和在B中裝載類X的裝載器相同,否則賦值語句失敗

    為什么會產生上面的輸出,我們可以來看一個以下的代碼

 首先是一個簡單的類調用:

類Foo3

1 public class Foo3 implements IFoo{
2     public void hello() throws Exception{
3         Class<?> clazz = Foo.class;
4         Foo foo2 = Foo4.foo;
5     }
6 }

在上面的代碼中,變量foo2引用了類Foo4的一個靜態引用:

1 public class Foo4 {
2     public static Foo foo = new Foo();
3 }

類Foo是一個非常簡單的java類,即普通的java類:

1 public class Foo implements IFoo{}

  重點在於如何運行這段代碼,我們運行一段代碼,分別使用兩個類加載器來加載同一個類類Foo,運行代碼如下

1 MyClassLoader3 myClassLoader3 = new MyClassLoader3(T.class.getClassLoader());
2 IFoo foo3 = (IFoo) (myClassLoader3.loadClass("com.m_ylf.study.java.classLoad.Foo3").newInstance());
3 foo3.hello();

在上面的代碼中,采用自定義的classLoader來定義類Foo3,我們來看具體的定義:

1 public Class<?> loadClass(String name) throws ClassNotFoundException {
2     if("Foo".equals(name) ) {
3         //自定義
4     }
5     if("Foo3".equals(name) ) {
6         //自定義
7     }
8     return super.loadClass(name);
9 }

 其實就是將類類Foo和類Foo3交由classLoader3即我們自定義加載器來加載,其它的類仍交由super即appClassLoader來加載。現在運行這段代碼,即會有一個出錯信息,出錯信息如下:

Exception in thread "main" java.lang.LinkageError: loader constraint violation: when resolving field "foo" the class
loader (instance of MyClassLoader3) of the referring class, Foo4, and the class loader (instance of sun/misc
/Launcher$AppClassLoader) for the field's resolved type, /Foo, have different Class objects for that type
    at Foo3.hello(Foo3.java:7)

  錯誤在第7行,即Foo foo2 = Foo4.foo;這一行出錯了。

    為什么會出錯,我來看來第一行代碼:Class<?> clazz = Foo.class;這段代碼,會對Foo類進行加載,采用的加載器為myClassLoader3,即加載Foo3類時所使用的加載器。這句話運行之后,即表示類Foo已經被加載了,且加載器為myClassLoader3。
    第二行代碼:Foo foo2 = Foo4.foo。這段代碼會初始化Foo4,由於myClassLoader3並沒有特殊處理Foo4,所以將由父類加載器,即AppClassLoader來加載,在加載過程中,因為調用到了Foo4.foo,所以會加載Foo類。這個加載是在Foo4類初始化時進行加載的。因為在碰到類Foo時,appClassLoader顯示其從未加載過foo(先前的foo是由myClassLoader加載的,而不是由appClassLoader加載的),所以又會加載Foo。
    這時候,類Foo就會有兩個類加載器,一個是由myClassLoader3加載的,另一個是由appClassLoader加載的。如果兩個類分開運行,代碼是沒有問題的。
    問題就出在這個賦值語句,或者說是對象引用上。在Foo3內部使用Foo4.foo時,JVM會記錄Foo4.foo在foo3內部的類引用和加載器,在這個運行代碼中,此加載器為myClassLoader,因為在調用Foo4.foo之前已經加載了Foo。然而,在引用時,它將得到聲明Foo4.foo時的Foo類型的加載器,在Foo4.foo中, Foo類型的加載器為appClassLoader。JVM在運行時會對這兩個加載器進行驗證,JVM規范中要求這兩個加載器必須要一致,否則將報類驗證錯誤,即VerifyError的錯誤,這是為了防止不正常的類冒充正確的類進行類型欺騙。如在類Foo3中的Foo是來自於黑客故意構建的一個類時。

    我們再來看關於jbmp的問題,這是由於引用juel.jar時,里面有一個類如ExpressionFactory類,此類在類JspApplicationContext中被聲明。在juel.jar中,類ExpressionFactory已經被jspClassLoader加載了,現在要進行賦值語句,即=由jspContext中取得的expressionFactory對象。而JspApplicationContext是由Tomcat的StandardClassLoader類加載的,在類JspApplicationContext中聲明的expressionFactory字段自然也是由StandardClassLoader類加載的。現在兩個由不同類加載器加載的同一個對象要進行引用操作,自然不能通過JVM的驗證了。
    總而言之,就是說JVM在引用其它類的字段,或者調用其它類的方法時,將進行類型驗證。驗證包括,字段的類型驗證,方法的返回類型驗證,方法參數類型驗證等。驗證的內容就驗證在調用方和被調用方時,同一個類的加載器是否一致。即在調用方時,記錄的字段(參數)類型的加載器與被調用方法記錄的字段(參數)類型的加載器是否一致。如果不一致,自然就不會被JVM驗證通過。

ref:http://www.iflym.com/index.php/code/understand-jvm-load-constraint.html

 

上面這篇blog和我在項目中遇到的問題是一致的,我們在項目中需要對舊版本的Class對象就行替換,之前的做法僅僅是把Impl中值給替換了,之后在debug過程中發現這樣是不夠的,因為在tuscany的加載過程中它會對具體的implementation實現進行Introspection來檢查這個實現中有哪些Reference、Service等等,它會將Reference的字段保存下來,然后在運行過程中通過相應的Injector來進行注入,一開始的做法,我們是將Injector里面保存的method和field用新版本的給替換(因為我們發現tuscany里面的注入的具體實現是通過反射來實現的),這樣改完之后運行時就出現了上面blog中出現的問題。

仔細分析了下錯誤原因,我們發現tuscany通過反射注入的值是舊版本的,而我們的method、field對象都是新版本的,這樣就會出現IllegalArgumenException錯誤,分析之后得出結論:tuscany用來生成注入值所使用的字段接口仍然是舊版本的,也就是說我們的替換不完全,通過對WireObjectFactory中保存的interfaze的替換,將舊版本從中移除,這樣反射時就不會出錯了

 

在調試過程中還遇到另外一個問題,由於field是private類型的,當我們需要對它進行注入時取消java語言訪問控制檢查

1 newField.setAccessible(true);

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM