Mockito 实现原理(3):如何对 final 类进行 mock

周育
2023-12-01

背景

前面两篇提到,Mockito 默认基于创建派生类(subclass)来实现 mock(包括 spy)。

那么问题来了,如果我的类标记为 final,明确禁止创建派生类,那不就没法 mock 了吗?

为了解决这个问题,Mockito 2 中引入了 InlineByteBuddyMockMaker。和前面讨论过的默认的 SubclassByteBuddyMockMaker 相比,这个 InlineByteBuddyMockMaker 同样基于 Byte Buddy 这个提供 Java 字节码操作功能的第三方库,但会尽量不通过创建派生类来实现 mock。

(注:本文基于 Mockito 4.6.1 源码

方法

正常方法

对 final 类进行 mock,需要用 InlineByteBuddyMockMaker 替换掉默认的 SubclassByteBuddyMockMaker

替换方法是通过创建一个配置文件。按照这篇教程,应该是在 src/test/resources/mockito-extensions 这个目录下,创建一个名为 org.mockito.plugins.MockMaker 的文件(这个名字其实就是 MockMaker 接口,我们其实就是在为这个接口指定一个实现,否则就会用默认的 SubclassByteBuddyMockMaker 实现了),然后在这个文件里写入:

mock-maker-inline

或者(下面这个是我在源码注释中看到的,其实就是我们要使用的实现类):

org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker

阅读源码时可以使用的方法

不幸的是,我用这个方法暂时还没有成功。但因为我是在研究 Mockito 的源码,所以我直接修改了源码中的这个文件:

// org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java
// 第 29 行开始
DEFAULT_PLUGINS.put(
		MockMaker.class.getName(),
		"org.mockito.internal.creation.bytebuddy.ByteBuddyMockMaker"); // 改之前

把最下面一行改成了:

// org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java
// 第 29 行开始
DEFAULT_PLUGINS.put(
		MockMaker.class.getName(),
		"org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker"); // 改之后

这样也实现了替换效果。

什么情况下可以不创建派生类

这个 InlineByteBuddyMockMaker 也不是万能的。在某些情况下,不创建派生类是行不通的,于是它本身会使用 SubclassByteBuddyMockMaker 作为兜底。

这些必须要创建派生类的情况包括:

  • 被 mock 的类是抽象类;
  • mock 时,设置了要额外实现的接口(这是 Mockito 的一个功能);
  • mock 时,显性设置其支持序列化;

正常情况下我们就是直接 mock,不会设置什么要额外实现的接口或者序列化之类的。所以不用担心,我们的 final 类(或方法)通常来说就是可以 mock 的!

原理

InlineByteBuddyMockMaker 基于 Java Instrumentation API ,这是 Java 提供的一个可以像现有的已编译的类添加字节码的功能。

具体在源码中的体现:

// org/mockito/internal/creation/bytebuddy/InlineBytecodeGenerator.java
// 第 371 行
@Override
public byte[] transform(
        ClassLoader loader,
        String className,
        Class<?> classBeingRedefined,
        ProtectionDomain protectionDomain,
        byte[] classfileBuffer) {
    if (classBeingRedefined == null
            || !mocked.contains(classBeingRedefined)
                    && !flatMocked.contains(classBeingRedefined)
            || EXCLUDES.contains(classBeingRedefined)) {
        return null;
    } else {
        try {
            return byteBuddy
                    .redefine(
                            classBeingRedefined,
                            //        new ClassFileLocator.Compound(
                            ClassFileLocator.Simple.of(
                                    classBeingRedefined.getName(), classfileBuffer)
                            //            ,ClassFileLocator.ForClassLoader.ofSystemLoader()
                            //        )
                            )
                    // Note: The VM erases parameter meta data from the provided class file
                    // (bug). We just add this information manually.
                    .visit(new ParameterWritingVisitorWrapper(classBeingRedefined))
                    .visit(mockTransformer)
                    .make()
                    .getBytes();
        } catch (Throwable throwable) {
            lastException = throwable;
            return null;
        }
    }
}

这个 transform 实现的是 java.lang.instrument.ClassFileTransformer 接口中的同名方法,总之就是对现有类的字节码进行修改,然后重新加载这个类。transform 就是用来“对现有类的字节码进行修改”的钩子方法。

不出意外,transform 也是借助了 Byte Buddy 实现了字节码修改,所以源码中没有体现出来很复杂的东西,真正复杂的地方都在 Byte Buddy 里了。

这里我还没完全看懂,反正无论如何,和前两篇讲到的 SubclassByteBuddyMockMaker 差不多,Mockito 会“植入”一个拦截器,这样你在调用 mock 对象的任何方法时都会走到这个拦截器里。这个拦截器会记录每次调用的信息,可以设置预期返回结果,等等。

小结

一个问题是,既然 InlineByteBuddyMockMaker 本身以 SubclassByteBuddyMockMaker 作为兜底,又增加了对 final 类/方法的支持,为什么不用它作为默认的 MockMaker 实现呢?还非得通过复杂的配置来切换。

原因大概是,InlineByteBuddyMockMaker 会对类本身进行修改,这不是一件好事,如果处理不当可能会带来意外问题。所以默认情况下,Mockito 是不鼓励用 InlineByteBuddyMockMaker 的,考虑到对 final 类/方法进行 mock 的需求不大,需要通过配置来实现也在情理之中了。

 类似资料: