美文网首页Android技术进阶Android进阶之路Android开发经验谈
金三银四面试之【SPI原理及实现零耦合 】

金三银四面试之【SPI原理及实现零耦合 】

作者: 谁动了我的代码 | 来源:发表于2023-02-13 21:07 被阅读0次

SPI的定义

SPI英文全称为Service Provider Interface,是JDK提供的一套用于帮助使用第三方实现的技术工具。表现为JAVA应用层实现服务接口,第三方实现接口,然后通过SPI的方式实现服务调用。 SPI机制主要思想是将装配的控制权移到程序之外,在组件化设计中这个机制尤其重要,其核心思想就是解耦。

SPI整体机制

image

Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,最核心的思想就是服务注册+服务发现

SPI和API区别

API

image

SPI

image

为了更清楚的把这个问题讲明白,我们使用具体的图来说明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。

image

以上内容为金三银四常问的面试题;spi原理与实现零耦合的解析,有关Android开发的面试题及Android进阶技术;可以参考《Android精选面试题库》点击查看获取!

SPI使用注意事项

  • 无法按需加载。ServiceLoader每次都会加载所有的实现,如果有的没有用到也进行加载和实例化,会造成一定系统资源的浪费。
  • 线程安全问题。ServerLoader可以看作是一个工具类,提供了很多static方法,但是其内部用到了一些成员变量,这样就会导致在多线程调用的时候有线程安全问题,需要注意。
  • 异常吞噬。ServerLoader在加载类的过程中如果出现异常无法加载没有相关的异常抛出,导致一旦出现问题需要花时间进行定位。

相关文章

网友评论

    本文标题:金三银四面试之【SPI原理及实现零耦合 】

    本文链接:https://www.haomeiwen.com/subject/xisskdtx.html