封面来源:碧蓝航线 碧海光粼 活动CG
本文涉及的代码:java8/lambda
0. 前言
在前段时间在《Mockito 实战》一文中对静态方法的 mock 进行了补充。简单回顾下使用前的准备工作,如果导入的 Mockito 依赖是 mockito-inline
,直接使用即可;但如果导入的依赖是 mockito-core
,除了更换依赖外,还要在项目的 ClassPath 目录下新建 mockito-extensions
目录,然后创建 org.mockito.plugins.MockMaker
和 org.mockito.plugins.MemberAccessor
文件,并分别追加内容 mock-maker-inline
和 member-accessor-module
即可在 Mockito 中 mock 静态方法。
那这是怎么实现的呢?为什么创建两个特定的文件并追加特定的内容就能让 Mockito 支持 mock 静态方法?
先说结论:这利用了类似 SPI 的机制。
那 SPI 又是什么?在其他地方有体现吗?
1. Mockito 与 SPI
先简单看下为什么说 Mockito 利用 SPI 实现了静态方法的 mock。
首先创建了 org.mockito.plugins.MockMaker
和 org.mockito.plugins.MemberAccessor
两个文件,这两个文件的文件名格式似乎是类的全限定名。以 org.mockito.plugins.MockMaker
为例,搜索一下是否这个类:
1 2 3 4 5 package org.mockito.plugins;public interface MockMaker { }
能够搜到名为 MockMaker
的接口。
既然如此,使用 IDEA 的 Ctrl + Shift + F 全局搜索快捷键搜索 mock-maker-inline
应该也能有所收获。
1 2 3 4 5 6 7 8 9 10 11 12 13 class DefaultMockitoPlugins implements MockitoPlugins { private static final Map<String, String> DEFAULT_PLUGINS = new HashMap <>(); static final String INLINE_ALIAS = "mock-maker-inline" ; static { DEFAULT_PLUGINS.put( INLINE_ALIAS, "org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker" ); } }
果不其然,能够在 DefaultMockitoPlugins
类中找到上述的代码片段。
上述代码片段的内容很好理解,就是往一个 Map 中添加了一些元素,其中一个元素的 key 就是 mock-maker-inline
,而这个 key 对应的 value 似乎又是一个类的全限定名。再来搜索一下:
1 2 3 4 5 @SuppressSignatureCheck class InlineDelegateByteBuddyMockMaker implements ClassCreatingMockMaker , InlineMockMaker, Instantiator { }
再回到最开始的 MockMaker
接口,如果查看它的实现类,会发现 InlineDelegateByteBuddyMockMaker
就是它的一个实现类。
也就是说,org.mockito.plugins.MockMaker
文件的内容 mock-maker-inline
指的就是 MockMaker
接口的实现类 InlineDelegateByteBuddyMockMaker
。
按照同样的方式,全局搜索下为实现 Mock 静态方法新建的 mockito-extensions
目录名 mockito-extensions
:
最终可以在 PluginInitializer
类中找到相关代码。新建目录名 mockito-extensions
作为 ClassLoader#getResources()
方法的参数的一部分来加载对应的资源,而 loadImpl()
方法的注释也说此方法等效于 java.util.ServiceLoader#load()
方法:
1 2 3 4 5 6 7 public <T> T loadImpl (Class<T> service) { }
这里的 java.util.ServiceLoader
就是 SPI 的核心类。
简单的分析到此结束,至此基本可以知道 Mockito 实现静态方法的 mock 确实使用到了 SPI 机制。
那 SPI 究竟是什么呢?
2. SPI 的概述与使用
2.1 SPI 的概述
SPI,全称为 Service Provider Interface,是 JDK 内置的一种 服务提供发现机制 ,常用来启用框架扩展和替换组件,主要被框架的开发人员使用。SPI 机制将装配的控制权移到程序之外,这在模块化设计中尤为重要,其主要目的就是为了 解耦 。有点 IoC 的味道了,将装配的控制权移到程序外。
SPI 机制的应用场景有很多,除去前面说的 Mockito 之外,在 JDBC 中也有体现。
2.2 MySQL 与 SPI
以 MySQL 为例,MySQL 的 Java 驱动就使用了 SPI 机制,简单看下。首先导入相关依赖:
1 2 3 4 5 <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.28</version > </dependency >
导入成功后,查看 mysql-connector-java
的源码包,可以看到有个 META-INF
目录,在这个目录下有一个 services
目录,里面还有一个名为 java.sql.Driver
的文件,打开这个文件:
1 com.mysql.cj.jdbc.Driver
按照相同的方式,先全局搜索文件名 java.sql.Driver
:
1 2 3 4 5 6 7 package java.sql;import java.util.logging.Logger;public interface Driver { }
可以看到在 JDK 的 java.sql
包下存在一个全限定名为 java.sql.Driver
的接口。按照前面的思路,很容易猜到 com.mysql.cj.jdbc.Driver
应该是 java.sql.Driver
接口的实现。全局搜索一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.mysql.cj.jdbc;import java.sql.DriverManager;import java.sql.SQLException;public class Driver extends NonRegisteringDriver implements java .sql.Driver { public Driver () throws SQLException { } static { try { DriverManager.registerDriver(new Driver ()); } catch (SQLException var1) { throw new RuntimeException ("Can't register driver!" ); } } }
在导入的 mysql-connector-java
依赖中找到了 com.mysql.cj.jdbc.Driver
的具体实现。
com.mysql.cj.jdbc.Driver
内除了一个无参构造方法外,有且仅有一个静态代码块,没有其他的方法。而在静态代码块中调用了 DriverManager#registerDriver()
,进入 DriverManager
内部:
1 2 3 4 5 6 7 public class DriverManager { static { loadInitialDrivers(); println("JDBC DriverManager initialized" ); } }
在 DriverManager
内也有一个静态代码块,并在内部调用了静态方法 loadInitialDrivers()
,同时能够找到如下代码片段:
再次发现 SPI 的核心类 java.util.ServiceLoader
,不难得出 JDBC 的加载过程确实也使用到了 SPI 机制。
除此之外,还能发现 java.lang.DriverManager
位于 JDK 的 rt.jar
中,这个类将由启动类加载器进行加载,它能够加载 rt.jar
包之外的类,打破双亲委派模型。
原因是 ServiceLoader
中使用了线程上下文类加载器去加载类(后文细说)。
关于类加载器和双亲委派模型的内容,可以参考《注解、类的加载、反射》一文。
2.3 SPI 的使用示例
以 MySQL 的 Java 驱动作为示例,不难得出 SPI 的使用规律如下:
1、定义一个接口;
2、为第 1 步中定义的接口编写实现类;
3、在 ClassPath 的 META-INF/services
目录(没有目录自行创建)下创建一个文件,文件名为第 1 步定义的接口全限定名,文件内容为定义的接口的实现类的全限定名。如果有多个实现,每个实现类的全限定名占独立的一行;
4、使用 ServiceLoader#load()
加载接口,然后获取含有接口实现的迭代器。
定义一个简单的接口:
1 2 3 4 5 6 7 8 9 package indi.mofan.spi;public interface Runnable { void run () ; }
为其编写两个实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package indi.mofan.spi.impl;import indi.mofan.spi.Runnable;public class SimpleRunner implements Runnable { @Override public void run () { System.out.println("Hello, World" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package indi.mofan.spi.impl;import indi.mofan.spi.Runnable;public class DemoRunner implements Runnable { @Override public void run () { System.out.println("Hello, Mofan" ); } }
然后按照第三步的做法,新建文件并编写内容:
最后测试一下:
1 2 3 4 5 6 7 8 9 @Test public void testSPI () { ServiceLoader<Runnable> loader = ServiceLoader.load(indi.mofan.spi.Runnable.class); Iterator<Runnable> iterator = loader.iterator(); while (iterator.hasNext()) { Runnable runner = iterator.next(); runner.run(); } }
运行结果如下:
Hello, World
Hello, Mofan
根据运行结果不难看出通过 ServiceLoader
可以加载定义的实现类。
简化写法
上述的迭代器使用可以使用 Java 提供的 For-Each 语法糖,使代码更加简洁:
1 2 3 4 5 6 7 @Test public void testSPI () { ServiceLoader<Runnable> loader = ServiceLoader.load(indi.mofan.spi.Runnable.class); for (Runnable runner : loader) { runner.run(); } }
3. 加载资源的其他方式
在分析 SPI 核心类 ServiceLoader
之前,需要先简单了解下 ClassLoader
类和 Class
类中资源加载的方式,因为在 ServiceLoader
中也使用到了它们,预先了解下还是很有必要的。
3.1 ClassLoader 提供的 API
在 java.lang.ClassLoader
中,提供了如下六种加载资源的方式:
1 2 3 4 5 6 7 8 9 10 11 12 public URL getResource (String name) public Enumeration<URL> getResources (String name) throws IOExceptionpublic InputStream getResourceAsStream (String name) public static URL getSystemResource (String name) public static Enumeration<URL> getSystemResources (String name) throws IOException public static InputStream getSystemResourceAsStream (String name)
这六种加载类的方式可以分为两组,一组是实例方法,一组是静态方法。在这两组方法中,又以 getResource()
和 getSystemResource()
最具代表性。
public URL getResource(String name)
1 2 3 4 5 6 7 8 9 10 11 12 public URL getResource (String name) { URL url; if (parent != null ) { url = parent.getResource(name); } else { url = getBootstrapResource(name); } if (url == null ) { url = findResource(name); } return url; }
一股熟悉的味道,该方法使用了类似双亲委派模型的实现,双亲委派模型的相关内容可以参考 注解、类的加载、反射 一文。
通过方法的注释得知,getResource()
方法用于查找具有给定名称的资源,这些资源可以是图像、音频、文本等,资源的名称是通过 /
分割的路径名。简单使用下:
1 2 3 4 5 6 7 8 9 10 11 12 public class MyLoader { public static void main (String[] args) { ClassLoader classLoader = MyLoader.class.getClassLoader(); System.out.println(classLoader.getResource("" )); } }
很明显,输出的结果是当前 Module 的 ClassPath 路径。
也就是说,getResource()
是基于用户应用程序的 ClassPath 去搜索资源的,并且资源路径需要以 /
进行分割,并且 不能以 /
开头。 比如将上述代码修改为 classLoader.getResource("/")
后的输出结果为 null
,表示未找到指定资源。
public static URL getSystemResource(String name)
1 2 3 4 5 6 7 8 9 10 public static URL getSystemResource (String name) { ClassLoader system = getSystemClassLoader(); if (system == null ) { return getBootstrapResource(name); } return system.getResource(name); }
简单测试下:
1 2 3 4 5 public static void main (String[] args) { System.out.println(ClassLoader.getSystemResource("" )); }
总结
ClassLoader
中加载资源的核心方法是 ClassLoader#getResource(String name)
,按照双亲委派模型并基于用户应用程序的 ClassPath 路径去搜索资源,资源的名称需要使用 /
分割,并且不能以 /
作为资源名称的起始字符。
如果未找到指定的资源,返回 null
,而不是抛出异常。
3.2 Class 提供的 API
在 java.lang.Class
中也提供了两个加载资源的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public java.net.URL getResource (String name) { name = resolveName(name); ClassLoader cl = getClassLoader0(); if (cl==null ) { return ClassLoader.getSystemResource(name); } return cl.getResource(name); } public InputStream getResourceAsStream (String name) { name = resolveName(name); ClassLoader cl = getClassLoader0(); if (cl==null ) { return ClassLoader.getSystemResourceAsStream(name); } return cl.getResourceAsStream(name); }
通过比较 Class
和 ClassLoader
中资源加载的方法,前者比后者多做了一步 resolveName()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private String resolveName (String name) { if (name == null ) { return name; } if (!name.startsWith("/" )) { Class<?> c = this ; while (c.isArray()) { c = c.getComponentType(); } String baseName = c.getName(); int index = baseName.lastIndexOf('.' ); if (index != -1 ) { name = baseName.substring(0 , index).replace('.' , '/' ) +"/" +name; } } else { name = name.substring(1 ); } return name; }
不难看出这个方法对资源名称的处理分为了三种情况,当资源名称:
1、为 null
时,直接返回 null
;
2、以 /
开头时,去掉开头的 /
,基于用户应用程序的 ClassPath 路径搜索资源;
3、未以 /
开头时,以当前类所在包路径为起始路径搜索资源。比如在 indi.mofan.Main.java
中调用了 Main.class.getResource("pic.jpg")
,那么需要的加载资源路径为 indi/mofan/pic.jpg
。
4. ServiceLoader 的源码分析
4.1 构造实例的方式
先看 ServiceLoader
中的变量信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public final class ServiceLoader <S> implements Iterable <S> { private static final String PREFIX = "META-INF/services/" ; private final Class<S> service; private final ClassLoader loader; private final AccessControlContext acc; private LinkedHashMap<String,S> providers = new LinkedHashMap <>(); private LazyIterator lookupIterator; }
再看看它的构造方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public void reload () { providers.clear(); lookupIterator = new LazyIterator (service, loader); } private ServiceLoader (Class<S> svc, ClassLoader cl) { service = Objects.requireNonNull(svc, "Service interface cannot be null" ); loader = (cl == null ) ? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager() != null ) ? AccessController.getContext() : null ; reload(); }
reload()
方法在 ServiceLoader
中只被它的构造方法调用,但它的访问修饰符是 public
,因此可以使用 ServiceLoader
实例来调用这个方法以清空缓存并构造懒迭代器实例。
在 ServiceLoader
中有且仅有一个 私有 构造方法,因此是不能通过构造方法去实例化它的。当需要构造它的实例时,需要使用它内部的静态方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static <S> ServiceLoader<S> load (Class<S> service, ClassLoader loader) { return new ServiceLoader <>(service, loader); } public static <S> ServiceLoader<S> load (Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } public static <S> ServiceLoader<S> loadInstalled (Class<S> service) { ClassLoader cl = ClassLoader.getSystemClassLoader(); ClassLoader prev = null ; while (cl != null ) { prev = cl; cl = cl.getParent(); } return ServiceLoader.load(service, prev); }
load(Class<S> service, ClassLoader loader)
直接调用了仅有的构造方法,需要传入被加载类的 Class 对象和类加载器实例,而 load(Class<S> service)
只需传入被加载类的 Class 对象,默认使用线程上下文类加载器(一般情况下,指向的是应用类加载器或者说系统类加载器)。
在 loadInstalled(Class<S> service)
方法中能看到“双亲委派模型”的影子。未找到拓展类加载器时,使用系统类加载器;未找到系统类加载器时,使用启动类加载器。
4.2 迭代器方法
1 2 3 public final class ServiceLoader <S> implements Iterable <S> { }
ServiceLoader
实现了 Iterable
迭代器接口,必然会重写 iterator()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public Iterator<S> iterator () { return new Iterator <S>() { Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator(); public boolean hasNext () { if (knownProviders.hasNext()) return true ; return lookupIterator.hasNext(); } public S next () { if (knownProviders.hasNext()) return knownProviders.next().getValue(); return lookupIterator.next(); } public void remove () { throw new UnsupportedOperationException (); } }; }
iterator()
返回了 Iterator
接口的匿名实现,在判断是否存在下一个实例或获取下一个实例时,先使用缓存判断或获取,然后再使用懒迭代器判断或获取。除此之外,不支持移除操作。
那懒迭代器 LazyIterator
究竟是何方神圣?
4.3 懒迭代器详解
懒迭代器 LazyIterator
是 ServiceLoader
中的私有内部类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private class LazyIterator implements Iterator <S> { Class<S> service; ClassLoader loader; Enumeration<URL> configs = null ; Iterator<String> pending = null ; String nextName = null ; private LazyIterator (Class<S> service, ClassLoader loader) { this .service = service; this .loader = loader; } }
再看下内部的方法,方法只有四个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 private boolean hasNextService () { if (nextName != null ) { return true ; } if (configs == null ) { try { String fullName = PREFIX + service.getName(); if (loader == null ) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files" , x); } } while ((pending == null ) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false ; } pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true ; } private S nextService () { if (!hasNextService()) throw new NoSuchElementException (); String cn = nextName; nextName = null ; Class<?> c = null ; try { c = Class.forName(cn, false , loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found" ); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype" ); } try { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated" , x); } throw new Error (); } public boolean hasNext () { if (acc == null ) { return hasNextService(); } else { PrivilegedAction<Boolean> action = new PrivilegedAction <Boolean>() { public Boolean run () { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } public S next () { if (acc == null ) { return nextService(); } else { PrivilegedAction<S> action = new PrivilegedAction <S>() { public S run () { return nextService(); } }; return AccessController.doPrivileged(action, acc); } } public void remove () { throw new UnsupportedOperationException (); }
在 hasNextService()
中使用到了 ServiceLoader
中的 parse(Class<?> service, URL u)
方法,用于获取含有需要被加载的实现类的全限定名的迭代器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 private Iterator<String> parse (Class<?> service, URL u) throws ServiceConfigurationError { InputStream in = null ; BufferedReader r = null ; ArrayList<String> names = new ArrayList <>(); try { in = u.openStream(); r = new BufferedReader (new InputStreamReader (in, "utf-8" )); int lc = 1 ; while ((lc = parseLine(service, u, r, lc, names)) >= 0 ); } catch (IOException x) { fail(service, "Error reading configuration file" , x); } finally { try { if (r != null ) r.close(); if (in != null ) in.close(); } catch (IOException y) { fail(service, "Error closing configuration file" , y); } } return names.iterator(); }
在 parse()
方法中又调用了 parseLine()
,它也是 ServiceLoader
中的一个私有方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 private int parseLine (Class<?> service, URL u, BufferedReader r, int lc, List<String> names) throws IOException, ServiceConfigurationError { String ln = r.readLine(); if (ln == null ) { return -1 ; } int ci = ln.indexOf('#' ); if (ci >= 0 ) ln = ln.substring(0 , ci); ln = ln.trim(); int n = ln.length(); if (n != 0 ) { if ((ln.indexOf(' ' ) >= 0 ) || (ln.indexOf('\t' ) >= 0 )) fail(service, u, lc, "Illegal configuration-file syntax" ); int cp = ln.codePointAt(0 ); if (!Character.isJavaIdentifierStart(cp)) fail(service, u, lc, "Illegal provider-class name: " + ln); for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) { cp = ln.codePointAt(i); if (!Character.isJavaIdentifierPart(cp) && (cp != '.' )) fail(service, u, lc, "Illegal provider-class name: " + ln); } if (!providers.containsKey(ln) && !names.contains(ln)) names.add(ln); } return lc + 1 ; }
LazyIterator
也是 Iterator
接口的实现,它的 Lazy 则体现在它总是在 ServiceLoader
的 Iterator
接口匿名实现在执行 hasNext()
或 next()
时才会“懒判断”是否存在下一个实现类的实例或“懒加载”下一个实现类的实例。
parse()
方法用于读取被加载资源内部的每行实现类的全限定名,而 parseLine()
则是主要用于解析并判断实现类的全限定名是否合法。
以上就是整个 ServiceLoader
中的全部代码,可以看出具体实现并不复杂,但它的功能很强大,被广泛应用于 JDBC、JNDI 等类库中。