同一个java类由不同的classloader加载问题
最近在测试项目代码中遇到同一个类由不同的classloader加载后出现的问题:
- 类A中有一个字段a,它的类型为X
- 类B中有一个字段b,它的类型也为X
- 类A由classLoaderA所加载,类B由classLoaderB所加载
- 执行赋值语句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);