使用 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
相关文章
深入探究Camunda监听器
执行监听器与任务监听器
2171 1
深入探究Camunda监听器
|
SQL 监控 Java
Springboot整合p6spy实现sql监控
Springboot整合p6spy实现sql监控
958 0
|
12月前
|
开发工具 Android开发 iOS开发
flutter 环境配置
flutter 环境配置
2465 162
|
SQL 安全 数据库
已成功与服务器建立连接 但是在登录过程中发生错误。 provider 共享内存提供程序 error 0 管道的另一端上无任何进程。
用户 'sa' 登录失败。该用户与可信 SQL Server 连接无关联。  说明: 执行当前 Web 请求期间,出现未处理的异常。
4840 0
|
Java 测试技术 Maven
Java一分钟之-PowerMock:静态方法与私有方法测试
通过本文的详细介绍,您可以使用PowerMock轻松地测试Java代码中的静态方法和私有方法。PowerMock通过扩展Mockito,提供了强大的功能,帮助开发者在复杂的测试场景中保持高效和准确的单元测试。希望本文对您的Java单元测试有所帮助。
2305 2
|
存储 安全 Java
解密SimpleDateFormat类的线程安全问题和六种解决方案!
提起SimpleDateFormat类,想必做过Java开发的童鞋都不会感到陌生。没错,它就是Java中提供的日期时间的转化类。这里,为什么说SimpleDateFormat类有线程安全问题呢?有些小伙伴可能会提出疑问:我们生产环境上一直在使用SimpleDateFormat类来解析和格式化日期和时间类型的数据,一直都没有问题啊!接下来,我们就一起看下在高并发下SimpleDateFormat类为何会出现安全问题,以及如何解决SimpleDateFormat类的安全问题。
2197 1
解密SimpleDateFormat类的线程安全问题和六种解决方案!
|
XML 数据格式
XML Schema 复杂元素类型详解:定义及示例解析
在XML Schema(XSD)中,复杂元素包含其他元素和/或属性,分为4类:空元素、仅含元素、仅含文本和既含元素也含文本。定义复杂元素可通过直接声明或引用预定义的复杂类型。复杂空元素仅含属性,而仅含元素的类型则只包含其他子元素。XSD提供了`&lt;xs:sequence&gt;`、`&lt;xs:all&gt;`、`&lt;xs:choice&gt;`等指示器来规定元素顺序和出现次数,以及`&lt;xs:attributeGroup&gt;`和`&lt;xs:group&gt;`来组织元素和属性。
493 7
|
敏捷开发 测试技术 持续交付
【git分支管理策略】如何高效的管理好代码版本
【git分支管理策略】如何高效的管理好代码版本
1425 0
|
SQL OLAP 数据库
深入OceanBase内部机制:资源隔离实现的方式总结
深入OceanBase内部机制:资源隔离实现的方式总结