SPI的定义
SPI英文全称为Service Provider Interface,是JDK提供的一套用于帮助使用第三方实现的技术工具。表现为JAVA应用层实现服务接口,第三方实现接口,然后通过SPI的方式实现服务调用。 SPI机制主要思想是将装配的控制权移到程序之外,在组件化设计中这个机制尤其重要,其核心思想就是解耦。
SPI整体机制
Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,最核心的思想就是服务注册+服务发现
SPI和API区别
API
SPI
为了更清楚的把这个问题讲明白,我们使用具体的图来说明SPI与API区别,上图就很清晰的说明了这两个问题
一般模块之间通信基本上都是通过接口,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口概念”。当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是API,这种接口和实现都是放在实现方的。接口和实现方属于同一个模块,密切不可分割。当接口存在于调用方这边时,就是SPI,由接口调用方确定接口规则,然后由不同的具体业务去根据这个规则对这个接口进行实现,从而提供服务,举个通俗易懂的例子:一个电脑制造公司,设计好了充电器标准图纸以后,那么接下来就可以把这个图纸分发给不同的厂商去生产,最后只要严格按照图纸要求,就可以生产合格的商品。通过上面的图2和图3以及配合上面的文字介绍,相信大家应该很非常清楚API和SPI的区别了
SPI实现原理
源码分析:
ServiceLoader源码
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;
// The access control context taken when the ServiceLoader is created
// Android-changed: do not use legacy security code.
// private final AccessControlContext acc;
//providers就是不同实现类的缓存,key就是实现类的全限定名,value就是实现类的实例
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// //内部类LazyIterator的实例
private LazyIterator lookupIterator;
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;
// Android-changed: Do not use legacy security code.
// On Android, System.getSecurityManager() is always null.
// acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
private static void fail(Class<?> service, String msg, Throwable cause)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg,
cause);
}
private static void fail(Class<?> service, String msg)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg);
}
private static void fail(Class<?> service, URL u, int line, String msg)
throws ServiceConfigurationError
{
fail(service, u + ":" + line + ": " + msg);
}
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;
}
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();
}
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;
}
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,
// Android-changed: Let the ServiceConfigurationError have a cause.
"Provider " + cn + " not found", x);
// "Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
// Android-changed: Let the ServiceConfigurationError have a cause.
ClassCastException cce = new ClassCastException(
service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
fail(service,
"Provider " + cn + " not a subtype", cce);
// 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(); // This cannot happen
}
public boolean hasNext() {
// Android-changed: do not use legacy security code
/* 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() {
// Android-changed: do not use legacy security code
/* 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();
}
}
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();
}
};
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
/**
* Creates a new service loader for the given service type, using the
* current thread's {@linkplain java.lang.Thread#getContextClassLoader
* context class loader}.
*
* <p> An invocation of this convenience method of the form
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>)</pre></blockquote>
*
* is equivalent to
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>,
* Thread.currentThread().getContextClassLoader())</pre></blockquote>
*
* @param <S> the class of the service type
*
* @param service
* The interface or abstract class representing the service
*
* @return A new service loader
*/
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
/**
* Creates a new service loader for the given service type, using the
* extension class loader.
*
* <p> This convenience method simply locates the extension class loader,
* call it <tt><i>extClassLoader</i></tt>, and then returns
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>, <i>extClassLoader</i>)</pre></blockquote>
*
* <p> If the extension class loader cannot be found then the system class
* loader is used; if there is no system class loader then the bootstrap
* class loader is used.
*
* <p> This method is intended for use when only installed providers are
* desired. The resulting service will only find and load providers that
* have been installed into the current Java virtual machine; providers on
* the application's class path will be ignored.
*
* @param <S> the class of the service type
*
* @param service
* The interface or abstract class representing the service
*
* @return A new service loader
*/
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);
}
public static <S> S loadFromSystemProperty(final Class<S> service) {
try {
final String className = System.getProperty(service.getName());
if (className != null) {
Class<?> c = ClassLoader.getSystemClassLoader().loadClass(className);
return (S) c.newInstance();
}
return null;
} catch (Exception e) {
throw new Error(e);
}
}
// END Android-added: loadFromSystemProperty(), for internal use.
/**
* Returns a string describing this service.
*
* @return A descriptive string
*/
public String toString() {
return "java.util.ServiceLoader[" + service.getName() + "]";
}
}
4.1 ServiceLoader.load加载入口,整个方法的入口是java.util.ServiceLoader#load 为入口,将当前接口Class类型及其类加载器传入至Loader变量中
@CallerSensitive
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
loader.iterator() 返回一个迭代器。首先会到providers中去查找有没有存在的实例,有就直接返回,没有再到LazyIterator中查找变量传入之后,初始化类:LazyIterator,从名称就可以看出来这是一个懒加载的迭代器,只有真正使用触发时才会进行实例的,初始化,核心初始化逻辑在方法:java.util.ServiceLoader.
LazyIterator#hasNextService中
//其他代码忽略
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;
}
//其他代码忽略
总体的实现步骤:
- 首先拿到配置文件名fullName
- 通过类加载器获得所有模块的配置文件
- 依次扫描每个配置文件的内容,返回配置文件内容Iterator pending,每个配置文件中可能有多个实现类的全限定名,所以pending也是个迭代器
4.2 分析nextService方法
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", x);
}
if (!service.isAssignableFrom(c)) {
ClassCastException cce = new ClassCastException(
service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
fail(service,
"Provider " + cn + " not a subtype", cce);
}
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(); // This cannot happen
}
- 首先根据nextName,Class.forName加载拿到具体实现类的class对象
- Class.newInstance()实例化拿到具体实现类的实例对象
- 将实例对象转换service.cast为接口
- 返回实例对象
SPI 机制实现解耦
如下的示例展示了通过 ServiceLoader 类加载指定接口的所有服务提供者并进行调用的简单实现。
1、定义接口 test.DirMonitor,包含一个方法 start();
2、实现接口 test.DirMonitor,定义两个实现类 test.ObserverMonitor 和 test.LoopMonitor;
3、设置接口的实现类列表。创建目录 META-INF/services/,新建文件 test.DirMonitor,内容如下:
test.ObserverMonitor test.LoopMonitor
4、在程序中通过 ServiceLoader 类加载 test.DirMonitor 接口的实现类,并遍历所有实现类,调用 start() 方法;
从上面的示例可以看出,在代码中仅仅使用到了接口 test.DirMonitor,并没有在代码中使用到具体实现类。通过这种方法,可以实现解耦,接口与实现类可以由不同的开发人员实现,编译到不同的 jar 包中,甚至实现插件的定义与开发。
spi 包的本地化扩展
java.util.spi 包提供了一些抽象类,可以用于扩展 Java 的本地化服务。本地化 SPI 的使用方法与 ServiceLoader 的 SPI 稍有不同,本地化 SPI 的实现需要打包成 jar 包后,放置于运行的 jre/lib/ext 目录下方能生效。
java.util.spi 包提供的抽象类如下所示:
- CalendarDataProvider:为 java.util.Calendar 类的参数提供本地化数据的服务提供者的抽象类;
- CalendarNameProvider:为 java.util.Calendar 类的字段提供本地化名称的服务提供者的抽象类;
- CurrencyNameProvider:为 java.util.Currency 提供本地化货币符号名称的服务提供者的抽象类;
- LocaleNameProvider:为 java.util.Locale 类提供本地化名称的服务提供者的抽象类;
- LocaleServiceProvider:其他服务提供者抽象类的基类;
- ResourceBundleControlProvider:服务接口,用于提供 java.util.ResourceBundle.Control 的实现类;
- TimeZoneNameProvider:为 java.util.TimeZone 提供本地化时区的服务提供者的抽象类。
以 CalendarDataProvider 为例,该抽象类用于实现本地化的日历数据。在下面的例子中,CalendarDataProviderSPI 类设置了每周的第一天为 3,一年中第一周所需的最少天数为 6。
按照 SPI 的要求,在 META-INF/services/ 下建立 java.util.spi.CalendarDataProvider 文件,并写入 testspi.CalendarDataProviderSPI。打包为 jar 后,放置于 jre/lib/ext 目录后,执行下面的代码,则打印的数字分别为 3 和 6,而不再是默认的 1 和 1。
以上内容为金三银四常问的面试题;spi原理与实现零耦合的解析,有关Android开发的面试题及Android进阶技术;可以参考《Android精选面试题库》点击查看获取!
SPI使用注意事项
- 无法按需加载。ServiceLoader每次都会加载所有的实现,如果有的没有用到也进行加载和实例化,会造成一定系统资源的浪费。
- 线程安全问题。ServerLoader可以看作是一个工具类,提供了很多static方法,但是其内部用到了一些成员变量,这样就会导致在多线程调用的时候有线程安全问题,需要注意。
- 异常吞噬。ServerLoader在加载类的过程中如果出现异常无法加载没有相关的异常抛出,导致一旦出现问题需要花时间进行定位。
网友评论