太长不看版:

  1. ClassLoader不同,instanceof恒定为false
  2. ClassLoader不同,子类不能强转为父类。
  3. PowerMock会使用自定义的MockClassLoader来加载一些类。为什么要这么做,目前没有深究。推测是为了实现@PrepareForTest注解的功能而做这个处理的。
  4. 使用@PowerMockIgnore注解就可以禁止MockClassLoader加载指定的类(包)

图片

问题描述

在这样的一个单元测试类中,我遇到了一个非常诡异的问题:

/**
 * The type XwIcuBizSkeleton4RegOnlyImplTest.
 *
 * @author Loy
 */
  @RunWith(PowerMockRunner.class)
  @PowerMockIgnore({"javax.management.*"})
  @PrepareForTest(SimpleHttpClient.class)
  public class XwIcuBiz4RegOnlyImplTest {
     // 略
  }

在运行时它会抛出这个异常:

Caused by: java.lang.ClassCastException: sun.management.OperatingSystemImpl cannot be cast to com.sun.management.OperatingSystemMXBean
at com.xxx.SystemDefaultMetricRegistry.appendJvm(SystemDefaultMetricRegistry.java:29)
at com.xxx.SystemDefaultMetricRegistry.(SystemDefaultMetricRegistry.java:17)

直接原因,是代码中使用了这样一行代码来做报警处理:

Monitor
    .fatal("ICU.RegisterOnly.REG_RESULT_REJECTED")
    .addTag("methodName", "BaseXwIcuBizSkeleton4PollImpl.afterPoll")
    .count(1);

这一行代码七拐八绕地,调用到了这一行代码:

final com.sun.management.OperatingSystemMXBean operatingSystemMXBean 
    = (com.sun.management.OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();

这也就是上面异常中所指向的、做了强制类型转换的地方。

但是吊诡的是,异常信息中提到的两个类(sun.management.OperatingSystemImplcom.sun.management.OperatingSystemMXBean),是有继承关系的:OperatingSystemImpl实现了OperatingSystemMXBean接口,为什么子类无法强转为父类?

class OperatingSystemImpl 
extends BaseOperatingSystemImpl 
implements OperatingSystemMXBean {...}

追查原因

用断点跟踪到抛出异常的那行代码,借助IDEA的Evaluate Expression工具,我首先确定了几件事情:

  1. ManagementFactory.getOperatingSystemMXBean() 返回的的确是OperatingSystemImpl 的实例。

  2. ManagementFactory.getOperatingSystemMXBean() instanceof com.sun.management.OperatingSystemMXBean,返回值为false

  3. ManagementFactory.getOperatingSystemMXBean() instanceof sun.management.OperatingSystemImpl,返回值为true

然后,我到技术群里问了一嘴:

我:两个class不在同一个classLoader下的话,instanceof是不是一定是false? 大佬:我印象中是

于是我又跑了两个表达式:

  1. sun.management.OperatingSystemImpl.class.getClassLoader(),返回值为null

  2. com.sun.management.OperatingSystemMXBean.class.getClassLoader(),返回值为MockClassLoader@xxxx,xxxx是一串数字。这里的MockClassLoader,是org.powermock.core.classloader.MockClassLoader

综合上面五个表达式的运行结果,原因就很清楚了:这两个class,一个是在单元测试启动时,由BootStrap ClassLoader加载的;另一个是在运行到一半时,由PowerMock自定义的MockClassLoader加载的。ClassLoader不一样,导致了子类无法强转为父类。

解决方案

那么,要怎么解决这个问题呢?MockClassLoader的javadoc里有这样的说明:

The classloader loads and modified all classes except:

  1. system classes. They are deferred to system classloader
  2. classes that locate in packages that specified as packages to ignore with using addIgnorePackage(String…)

渣翻译下:

这个ClassLoader会加载并修改所有的类,除了以下两种类之外:

  1. 系统类。这些类会推给系统ClassLoader加载,而不会被MockClassLoader加载。
  2. 通过addIgnorePackage(String...)方法来指定了要忽略某个包,那么这个包下的类都不会被MockClassLoader加载。

所以,如果能告诉PowerMock某个类/包应该被MockClassLoader给ignore掉,这个问题应该就可以得到解决了。所谓七步之内必有解药,在MockClassLoader的包里看一圈,就能看到PowerMockIgnore这个注解,它的javadoc里也说明了:

This annotation tells PowerMock to defer the loading of classes with the names supplied to value() to the system classloader.

也渣翻译下:

这个注解的作用就是告诉PowerMock:在value中声明的类(包)应该推给系统的ClassLoader来处理。

so……在类开头加上@PowerMockIgnore({"javax.management.*", "com.sun.management.*", "sun.management.*"})就好了。

总结一下

  1. ClassLoader不同,instanceof恒定为false
  2. ClassLoader不同,子类不能强转为父类。

PowerMock会使用自定义的MockClassLoader来加载一些类。为什么要这么做,目前没有深究。推测是为了实现@PrepareForTest注解的功能而做这个处理的。 但就是它的这个特性,引发了ClassCastException。 使用@PowerMockIgnore注解就可以禁止MockClassLoader加载指定的类(包)。这样就可以把这些类都交给系统的ClassLoader,从而解决前面说的异常。