在使用 PowerMock 测试代码时,偶尔会遇到一个类型转换错误,例如下面的代码
@RunWith(PowerMockRunner.class)
public class AESUtilTest {
@Test
public void test() throws Exception {
// 加密
String enString = AESUtil.Encrypt(cSrc, cKey);
}
}
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class AESUtil {
public static String Encrypt(String sSrc, String sKey) throws Exception {
// .. 网上有很多例子,这里就省略了
}
}
在运行时会报错:
java.lang.ClassCastException: com.sun.crypto.provider.AESCipher$General cannot be cast to javax.crypto.CipherSpi
at javax.crypto.Cipher.chooseProvider(Cipher.java:863)
at javax.crypto.Cipher.init(Cipher.java:1252)
at javax.crypto.Cipher.init(Cipher.java:1189)
在网上很容易就可以搜到对应的解决方法:在测试类的开头,添加 @PowerMockIgnore("javax.crypto.*")
,就可以了。
但是,如果打开 com.sun.crypto.provider.AESCipher
的源码:
package com.sun.crypto.provider;
import java.security.*;
import java.security.spec.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.crypto.BadPaddingException;
import java.nio.ByteBuffer;
abstract class AESCipher extends CipherSpi {
public static final class General extends AESCipher {
public General() {
super(-1);
}
}
// 省略其他代码
}
也就是说, AESCipher
是 CipherSpi
的子类,那为什么向上转型会失败呢?
@PowerMockIgnore 的作用
在 @PowerMockIgnore
源码开头,有这么一句注释:
This annotation tells PowerMock to defer the loading of classes with the names supplied to value() to the system classloader.
也就是说,@PowerMockIgnore
中配置的类,会使用 system classloader 来加载。
而在 Java 程序中,一个类需要由加载它的类加载器,和这个类本身一同确立在 JVM 中的唯一性。也就是说,即使两个类来自同一个 .class 文件,只要加载的类加载器不同, 那么这两个类就不相等。因此,有可能是由于类加载器导致的类型转换异常。
源码分析
为了验证上面的猜测,根据报错的日志,我们跳转到对应报错的位置Cipher.java:863
:
// 此处省略其他代码
if (thisSpi == null) {
// 报错的转换就在这里
// 在这里打个断点
thisSpi = (CipherSpi)s.newInstance(null);
}
在返回语句处con.newInstance()
打个断点,我们用 idea 的 evaluate 功能,直接调用 CipherSpi.class.getClassLoader()
,可以看到此处的 Cipher
是 JavassistMockClassLoader
加载的,这是一个 PowerMock 的类加载器 。
重启,这次我们跟踪进入 s.newInstance
方法,发现代码执行到 java.security.Provider:1595
附近:
public static final Cipher getInstance(String transformation)
throws NoSuchAlgorithmException, NoSuchPaddingException
{
// 此处省略其他代码
// 在此处打断点可以看到,clazz = AESCipher.General.class
Class<?> clazz = getImplClass();
Class<?>[] empty = {};
Constructor<?> con = clazz.getConstructor(empty);
return con.newInstance();
} else {
// 此处省略其他代码
我们用 idea 的 evaluate 功能,直接调用 AESCipher.General.class.getClassLoader()
,可以看到,加载 AESCipher.General
的是 ExtClassLoader
。注意到 AESCipher.General extends AESCipher
,我们直接调用 AESCipher.General.getSuperclass().getSuperclass()
,可以看到 AESCipher
的父类正是Cipher
,但是加载这个 Cipher
的类加载器是 ExtClassLoader
,而不是上面的 JavassistMockClassLoader
。
也就是说,在这里的 AESCipher
和 Cipher
都是 ExtClassLoader
加载进来的。
那 JavassistMockClassLoader
为什么不顺手加载一下 AESCipher
呢?我们继续看
getImplClass()
:
private Class<?> getImplClass() throws NoSuchAlgorithmException {
try {
Reference<Class<?>> ref = classRef;
Class<?> clazz = (ref == null) ? null : ref.get();
if (clazz == null) {
// 打断点可以看到这里的 cl 是 ExtClassLoader,也就是之前提到的“system classloader”
ClassLoader cl = provider.getClass().getClassLoader();
if (cl == null) {
clazz = Class.forName(className);
} else {
// 通过打断点可以发现,AESCipher.class 就是这里加载进来的
// 并且在加载过程中,Cipher 作为 AESCipher 的依赖,也被 ExtClassLoader 加载进来了
clazz = cl.loadClass(className);
}
// 省略后续代码
}
return clazz;
} catch (ClassNotFoundException e) {
// 省略异常处理代码
}
}
我们再看一下这个 provider
的源码,可以发现这是java.security.Provider
。那么 Provider
为什么是 ExtClassLoader
加载进来的?JavassistMockClassLoader
为什么不去加载Provider
?
在 JavassistMockClassLoader
的源码中,没有 LoadClass
方法;在它的父类 MockClassLoader
中,开头的注释有这么两行:
The classloader loads and modified all classes except:
- system classes. They are deferred to system classloader
- classes that locate in packages that specified as packages to ignore with using MockClassLoaderConfiguration.addIgnorePackage(String...)
这里 system classloader 又出现了;但还是没有 loadClass(String)
方法。再看 MockClassLoader
的父类 DeferSupportingClassLoader
,我们终于找到了入口:
/** DeferSupportingClassLoader 没有复写 ClassLoader 的 loadClass 方法
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = findLoadedClass1(name);
if (clazz == null) {
clazz = loadClass1(name, resolve);
}
return clazz;
}
}
private Class<?> loadClass1(String name, boolean resolve) throws ClassNotFoundException {
Class<?> clazz;
// 这个 shouldDefer 决定了是用 system classloader 加载,还是用 PowerMock 的类加载器加载
if (shouldDefer(name)) {
clazz = loadByDeferClassLoader(name);
} else {
clazz = loadClassByThisClassLoader(name);
}
if (resolve) {
resolveClass(clazz);
}
classes.put(name, new SoftReference<Class<?>>(clazz));
return clazz;
}
// 省略无关代码
private boolean shouldDefer(String name) {
// 这个 Configuration 又是啥?
return configuration.shouldDefer(name);
}
在 Configuration
类中,我们终于找到了最后的答案:Provider
是由 system classloader
加载的,但com.sun.crypto.provider.Cipher
不是,需要手动添加注解@PowerMockIgnore
。
public class MockClassLoaderConfiguration {
/*
* Classes that should always be deferred regardless of what the user
* specifies in annotations etc.
* 这里包括了所有必须由 system classloader 加载的类
* 主要是 java 自带的类,还有 TestNG,JUnit,
* 以及 PowerMock 自身依赖等测试框架类
*/
static final String[] PACKAGES_TO_BE_DEFERRED = new String[]{
"org.hamcrest.*",
"jdk.*",
"java.*", // java.security.Provider 正好在这个 package 里面
"javax.accessibility.*",
"sun.*",
"org.junit.*",
"org.testng.*",
"junit.*",
"org.pitest.*",
"org.powermock.modules.junit4.common.internal.*",
"org.powermock.modules.junit3.internal.PowerMockJUnit3RunnerDelegate*",
"org.powermock.core*",
"org.jacoco.agent.rt.*"
};
// 而且 com.sun.crypto.provider.Cipher 并不在这个列表中
// 省略后续代码
}
为什么 PowerMock 需要定义一个自己的 ClassLoader ?
看完上面的分析,我们会发现,这个类型转换异常的问题,其实是由于 PowerMock 自己的 ClassLoader 导致的。那为什么 PowerMock 要定义一个自己的 ClassLoader ?
在给出答案之前,我们考虑一个问题:如何 Mock 一个类的方法?Mock 方法的本质就是替换一个方法的实现,在这方面已经有一个非常典型的例子:Spring 的动态代理。Spring 支持两种不同的动态代理:
- JDK 动态代理,通过生成一个代理类对象,拦截所有的调用,处理后转发调用到被代理对象的方法
- Cglib 动态代理,通过生成一个代理类的子类,覆写父类的方法
但是这两种方法都无法 Mock 静态方法,因为静态方法是定义在类中的,与类的实例无关。所以,要修改一个静态方法,只能修改类的定义。
但是,PowerMock 还支持 @Spy
注解,也就是说,PowerMock 不能凭空生成一个类的定义,因此,PowerMock 需要读取原来的类的定义。因此,可行的方法看来只有一个:自定义一个 ClassLoader,加载需要 Mock 的类,修改类的定义,然后重新加载。
在上面 DeferSupportingClassLoader
的源码中,有一行 clazz = loadClassByThisClassLoader(name);
,`MockClassLoader
` 覆写了这个方法。我们看一下这个方法里面到底做了什么事情:
// MockClassLoader
@Override
protected Class<?> loadClassByThisClassLoader(String className) throws ClassFormatError, ClassNotFoundException {
final Class<?> loadedClass;
// 先加载原本的类
Class<?> deferClass = deferTo.loadClass(className);
// 判断是否需要对类进行修改,并重新加载这个类
if (getConfiguration().shouldMockClass(className)) {
loadedClass = loadMockClass(className, deferClass.getProtectionDomain());
} else {
loadedClass = loadUnmockedClass(className, deferClass.getProtectionDomain());
}
return loadedClass;
}
参考文献:
- 《深入理解Java虚拟机》
- mocking-static-methods-in-java-system-classes