• 自作聪明的junit.swingui.TestRunner


    问题
    在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/%3C3E1F1A31.2000605@attbi.com%3E

    author: emu(黄希彤)
  • 相关阅读:
    jenkins
    k8s 驱逐限制
    jenkins+k8s 实现持续集成
    镜像更新备份
    【工具分享】2020年4月 phpstorm2020.1 for mac
    【排坑】mac安装homebrew会遇到的各种问题解决方案
    记一次C盘扩容
    2018夏季工作台之再设计
    left join后面加上where条件浅析
    编程随想篇(2018夏)
  • 原文地址:https://www.cnblogs.com/stonehuang/p/6603241.html
Copyright © 2020-2023  润新知