使用 PowerMock 时类型转换异常问题

简介: 本文主要从源码层面分析使用 PowerMock 时,偶尔会出现的向上转型报错的原因

在使用 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);
        }
    }
    // 省略其他代码
}

也就是说, AESCipherCipherSpi 的子类,那为什么向上转型会失败呢?

@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() ,可以看到此处的 CipherJavassistMockClassLoader 加载的,这是一个 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

也就是说,在这里的 AESCipherCipher 都是 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:

  1. system classes. They are deferred to system classloader
  2. 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 支持两种不同的动态代理:

  1. JDK 动态代理,通过生成一个代理类对象,拦截所有的调用,处理后转发调用到被代理对象的方法
  2. 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;
    }

参考文献:

  1. 《深入理解Java虚拟机》
  2. mocking-static-methods-in-java-system-classes
相关文章
|
4月前
|
IDE Java 测试技术
单元测试问题之Mockito 3.4mock静态方法如何解决
单元测试问题之Mockito 3.4mock静态方法如何解决
115 1
|
5月前
|
Java 测试技术 API
详解单元测试问题之Mockito的注入过程如何解决
详解单元测试问题之Mockito的注入过程如何解决
104 1
|
4月前
|
测试技术
如何使用 JUnit 测试方法是否存在异常
【8月更文挑战第22天】
72 0
|
5月前
|
测试技术
详解单元测试问题之Mockito中@Mock注解的执行步骤如何解决
详解单元测试问题之Mockito中@Mock注解的执行步骤如何解决
59 2
|
5月前
|
Java 编译器
Java编译器注解运行和自动生成代码问题之指定一个注解处理器处理所有类型的注解的问题如何解决
Java编译器注解运行和自动生成代码问题之指定一个注解处理器处理所有类型的注解的问题如何解决
|
5月前
|
Java 编译器
Java编译器注解运行和自动生成代码问题之运行时注解问题如何解决
Java编译器注解运行和自动生成代码问题之运行时注解问题如何解决
|
Java 编译器 数据库连接
深入了解数据校验(Bean Validation):基础类打点(ValidationProvider、ConstraintDescriptor、ConstraintValidator)【享学Java】(中)
深入了解数据校验(Bean Validation):基础类打点(ValidationProvider、ConstraintDescriptor、ConstraintValidator)【享学Java】(中)
深入了解数据校验(Bean Validation):基础类打点(ValidationProvider、ConstraintDescriptor、ConstraintValidator)【享学Java】(中)
|
前端开发
2021-08-12参数绑定,类型转换,数据校验,处理异常
2021-08-12参数绑定,类型转换,数据校验,处理异常
41 0