Mockito 详解(二)插件机制

Mockito 通过插件形式对外提供了扩展能力,本篇主要分析其插件加载原理。

注册插件

插件通过 PluginRegister 进行注册,现在只支持四个组件,分别是:

  • MockMaker
  • StackTraceCleanerProvider
  • InstantiatorProvider
  • AnnotationEngine

PluginRegistry 是 package 级别的 class,其初始化的组件通过 public 级别的类 Plugins 对外提供:

public class Plugins {
  private static final PluginRegistry registry = new PluginRegistry();

  public static StackTraceCleanerProvider getStackTraceCleanerProvider() {
    return registry.getStackTraceCleanerProvider();
  }

  public static MockMaker getMockMaker() {
    return registry.getMockMaker();
  }

  public static InstantiatorProvider getInstantiatorProvider() {
    return registry.getInstantiatorProvider();
  }

  public static AnnotationEngine getAnnotationEngine() {
    return registry.getAnnotationEngine();
  }
}

Mockito 提供了默认的插件。

  • AnnotationEngine
    • org.mockito.internal.configuration.InjectingAnnotationEngine
  • InstantiatorProvider
    • org.mockito.internal.creation.instance.DefaultInstantiatorProvider
  • MockMaker
    • Default = org.mockito.internal.creation.bytebuddy.ByteBuddyMockMaker
    • Inline = org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker
    • Android = org.mockito.android.internal.creation.AndroidByteBuddyMockMaker
  • StackTraceCleanerProvider
    • org.mockito.internal.exceptions.stacktrace.DefaultStackTraceCleanerProvider

加载插件

PluginLoader 负责加载插件。

需要深入了解 ClassLoader 机制。

构造

private final PluginSwitch pluginSwitch;
private final Map<String, String> alias;

public PluginLoader(PluginSwitch pluginSwitch) {
    this.pluginSwitch = pluginSwitch;
    this.alias = new HashMap<String, String>();
}
  • pluginSwitch 用于是否需要加载组件。(后面会分析)
  • alias 表示插件别名,可以用一个简单的名字表示插件的全称(fully qualified type name)。
PluginLoader withAlias(String name, String type) {
    alias.put(name, type);
    return this;
}

加载插件

<T> T loadPlugin(final Class<T> pluginType, String defaultPluginClassName)

loadPlugin 方法接受两个参数,一个是插件Class,另一个是默认插件名称。

首先尝试加载 pluginType,如果加载成功,直接返回插件实例。

T plugin = loadImpl(pluginType)
if (plugin != null) {
    return plugin;
}

loadImpl 方法的声明如下:

/**
  * Equivalent to {@link java.util.ServiceLoader#load} but without requiring
  * Java 6 / Android 2.3 (Gingerbread).
  */
private <T> T loadImpl(Class<T> service)

**1. 首先获取 ClassLoader **

ClassLoader loader = Thread.currentThread().getContextClassLoader();
if (loader == null) {
    loader = ClassLoader.getSystemClassLoader();
}

2. 然后加载 mockito-extensions 下配置的资源

Enumeration<URL> resources;
try {
    resources = loader.getResources("mockito-extensions/" + service.getName());
} catch (IOException e) {
    throw new IllegalStateException("Failed to load " + service, e);
}

以 Android 平台的 Mockito 为例

MockMaker 插件的路径为:

src/main/resources/mockito-extensions/org.mockito.plugins.MockMaker

MockMaker 插件的实现类为:

org.mockito.android.internal.creation.AndroidByteBuddyMockMaker`

那么 loader.getResources 的返回值 resources 中会包含 AndroidByteBuddyMockMaker

3. 通过 PluginFinder 寻找插件的类名

String foundPluginClass = new PluginFinder(pluginSwitch).findPluginClass(Iterables.toIterable(resources))

PluginFinder 的查询规则是:找到第一个不被 PluginSwitch 禁用掉的 plugin 类名。

PluginFinder#findPluginClass 的代码如下所示:

String findPluginClass(Iterable<URL> resources) {
    for (URL resource : resources) {
        InputStream s = null;
        try {
            s = resource.openStream();
            String pluginClassName = new PluginFileReader().readPluginClass(s);
            if (pluginClassName == null) {
                //For backwards compatibility
                //If the resource does not have plugin class name we're ignoring it
                continue;
            }
            if (!pluginSwitch.isEnabled(pluginClassName)) {
                continue;
            }
            return pluginClassName;
        } catch(Exception e) {
            throw new MockitoException("Problems reading plugin implementation from: " + resource, e);
        } finally {
            IOUtil.closeQuietly(s);
        }
    }
    return null;
}

4. 加载找到的插件

if (foundPluginClass != null) {
    String aliasType = alias.get(foundPluginClass);
    if (aliasType != null) {
        foundPluginClass = aliasType;
    }
    Class<?> pluginClass = loader.loadClass(foundPluginClass);
    Object plugin = pluginClass.newInstance();
    return service.cast(plugin);
}
  1. 因为 [插件名字] 可能是简称,所以需要尝试去 alias 寻找 [插件名字] 的 [类名]
  2. 然后利用上文获得的 loader 加载 [找到的插件类]
  3. 创建类实例,转换类型后返回

插件开关

PluginSwitch 用于判断是否需要加载 classpath 中配置的插件。

PluginSwitch 本身也是一个插件,在 PluginRegistry 中注册:

private final PluginSwitch pluginSwitch = new PluginLoader(new DefaultPluginSwitch())
    .loadPlugin(PluginSwitch.class, DefaultPluginSwitch.class.getName());

它的默认实现为 DefaultPluginSwitch

class DefaultPluginSwitch implements PluginSwitch {
  public boolean isEnabled(String pluginClassName) {
    return true;
  }
}

PluginLoader 的分析过程中,我们以 Android 平台的插件 MockMaker 为例已经稍微了解了插件的加载原理,下面再详细分析一下。

我们知道,插件通过 PluginRegistry 进行注册:

PluginRegistry.java

private final MockMaker mockMaker = new PluginLoader(pluginSwitch)
            .withAlias("mock-maker-inline", "org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker")
            .loadPlugin(MockMaker.class, "org.mockito.internal.creation.bytebuddy.ByteBuddyMockMaker");

以上代码可以看出,PluginLoader 的构造方法需要参数 pluginSwitch,也就是说 PluginLoader 可以根据 pluginSwitch 是否需要加载某个 plugin。

再来看一下 withAliasloadPlugin 的声明:

withAlias@PluginLoader.java

/**
 * Adds an alias for a class name to this plugin loader. Instead of the fully qualified type name,
 * the alias can be used as a convenience name for a known plugin.
 */
PluginLoader withAlias(String name, String type)

针对上例,withAlias 的参数对应关系如下:

  • name = “mock-maker-inline”
  • type = “org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker”

也就是说,当我们遇到名字为 mock-maker-inline 的插件时,如果没有被 pluginSwitch diasable 掉,那么就去加载 org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker 这个类。

loadPlugin@PluginLoader.java

/**
 * Scans the classpath for given pluginType. If not found, default class is used.
 */
@SuppressWarnings("unchecked")
<T> T loadPlugin(final Class<T> pluginType, String defaultPluginClassName)

还是针对上例,参数对应关系如下:

参数名 参数值
pluginType MockMaker.class
defaultPluginClassName “org.mockito.internal.creation.bytebuddy.ByteBuddyMockMaker”

直接看注释,具体原理还没有完全搞懂。

The plugin mechanism of mockito works in a similar way as the java.util.ServiceLoader, however instead of looking in the META-INF directory, Mockito will look in mockito-extensions directory. The reason for that is that Android SDK strips jar from the META-INF directory when creating an APK.

Mockito 加载插件的方式类似于 ServiceLoader,但是它是去 mockito-extensions 目录下寻找插件,而不是 META-INF

以 Android 平台为例:

├── android
│   └── src
│       └── main
│           ├── java
│           │   └── org
│           │       └── mockito
│           │           └── android
│           │               └── internal
│           │                   └── creation
│           │                       ├── AndroidByteBuddyMockMaker.java
│           │                       ├── AndroidLoadingStrategy.java
│           │                       └── AndroidTempFileLocator.java
│           └── resources
│               └── mockito-extensions
│                   └── org.mockito.plugins.MockMaker

mockito-extensions 目录下有一个文件 org.mockito.plugins.MockMaker,那么 PluginLoader 在加载 MockMaker 时会首先读取 org.mockito.plugins.MockMaker 文件的内容(类全称或者别名):

org.mockito.android.internal.creation.AndroidByteBuddyMockMaker

ClassLoader 把这个类加载进来之后,插件就加载完成了。

更新时间:

留下评论