SPI 机制(一) — 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<S> service) 方法进行接口实现类的加载。

小结:

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

三、使用流程

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

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

类的继承关系如下图所示:
在这里插入图片描述
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 接口的全路径类名信息的两个文件。
    在这里插入图片描述
  2. 每个文件里面存储的是当前文件名对应接口类的所有子类。
    在这里插入图片描述
    在这里插入图片描述

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. 实例不是单例。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值