问题
在junit.swingui.TestRunner的时候发现TestRunner启动过程中报错:
log4j:ERROR A "org.apache.log4j.ConsoleAppender" object is not assignable to a "org.apache.log4j.Appender" variable.
同时也发现一个平时工作正常的类在使用junit.swingui.TestRunner进行测试的时候报告一个奇怪的 ClassCastException,明明构造的对象的类是实现了指定的接口的,可是就是无法造型到接口上。
进一步研究发现,即使造型回原来的类也不行,虽然调试的时候显示构造的对象就是指定的类,但是就是无法造型成这个类,一度认为是妖人作祟或者机子被落了降头。
研究
求得庄老大再次出手,一下指出指出问题在于不同的类装载器装载的类无法相互造型的。于是进去junit.swingui.TestRunner里面去找类装载器,一翻折腾之后终于找到:
junit.runner.BaseTestRunner
|------junit.swingui.TestRunner
|------junit.textui.TestRunner
在BaseTestRunner里面定义了这样一个方法:
public TestSuiteLoader getLoader() {
if (useReloadingTestSuiteLoader())
return new ReloadingTestSuiteLoader();
return new StandardTestSuiteLoader();
}
不过注意到junit.textui.TestRunner是不会出上面的错误的,因为它自己重载了getLoader()方法,
/**
* Always use the StandardTestSuiteLoader. Overridden from
* BaseTestRunner.
*/
public TestSuiteLoader getLoader() {
return new StandardTestSuiteLoader();
}
但是junit.swingui.TestRunner就很自作聪明了,为了避免每次在点“run”按钮的时候装载运行器本身,就直接使用了基类的方法去获取装载器),这样基类就可以调用自己的getLoader方法来决定要启用那个classloader:
public TestSuiteLoader getLoader() {
if (useReloadingTestSuiteLoader())
return new ReloadingTestSuiteLoader();
return new StandardTestSuiteLoader();
}
如果我们用sun的jdk的话,这个方法会返回一个TestCaseClassLoader对象,而这个对象在装载class的时候总是调用creatLoader方法:
protected TestCaseClassLoader createLoader() {
return new TestCaseClassLoader();
}
返回的其实是TestCaseClassLoader。这样如果被测试类使用了log4j的话,会造成org.apache.log4j.Appender类被 sun.misc.Launcher$AppClassLoader(也就是sun.misc.Launcher类的嵌入类AppClassLoader)装载一次(在启动test的过程中vm自动装载被引用到的类),然后在运行的时候又被junit.runner.TestCaseClassLoader再装载一次。由两个装载器装载进来的类不管是不是来自同一个.class文件,都会被认为是两个不同的类。因此就造成了上面的错误。
同样的,如果你在自己的代码里面这样装载类:
MyClass myClass = (MyClass)Thread.currentThread().getContextClassLoader().loadClass(mClassName);
也会造成相同的问题并抛出ClassCastException。因为MyClass是在运行测试的过程由junit.runner.TestCaseClassLoader装载的,而Thread.currentThread().getContextClassLoader()却指向的是sun.misc.Launcher$AppClassLoader。
解决方法
1 java -Dlog4j.ignoreTCL junit.swingui.TestRunner
我猜TCL是ThreadClassLoader的缩写,这个参数的意思大概就是让log4j忽略Thread自己的类装载器(sun.misc.Launcher$AppClassLoader),改而使用当前Class的装载器(junit.runner.TestCaseClassLoader)来装载。但是这个方法只能解决log4j的错误报告(改变了org.apache.log4j.ConsoleAppender的装载方式),但是对我们自己写的代码中的问题却没有作用。
2 在我们自己的类里面写上一段静态代码:
static{
Thread.currentThread().setContextClassLoader(MyClassFactory.class.getClassLoader());
}
和方法一类似,这也是在工厂类中用加载了当前lass的装载器(TestCaseClassLoader)来代替Thread的初始化装载器sun.misc.Launcher$AppClassLoader。这个方法可以解决我们自己代码中的问题,并且不会带来影响原来的其他代码。结合第一种方法可以解决上面的两个问题。但是如果你有好几个工厂类,或者你用的其他包里面用了这样的装载方式……那你还可以试试下面的偏门:
3 注意到BaseTestRunner要进行一个useReloadingTestSuiteLoader()判断才决定返回哪个装载器
public TestSuiteLoader getLoader() {
if (useReloadingTestSuiteLoader())
return new ReloadingTestSuiteLoader();
return new StandardTestSuiteLoader();
}
我们来看看这个判断过程:
protected boolean useReloadingTestSuiteLoader() {
return getPreference("loading").equals("true") && !inVAJava() && fLoading;
}
嗯,里面有个inVAJava()是什么玩意儿?
public static boolean inVAJava() {
try {
Class.forName("com.ibm.uvm.tools.DebugSupport");
}
catch (Exception e) {
return false;
}
return true;
}
原来它是想判断如果当前使用的是ibm的虚拟机就使用默认装载器,但是判断的条件也忒简单了点,很容易就吧它给蒙过去了:
在当前工程下创建com.ibm.uvm.tools包,在其中创建DebugSupport类:
package com.ibm.uvm.tools;
public class DebugSupport{}
没有错,就这个空白的类,这样就可以把junit.swingui.TestRunner给蒙倒。这样做据说的副作用是,每次点run按钮的时候,都要重起gui环境,但是我没有发现有什么区别。不过要是没有区别,人家又干吗费那么多事呢?不解。
4 excluded.properties
以前没有注意到,junit有一个配置文件可以配置不需要用TestCaseClassLoader装载的包和类,多亏网友linux_china提醒,现在有了第四个解决方法:
在工程下建立junit.runner包,然后在包里面放 excluded.properties文件,内容象这样:
#
# The list of excluded package paths for the TestCaseClassLoader
#
excluded.0=sun.*
excluded.1=com.sun.*
excluded.2=org.omg.*
excluded.3=javax.*
excluded.4=sunw.*
excluded.5=java.*
excluded.6=org.w3c.dom.*
excluded.7=org.xml.sax.*
excluded.8=net.jini.*
excluded.9=org.apache.log4j.*
excluded.10=emu.*
......
这样junit读到这些包的时候就不会用TestCaseClassLoader去装载他们了。
参考资料
http://mail-archives.apache.org/mod_mbox/logging-log4j-user/200301.mbox/%[email protected]%3E
author: emu(黄希彤)