SPI 机制及在Android中的使用
作者:FredYe
链接:https://juejin.cn/post/698516883725982108
SPI即 Service Provider Interface,该方案是为某个接口动态寻找服务的机制,类似IOC的思想。
SPI的使用
先通过一个简单的例子来对SPI机制有一个初步的认识
定义接口
在Android Studio中新建一个module,新增一个接口Machine, 接口定义如下:
1public interface Machine {
2 void powerOn();
3}
实现类
新增两个实现类,分别是TV和Computer, 如下:
1public class TV implements Machine {
2 @Override
3 public void powerOn() {
4 System.out.println("TV power on");
5 }
6}
7
8
9public class Computer implements Machine {
10 @Override
11 public void powerOn() {
12 System.out.println("Computer power on");
13 }
14}
定义类关系
在main目录下定义一个resources.META-INF.services目录, 在该目录下添加一个名为com.fred.spi.Machine文件,需要注意该文件名必须是和上面的Machine对应上。
在 com.fred.spi.Machine
文件中添加两行
1com.fred.spi.impl.Computer
2com.fred.spi.impl.TV
测试
我们定义一个
MachineFactory类,用一个工厂来管理。
1public class MachineFactory {
2 private static MachineFactory mInstance;
3 private Iterator<Machine> mIterator;
4
5 private MachineFactory() {
6 ServiceLoader<Machine> loader = ServiceLoader.load(Machine.class);
7 mIterator = loader.iterator();
8 }
9
10 static MachineFactory getInstance() {
11 if (null == mInstance) {
12 synchronized (MachineFactory.class) {
13 if (null == mInstance) {
14 mInstance = new MachineFactory();
15 }
16 }
17 }
18 return mInstance;
19 }
20 Machine getMachine() {
21 return mIterator.next();
22 }
23 boolean hasNextMachine() {
24 return mIterator.hasNext();
25 }
26}
测试入口文件
1public static void main(String[] args) {
2 MachineFactory factory = MachineFactory.getInstance();
3 while (factory.hasNextMachine()) {
4 factory.getMachine().powerOn();
5 }
6}
执行上面的代码会输出
1Computer power on
2TV power on
从java代码的层面上看,我们并没有任何地方new Computer, TV; 更没有执行其powerOn()方法,只是在一个配置文件里面加了Computer, TV对应的类名。
SPI机制原理
在MachineFactory中我们可以看到,加载Machine接口的实现类只依赖于一行代码:
1ServiceLoader<Machine> loader = ServiceLoader.load(Machine.class);
接着只需要对ServiceLoader进行遍历,就可以找到所有有实现类。在上面的例子中,因为我们在com.fred.spi.Machine文件中配了两个,所以能找到两个实现类。
`ServiceLoader`的源码
我们来从ServiceLoader的源码角度看看是如何完成类加载的。由于ServiceLoader是在rt.jar包中, 我们在安装jdk的时候是可以下载一个src.zip文件,可以将该文件导入到IDE中去关联源码,但rt.jar并不在src.zip中,从这里可以拿到相关的代码。其核心代码如下:
1public final class ServiceLoader<S>
2 implements Iterable<S>
3{
4 private static final String PREFIX = "META-INF/services/";
5 public static <S> ServiceLoader<S> load(Class<S> service) {
6 ClassLoader cl = Thread.currentThread().getContextClassLoader();
7 return ServiceLoader.load(service, cl);
8 }
9
10 public static <S> ServiceLoader<S> load(Class<S> service,
11 ClassLoader loader) {
12 return new ServiceLoader<>(service, loader);
13 }
14
15 private ServiceLoader(Class<S> svc, ClassLoader cl) {
16 service = Objects.requireNonNull(svc, "Service interface cannot be null");
17 loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
18 acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
19 reload();
20 }
21
22 public void reload() {
23 providers.clear();
24 lookupIterator = new LazyIterator(service, loader);
25 }
26
27 private Iterator<String> parse(Class<?> service, URL u)
28 throws ServiceConfigurationError
29 {
30 InputStream in = null;
31 BufferedReader r = null;
32 ArrayList<String> names = new ArrayList<>();
33 try {
34 in = u.openStream();
35 r = new BufferedReader(new InputStreamReader(in, "utf-8"));
36 int lc = 1;
37 while ((lc = parseLine(service, u, r, lc, names)) >= 0);
38 } catch (IOException x) {
39 fail(service, "Error reading configuration file", x);
40 } finally {
41 try {
42 if (r != null) r.close();
43 if (in != null) in.close();
44 } catch (IOException y) {
45 fail(service, "Error closing configuration file", y);
46 }
47 }
48 return names.iterator();
49 }
50
51 private class LazyIterator
52 implements Iterator<S>
53 {
54
55 Class<S> service;
56 ClassLoader loader;
57 Enumeration<URL> configs = null;
58 Iterator<String> pending = null;
59 String nextName = null;
60
61 private LazyIterator(Class<S> service, ClassLoader loader) {
62 this.service = service;
63 this.loader = loader;
64 }
65
66 private boolean hasNextService() {
67 if (nextName != null) {
68 return true;
69 }
70 if (configs == null) {
71 try {
72 String fullName = PREFIX + service.getName();
73 if (loader == null)
74 configs = ClassLoader.getSystemResources(fullName);
75 else
76 configs = loader.getResources(fullName);
77 } catch (IOException x) {
78 fail(service, "Error locating configuration files", x);
79 }
80 }
81 while ((pending == null) || !pending.hasNext()) {
82 if (!configs.hasMoreElements()) {
83 return false;
84 }
85 pending = parse(service, configs.nextElement());
86 }
87 nextName = pending.next();
88 return true;
89 }
90
91 private S nextService() {
92 if (!hasNextService())
93 throw new NoSuchElementException();
94 String cn = nextName;
95 nextName = null;
96 Class<?> c = null;
97 try {
98 c = Class.forName(cn, false, loader);
99 } catch (ClassNotFoundException x) {
100 fail(service,
101 "Provider " + cn + " not found");
102 }
103 if (!service.isAssignableFrom(c)) {
104 fail(service,
105 "Provider " + cn + " not a subtype");
106 }
107 try {
108 S p = service.cast(c.newInstance());
109 providers.put(cn, p);
110 return p;
111 } catch (Throwable x) {
112 fail(service,
113 "Provider " + cn + " could not be instantiated",
114 x);
115 }
116 throw new Error(); // This cannot happen
117 }
118
119 public boolean hasNext() {
120 if (acc == null) {
121 return hasNextService();
122 } else {
123 PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
124 public Boolean run() { return hasNextService(); }
125 };
126 return AccessController.doPrivileged(action, acc);
127 }
128 }
129
130 public S next() {
131 if (acc == null) {
132 return nextService();
133 } else {
134 PrivilegedAction<S> action = new PrivilegedAction<S>() {
135 public S run() { return nextService(); }
136 };
137 return AccessController.doPrivileged(action, acc);
138 }
139 }
140 }
141 }
142}
可以看到,其思路就是:
先获得一个classloader
然后去加载
META-INF/services/下面的文件,获取相关的配置,如代码:
1String fullName = PREFIX + service.getName();
2if (loader == null)
3 configs = ClassLoader.getSystemResources(fullName);
4else
5 configs = loader.getResources(fullName);
获取对应的实现类名, 即
parse方法利用反射,根据类名去创建对应的实例, 即
nextService方法
Android中的应用
SPI机制能够较好的解藕,便于代码的扩展,比如有这么个场景,我们需要从多个数据源获取数据,每一个数据源相关的操作都作为一个子module集成到app中,这个时候,我们可以定义一个META-INF/services/xxx文件,来配置数据源。
如果开发者自己写过类似于ARouter这种路由框架,肯定会了解com.google.auto.service:auto-service, 该组件便是简化了SPI的使用,让开发者不需要去手动维护META-INF/services/xxx。
在我们自己写一个路由框架时,会需要自己实现一个AbstractProcessor, 用来生成路由相关的配置。于是会定义一个类似的文件:
1@AutoService(Processor.class)
2// 允许/支持的注解类型,让注解处理器处理
3@SupportedAnnotationTypes({Constants.ROUTER_ANNOTATION_TYPES})
4// 指定JDK编译版本
5@SupportedSourceVersion(SourceVersion.RELEASE_7)
6// 注解处理器接收的参数
7@SupportedOptions({Constants.MODULE_NAME, Constants.APT_PACKAGE})
8public class RouterProcessor extends AbstractProcessor {
9 private Elements elementsUtils;
10
11 ....
12
13}
手写注入
如果我们不采用自动注入的方式,我们需要自己去维护一个META-INF/services/xxx文件(上面代码中的第一行就没有必要加了),如下;
我们需要将对应Processor配置到这个文件里面,而Google的com.google.auto.service:auto-service组件便是简化了SPI的使用,让开发者不需要去手动维护META-INF/services/xxx。只需要加一个注解,由框架在编译时自动生成这个配置文件
自动注入
再回到上面的第一行代码@AutoService(Processor.class), 这个注解是auto-service这个库提供的。在编译阶段,会执行AutoServiceProcessor的process方法,在该方法中会先调用generateConfigFiles生成配置文件,如下:
1 private void generateConfigFiles() {
2 Filer filer = processingEnv.getFiler();
3
4 for (String providerInterface : providers.keySet()) {
5 String resourceFile = "META-INF/services/" + providerInterface;
6 log("Working on resource file: " + resourceFile);
7 try {
8 SortedSet<String> allServices = Sets.newTreeSet();
9 try {
10
11 FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",
12 resourceFile);
13 log("Looking for existing resource file at " + existingFile.toUri());
14 Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
15 log("Existing service entries: " + oldServices);
16 allServices.addAll(oldServices);
17 } catch (IOException e) {
18 log("Resource file did not already exist.");
19 }
20
21 Set<String> newServices = new HashSet<String>(providers.get(providerInterface));
22 if (allServices.containsAll(newServices)) {
23 log("No new service entries being added.");
24 return;
25 }
26
27 allServices.addAll(newServices);
28 log("New service file contents: " + allServices);
29 FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
30 resourceFile);
31 OutputStream out = fileObject.openOutputStream();
32 ServicesFiles.writeServiceFile(allServices, out);
33 out.close();
34 log("Wrote to: " + fileObject.toUri());
35 } catch (IOException e) {
36 fatalError("Unable to create " + resourceFile + ", " + e);
37 return;
38 }
39 }
40 }
最终生成了配置文件中的类便指向了我们自定义的Processor。
当这个模块在使用的时候,便可以通过该配置找到具体的实现类,并完成实例化。
推荐阅读:
什么?Android 编译线程爆了, gradle 内存 OOM 解决之路
巨丝滑 —— 自己动手撸一个Android图片编辑器(支持长图)