Android ServiceLoader使用详解和源码分析

一、SPI(Service Provider Interface)

在介绍ServiceLoader之前,需要先说下SPI (Service Provider Interface)这个概念。

SPI属于动态加载接口实现类的的一项技术,是JDK内置的一种服务提供发现机制,使用ServiceLoader去加载接口对应的实现,这样我们就不用关注实现类,ServiceLoader会告诉我们。官方文档描述为:为某个接口寻找服务的机制,类似IOC思想,将装配的控制权交给ServiceLoader。

使用场景

只提供服务接口,具体服务由其他组件实现,接口和具体实现分离(类似桥接),同时能够通过系统的ServiceLoader拿到这些实现类的集合,统一处理,这样在组件化中往往会带来很多便利,SPI机制可以实现不同模块之间方便的面向接口编程,拒绝了硬编码的方式,解耦效果很好。

例如有如下工程结构的项目:

image.png

场景:如果想在组件1中使用主工程或组件1使用组件2的方法或变量,如何实现

  1. 常见方式,在组件1声明一些接口,由主工程实现。然后在初始化组件1的时候,通过注入方式传递给组件1。
  2. ServiceLoader方式,如果都通过依赖注入的方式,组件间耦合较重。ServiceLoader方式也是组件1声明接口,主工程实现。但是无须注入,这部分工作由ServiceLoader自动实现。

二、使用方式

ServiceLoader的使用流程遵循:服务约定 -> 服务实现 -> 服务注册 -> 服务发现/使用。
首先约定几个概念名词,并且后文中,以这些名词行文。
概念说明备注服务接口或(通常是)抽象类出于加载的目的,服务由单一类型表示,即单一接口或抽象类。 (可以使用具体类,但不建议这样做。)服务提供者服务(接口和抽象类)的具体实现。服务提供者可以以扩展形式引入,例如jar包;也可以通过将它们添加到应用程序的类路径或通过其他一些特定于平台的方式来提供。给定服务提供者包含一个或多个具体类,这些类使用特定于提供者的数据和代码扩展该服务类型。 此工具强制执行的唯一要求是提供程序类必须具有零参数构造函数,以便它们可以在加载期间实例化。

  1. 服务约定
    定义好接口或抽象类作为服务

  2. 服务实现
    实现定义好的服务,由于ServiceLoader可以更方便不同组件间通信,高度解耦。所以更常见的场景是服务可能是定义在底层组件或引入jar包,在上层业务代码中具体实现。

  3. 服务注册
    约定和实现了服务后,需要对服务进行注册,系统才能定位到该服务。注册方式是在java同级目录,创建一个resources/META-INF/services的目录,在该目录下,以服务的全限定名创建一个SPI描述文件。目录层级图如下:

    image.png

    有了该文件,即可将服务提供者(接口实现类)的全限定名分行写入该文件,即完成服务注册
    PS. 注册目录路径是固定,至于为什么后,下文代码部分将会说明。

示例:

package com.example;
// 声明服务
public interface IHello {
 String sayHello();
}
---------------------------------------------------------------
// 实现服务
public class Hello implements IHello{
 @Override
 public String sayHello(){
 System.out.println("hello, world");
 }
}

---------------------------------------------------------------
// 使用服务
ServiceLoader<Hello> loader = ServiceLoader.load(IHello.class);
mIterator =loader.iterator(); 
while(mIterator.hasNext()){
 mIterator.next().sayHello();
}

三、代码逻辑

ServiceLoader成员变量说明

字段类型说明serviceClass<S>ServiceLoader加载的接口或抽象类loaderClassLoader类加载器providersLinkedHashMap<String,S>缓存加载的接口或抽象类(即service对象)lookupIteratorLazyIterator迭代器

3.1、ServiceLoader的创建

//ServiceLoader.class
 public static <S> ServiceLoader<S> load(Class<S> service,
 ClassLoader loader)
 {
 return new ServiceLoader<>(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();
 }

 /**
 * Clear this loader's provider cache so that all providers will be
 * reloaded.
 *
 * <p> After invoking this method, subsequent invocations of the {@link
 * #iterator() iterator} method will lazily look up and instantiate
 * providers from scratch, just as is done by a newly-created loader.
 *
 * <p> This method is intended for use in situations in which new providers
 * can be installed into a running Java virtual machine.
 */
 public void reload() {
 providers.clear();
 lookupIterator = new LazyIterator(service, loader);
 }

可以看出ServiceLoader#load方法创建了ServiceLoader对象,并且初始化了service、loader对象(含义见上表)。同时调用了reload方法清空了缓存,并且创建了LazyIterator对象,用来遍历加载的服务。

这个阶段发现只是做了初始化工作,但并没有加载注册的服务,所以这是一个懒加载过程(LazyIterator的命名也透露了这点)。

3.2、服务的注册

3.1小节中说了load方法只是做了一些初始化的工作,并没有注册服务。那么服务具体是在什么位置进行注册的呢?在使用时,会获取ServiceLoader的迭代器来遍历服务,看下ServiceLoader#iterator方法:

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();
 }
 };
 }

可见就是创建了一个迭代器对象,实现了3个方法hasNext(用以判断是否还有为遍历的服务)、next(获取服务)和remove。在内部用knownProviders缓存了已注册服务,每次调用hasNext或next方法时,先从缓存中的服务中取,没有再调用lookupIterator的对应方法

对于首次创建的情况,缓存中没有注册好的服务,如果调用hasNext,就会调用lookupIterator.hasNext(),代码如下

private class LazyIterator implements Iterator<S> {
 Class<S> service;
 ClassLoader loader;
 Enumeration<URL> configs = null;
 Iterator<String> pending = null;
 String nextName = null;
 public boolean hasNext() {
 return 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;
 }

}

①:判断nextName是否为null,表示下一个待注册服务的全称(目录路径+服务名),不为null表示有服务,直接返回true;否则继续执行
②:configs保存所有指定名称的资源,在这里就是我们声明的resources/META-INF/services/<package name>文件。如果为null,表示还未加载该资源文件。
③:构造要加载的资源文件全名,PREFIX的值为:

这就是为什么我们要创建resources/META-INF/services目录,并在该目录声明一个注册文件。

④:如果注册成功了,configs变量就储存了所有声明的服务全名。这一步才是真正注册了所有服务的位置,懒加载就是在这里进行的。
⑤:首次加载,pending迭代器对象为null,进入循环。如果configs里没有值,直接返回false,表明没有可注册的服务。否则进入⑥处
⑥:调用parse方法,它打开注册文件开始读取文件内容,每读到一行服务全名,如果它没有缓存过(即不在providers里)。就把它添加到一个列表中。直到文件内容全部读取完成,最后返回该列表的迭代器对象。

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 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;
 }

3.3 服务的使用

通过3.2小节讲解的步骤,现在服务已经全部注册了,可以获取各个服务实例并使用了。通常是通过next()方法获取服务的实例对象,代码如下:

 public S next() {
 return 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
 }

①:没有服务,直接抛出异常
②:根据服务的全限定名字加载其class对象。
③:调用newInstance创建服务的实例,并且将该实例类型装换成声明的服务类型(接口或抽象类)。
④:缓存一份,提升访问效率,避免每次都反射加载。
至此,就拿到了服务的实例,使用者就可以通过该实例对象去调用各种实例方法了。

4、总结

1、本文详细说明了SPI的概念和ServiceLoader的具体用法,其使用包括服务约定 -> 服务实现 -> 服务注册 -> 服务发现/使用等过程
2、通过代码分析,说明了ServiceLoader的底层实现,包括服务注册的懒加载机制、服务注册为什么固定目录以及服务使用时的hasNext()next()方法等。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354