SPI 机制及在Android中的使用

3,163 阅读5分钟

SPIService Provider Interface,该方案是为某个接口动态寻找服务的机制,类似IOC的思想。

SPI的使用

先通过一个简单的例子来对SPI机制有一个初步的认识

image.png

定义接口

在Android Studio中新建一个module,新增一个接口Machine, 接口定义如下:

public interface Machine {
    void powerOn();
}

实现类

新增两个实现类,分别是TVComputer, 如下:

public class TV implements Machine {
    @Override
    public void powerOn() {
        System.out.println("TV power on");
    }
}

public class Computer implements Machine {
    @Override
    public void powerOn() {
        System.out.println("Computer power on");
    }
}

定义类关系

  • main目录下定义一个resources.META-INF.services目录, 在该目录下添加一个名为com.fred.spi.Machine文件,需要注意该文件名必须是和上面的Machine对应上。
  • com.fred.spi.Machine文件中添加两行
    com.fred.spi.impl.Computer
    com.fred.spi.impl.TV
    

测试

  • 我们定义一个MachineFactory类,用一个工厂来管理。
public class MachineFactory {
    private static MachineFactory mInstance;
    private Iterator<Machine> mIterator;

    private MachineFactory() {
        ServiceLoader<Machine> loader = ServiceLoader.load(Machine.class);
        mIterator = loader.iterator();
    }

    static MachineFactory getInstance() {
        if (null == mInstance) {
            synchronized (MachineFactory.class) {
                if (null == mInstance) {
                    mInstance = new MachineFactory();
                }
            }
        }
        return mInstance;
    }
    Machine getMachine() {
        return mIterator.next();
    }
    boolean hasNextMachine() {
        return mIterator.hasNext();
    }
}

  • 测试入口文件
public static void main(String[] args) {
    MachineFactory factory = MachineFactory.getInstance();
    while (factory.hasNextMachine()) {
       factory.getMachine().powerOn();
    }
}

执行上面的代码会输出

Computer power on
TV power on

从java代码的层面上看,我们并没有任何地方new Computer, TV; 更没有执行其powerOn()方法,只是在一个配置文件里面加了Computer, TV对应的类名。

SPI机制原理

MachineFactory中我们可以看到,加载Machine接口的实现类只依赖于一行代码:

ServiceLoader<Machine> loader = ServiceLoader.load(Machine.class);

接着只需要对ServiceLoader进行遍历,就可以找到所有有实现类。在上面的例子中,因为我们在com.fred.spi.Machine文件中配了两个,所以能找到两个实现类。

ServiceLoader的源码

我们来从ServiceLoader的源码角度看看是如何完成类加载的。由于ServiceLoader是在rt.jar包中, 我们在安装jdk的时候是可以下载一个src.zip文件,可以将该文件导入到IDE中去关联源码,但rt.jar并不在src.zip中,从这里可以拿到相关的代码。 其核心代码如下:

public final class ServiceLoader<S>
    implements Iterable<S>
{
    private static final String PREFIX = "META-INF/services/";
    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;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }

    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }

    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,
                         "Provider " + cn + " not found");
                }
                if (!service.isAssignableFrom(c)) {
                    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() {
                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() {
                if (acc == null) {
                    return nextService();
                } else {
                    PrivilegedAction<S> action = new PrivilegedAction<S>() {
                        public S run() { return nextService(); }
                    };
                    return AccessController.doPrivileged(action, acc);
                }
            }
        }
    } 
}

可以看到,其思路就是:

  1. 先获得一个classloader
  2. 然后去加载META-INF/services/下面的文件,获取相关的配置,如代码:
String fullName = PREFIX + service.getName();
if (loader == null)
    configs = ClassLoader.getSystemResources(fullName);
else
    configs = loader.getResources(fullName);
  1. 获取对应的实现类名, 即parse方法
  2. 利用反射,根据类名去创建对应的实例, 即nextService方法

Android中的应用

SPI机制能够较好的解藕,便于代码的扩展,比如有这么个场景,我们需要从多个数据源获取数据,每一个数据源相关的操作都作为一个子module集成到app中,这个时候,我们可以定义一个META-INF/services/xxx文件,来配置数据源。

如果开发者自己写过类似于ARouter这种路由框架,肯定会了解com.google.auto.service:auto-service, 该组件便是简化了SPI的使用,让开发者不需要去手动维护META-INF/services/xxx

在我们自己写一个路由框架时,会需要自己实现一个AbstractProcessor, 用来生成路由相关的配置。于是会定义一个类似的文件:

@AutoService(Processor.class)
// 允许/支持的注解类型,让注解处理器处理
@SupportedAnnotationTypes({Constants.ROUTER_ANNOTATION_TYPES})
// 指定JDK编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// 注解处理器接收的参数
@SupportedOptions({Constants.MODULE_NAME, Constants.APT_PACKAGE})
public class RouterProcessor extends AbstractProcessor {
    private Elements elementsUtils;
    
    ....

}

手写注入

如果我们不采用自动注入的方式,我们需要自己去维护一个META-INF/services/xxx文件(上面代码中的第一行就没有必要加了),如下;

image.png

我们需要将对应Processor配置到这个文件里面,而Google的com.google.auto.service:auto-service组件便是简化了SPI的使用,让开发者不需要去手动维护META-INF/services/xxx。只需要加一个注解,由框架在编译时自动生成这个配置文件

自动注入

再回到上面的第一行代码@AutoService(Processor.class), 这个注解是auto-service这个库提供的。在编译阶段,会执行AutoServiceProcessorprocess方法,在该方法中会先调用generateConfigFiles生成配置文件,如下:

 private void generateConfigFiles() {
    Filer filer = processingEnv.getFiler();

    for (String providerInterface : providers.keySet()) {
      String resourceFile = "META-INF/services/" + providerInterface;
      log("Working on resource file: " + resourceFile);
      try {
        SortedSet<String> allServices = Sets.newTreeSet();
        try {
       
          FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",
              resourceFile);
          log("Looking for existing resource file at " + existingFile.toUri());
          Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
          log("Existing service entries: " + oldServices);
          allServices.addAll(oldServices);
        } catch (IOException e) {
          log("Resource file did not already exist.");
        }

        Set<String> newServices = new HashSet<String>(providers.get(providerInterface));
        if (allServices.containsAll(newServices)) {
          log("No new service entries being added.");
          return;
        }

        allServices.addAll(newServices);
        log("New service file contents: " + allServices);
        FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
            resourceFile);
        OutputStream out = fileObject.openOutputStream();
        ServicesFiles.writeServiceFile(allServices, out);
        out.close();
        log("Wrote to: " + fileObject.toUri());
      } catch (IOException e) {
        fatalError("Unable to create " + resourceFile + ", " + e);
        return;
      }
    }
  }

最终生成了配置文件中的类便指向了我们自定义的Processor。

image.png

当这个模块在使用的时候,便可以通过该配置找到具体的实现类,并完成实例化。