SPI 机制(一) — ServiceLoader 解析

AutoService 解析

    • 一、概述
    • 二、原理
    • 三、使用流程
      • 1. 步骤1和步骤2 (定义接口 IFly、ISpeak,然后实现子类)
      • 2. 步骤3和步骤4
      • 3. 步骤5 (加载流程)
    • 四、ServiceLoader 源码解析
    • 五、ServiceLoader 的缺点

一、概述

SPI 全称为Service Provider Interface,是JDK内置的一种服务提供发现机制。简单来说,它是一种动态替换发现机制。例如,在设计组件化路由框架的时候就会使用到 SPI 设计思想。

场景:

假设有 A,B 两个业务组件,由于业务组件不存在相互依赖的问题,因此 A 组件就无法调用 B 组件的服务(API) 。但是我们可以使用 SPI 的思想实现服务(SPI) 的发现。这样就可以在 A 组件中调用 B 组件提供的功能了。

版本: Android 29

关联文章:

  1. SPI 机制(一) — ServiceLoader 解析
  2. SPI 机制(二) — AutoService 解析

二、原理

系统提供了一个 java.util.ServiceLoader 类用于在指定报名路径下的发现服务,那这个服务又是从哪里获取的呢?这里需要结合 Google 提供的 AutoService 库来一起分析一下。

AutoService 的依赖:

annotationProcessor 'com.google.auto.service:auto-service-annotations:1.0-rc7'
implementation 'com.google.auto.service:auto-service:1.0-rc7'

原理的实现步骤:

  1. 定义一个接口类 IFly。
  2. 创建一个实现类 FlyImpl 实现借口 IFly,并用 @AutoService 注解修饰。
  3. 在编译期会通过 AutoServiceProcessor 对被 @AutoService 注解修饰过的类进行处理,将该实现类(FlyImpl)的全路径类名信息写入一个文件中(该文件名为 IFly 全路径类名信息)。如果该接口有多个实现类,那么这些子类都会被写入统一个文件的不同行中。
  4. 步骤3生成的文件被存储在 META-INF/services 文件夹中。
  5. 在调用的时候,会使用 java.util.ServiceLoader 这个类的 load(Class service) 方法进行接口实现类的加载。

小结:

  1. java.util.ServiceLoader 是用于加载指定路径下的文件。
  2. @AutoService及其注解解析器是为了在指定的加载路径下生成相应的被加载文件。

三、使用流程

接下来我们按照上面的几个步骤进行分析。

1. 步骤1和步骤2 (定义接口 IFly、ISpeak,然后实现子类)

类的继承关系如下图所示:
SPI 机制(一) — ServiceLoader 解析_第1张图片
IFly
定义一个接口 IFly,并实现两个子类 FlyImpl1 、FlyImpl2。

public interface IFly {
    String fly();
}

@AutoService(IFly.class)
public class FlyImpl1 implements IFly {
    @Override
    public String fly() {
        return "FlyImpl1 fly";
    }
}

@AutoService({IFly.class, ISpeak.class})
public class FlyImpl2 implements IFly, ISpeak {
    @Override
    public String fly() {
        return "FlyImpl2 fly";
    }

    @Override
    public String speak() {
        return "FlyImpl2 speak";
    }
}

ISpeak
定义一个接口 ISpeak,并实现两个子类 ISpeakImpl1 、FlyImpl2 (实现了两个接口)。

public interface ISpeak {
    String speak();
}

@AutoService(ISpeak.class)
public class SpeakImpl1 implements ISpeak {
    @Override
    public String speak() {
        return "ISpeakImpl1 speak";
    }
}

@AutoService({IFly.class, ISpeak.class})
public class FlyImpl2 implements IFly, ISpeak {
    @Override
    public String fly() {
        return "FlyImpl2 fly";
    }

    @Override
    public String speak() {
        return "FlyImpl2 speak";
    }
}

2. 步骤3和步骤4

在 Android 打包编译后,我们来看下 Apk 的包结构。

  1. 生成了以 ISpeak、IFly 接口的全路径类名信息的两个文件。
    SPI 机制(一) — ServiceLoader 解析_第2张图片
  2. 每个文件里面存储的是当前文件名对应接口类的所有子类。
    SPI 机制(一) — ServiceLoader 解析_第3张图片
    SPI 机制(一) — ServiceLoader 解析_第4张图片

3. 步骤5 (加载流程)

// 1.根绝传入的接口名称,构建一个ServiceLoader。
ServiceLoader<IFly> load = ServiceLoader.load(IFly.class);
// 2.获取该接口的所有子类。
load.forEach(fly -> {
    String fly1 = fly.fly();
    System.out.println(fly1);
});

小结:

  1. 根据传入的接口名称,构建一个ServiceLoader。
  2. 获取该接口的所有子类。

四、ServiceLoader 源码解析

分析了整个加载流程的步骤,那下面我们就来具体分析一下 ServiceLoader.load() 是如何加载的。

ServiceLoader.load()

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

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

小结:

  1. 加载的时候,调用 ServiceLoader.load() 方法,传入指定接口Class。
  2. 传入的指定接口会赋值给 ServiceLoader 的成员变量 service 。
  3. 构建完成之后会返回一个 ServiceLoader 对象 (每个 ServiceLoader 对象都对应一个要加载的接口类) 。

下面看一下 reload 方法。

ServiceLoader.reload()

// providers 的key存储的是实现类的全路径信息,value是接口的实现类。
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

public void reload() {
    providers.clear();
    // 一个用于子类对象的迭代器。
    lookupIterator = new LazyIterator(service, loader);
}

private LazyIterator(Class<S> service, ClassLoader loader) {
    this.service = service;
    this.loader = loader;
}

小结:

执行完 ServiceLoader.load() 之后会返回一个 ServiceLoader 对象。由于 ServiceLoader 实现了 Iterable 接口,所以接下来我们会
调用 ServiceLoader.iterator() 来获取该接口有多少实现类。

ServiceLoader.iterator()

public Iterator<S> iterator() {
    return new Iterator<S>() {
    	// 将 providers 的数据赋值给 knownProviders,相当于之前已经加载过的。
        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();
        }
    };
}

小结:

几个参数的含义:

  1. knownProviders 存储之前加载过的实现类,避免再次解析文件。
  2. providers 存储所有的实现类,包括即将要从文件中查找的子类。
  3. lookupIterator 是 LazyIterator类型的,用来执行从文件中查找接口子类的迭代器。

我们知道,迭代器进行迭代操作时,会先执行 Iterator.hasNext() 方法判断是否有下一个数据,如果存在下一个数据,才会执行 Iterator.next() 方法。

接下来我们先来看一下 LazyIterator.hasNext() 方法。

LazyIterator.hasNext() 调用链

// LazyIterator.class
public boolean hasNext() {
    return hasNextService();
}

private static final String PREFIX = "META-INF/services/"

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
        	// 1.service是我们传入进来要查找的接口,这里就是在指定的"META-INF/services/"文件夹下查找指定文件名的文件。
            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;
        }
        // 2.解析文件,从里面读取所有子类的全路径名称,返回一个集合(迭代器)。
        pending = parse(service, configs.nextElement());
    }
    // 3.一次进行子类名称的获取,赋值给nextName,后面会通过反射构造 nextName 对象。
    nextName = pending.next();
    return true;
}

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;
        // 解析指定文件的每一行数据(每一行都是一个子类),如果没有数据了就返回-1。
        while ((lc = parseLine(service, u, r, lc, names)) >= 0);
    } catch (IOException x) {
        // ...省略代码...
    }
    // names 包含的指定接口的所有子类名称。
    return names.iterator();
}

// 对指定接口类的文件进行解析,获取里面的子类信息(每个子类都占用一行)。
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, List<String> names)
    	throws IOException, ServiceConfigurationError{
    // 1.读取文件的每一行。
    String ln = r.readLine();
    // 2.读取不到就返回-1。
    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) {
        // ...校验内容的合法性...
        // 3.判断是否加载过,没加载过就添加到集合中。
        if (!providers.containsKey(ln) && !names.contains(ln))
            names.add(ln);
    }
    // 4.lc自增,进行下一行的读取操作。
    return lc + 1;
}

小结:

  1. 调用 LazyIterator.hasNext() 会去 "META-INF/services/" 文件夹下查找指定 service 文件名的文件。
  2. 解析该文件的每一行信息 (每一行都存储了一个子类信息),并存储在 ArrayList 中。
  3. 从 ArrayList 中获取子类信息,并通过反射构造指定接口的子类对象。
  4. 将构造的对象保存到 providers 集合中,提升查找性能。

调用完 LazyIterator.hasNext() 方法后会执行 LazyIterator.next() 方法。

LazyIterator.next()

public S next() {
    return nextService();
}

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    //  1.nextName 会在 LazyIterator.hasNext() 方法中进行赋值,nextName 就是子类的全路径名称。
    String cn = nextName;
    nextName = null;
    // 2. 通过反射,构造一个给定 nextName 名称的对象实例。
    Class<?> c = Class.forName(cn, false, loader);
    // ...省略...
    try {
    	// 3.进行类型转换,转化成为我们指定的接口类型。
        S p = service.cast(c.newInstance());
        // 4.将已经加载起来的实例对象保存到内存当中,避免下次访问需要重新从文件进行解析加载。
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
		// ...省略...
    }
    throw new Error();          // This cannot happen
}

小结:

  1. 获取要构造的类的信息nextName (nextName 会在 LazyIterator.hasNext() 方法中进行赋值,nextName 就是子类的全路径名称)。
  2. 通过反射,构造一个给定 nextName 名称的对象实例。
  3. 进行类型转换,转化成为我们指定的接口类型。
  4. 将已经加载起来的实例对象保存到内存当中,避免下次访问需要重新从文件进行解析加载。

五、ServiceLoader 的缺点

通过对 ServiceLoader 的解析,我们也看出了 ServiceLoader 的一些缺陷。

  1. 会批量实例化指定的接口的所有子类,造成了内存的浪费。
  2. 获取单个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。
  3. 实例不是单例。

你可能感兴趣的:(开源框架源码分析)