关键词:SPI、ServiceLoader、AutoService、组件化、APT
对于APT(注解处理器),之前写过一篇,使用自定义注解自己实现ButterKnife功能。最近接手的新项目中用了一个新的东西,就是SPI,拿来研究下。
SPI, service provider interface,使用接口、配置文件和策略模式,实现了解耦、模块化和组件化。
ServiceLoader的使用
-
在公共模块或者接口模块中定义一个接口类; com.aya.demo.common.ILogInterface.java
javapublic interface ILogInterface{ void logD(String msg) }
-
在实际业务模块中,写一个接口的实现类; com.aya.demo.LogImpl.java
typescriptpublic class LogImpl implements ILogInterface{ @Override public void logD(String msg){ Log.d("AYA", msg); } }
-
在该业务模块中,在src/main/resources文件夹下创建/META-INF/services目录,并新建一个文件,文件名是接口的全路径,文件的内容是接口实现类的全路径; 文件路径如下: src/main/resources/META-INF/services/com.aya.demo.common.ILogInterface 文件内容是:
com.aya.demo.LogImpl
-
写一个serverloader工具类。
SPIUtils.java
swift/** * 如果有多个实现,取第一个 */ public static <T> T getSpiImpl(Class<T> t) { try { ServiceLoader<T> loader = ServiceLoader.load(t, CommonHolder.ctx.getClassLoader()); Iterator<T> iter = loader.iterator(); if (iter.hasNext()) { return iter.next(); } } catch (Exception e) { // } return null; } /** * 返回所有实现 */ public static <T> List<T> getSpiImpls(Class<T> t) { List<T> impls = new ArrayList<>(); try { ServiceLoader<T> loader = ServiceLoader.load(t, CommonHolder.ctx.getClassLoader()); Iterator<T> iter = loader.iterator(); while (iter.hasNext()) { impls.add(iter.next()); } } catch (Exception e) { // } return impls; }
-
使用接口类
iniILogInterface iLog = SPIUtils.getSpiImpl(ILogInterface.class); iLog.d("测试下");
使用SPI可以很好的实现解耦合组件化隔离,但是这样做有一个缺点,就是每次新建一个实现类,就需要在/META-INF/services目录下创建一个文件或者修改已有的文件,不能动态添加,有点麻烦而且容易写错。针对这种麻烦的事情,程序员们肯定会想办法处理掉的,所以Google又给我们提供了AutoServcie工具。
AutoService的使用
-
首先需要引入依赖
注意:这个依赖的引入是有讲究的,接口类所在的模块要全部引入
arduino// 依赖 autoService 库 implementation 'com.google.auto.service:auto-service:1.0' annotationProcessor 'com.google.auto.service:auto-service:1.0'
而实现类所在的模块只需要引入注解处理器即可:
arduino// 依赖 autoService 库 annotationProcessor 'com.google.auto.service:auto-service:1.0'
注意 :如果实现类使用的是kotlin编写的,则在引入依赖时需要使用
kapt
,即arduinoapply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' dependencies { kapt 'com.google.auto.service:auto-service:1.0' }
-
原先的接口类不用动,只需修改实现类 只需要在具体的实现类上面加上一个@AutoService注解,参数则为接口的class类。
com.aya.demo.LogImpl.java
typescript@AutoService(ILogInterface.class) public class LogImpl implements ILogInterface{ @Override public void logD(String msg){ Log.d("AYA", msg); } }
-
写一个serverloader工具类SPIUtils.java,同上。
这样,就可以像之前一样使用接口了。
AutoService原理
AutoService的原理就是借助自定义注解,在编译期使用注解处理器扫描注解,并自动生成/resources/META-INF/... 文件。
AutoService工具关键类是 AutoServiceProcessor.java 关键代码如下,加了一些注释:
scss
public class AutoServiceProcessor extends AbstractProcessor {
...
//Multimap:key可以重复
private final Multimap<String, String> providers = HashMultimap.create();
...
/**
* <ol>
* <li> For each class annotated with {@link AutoService}<ul>
* <li> Verify the {@link AutoService} interface value is correct
* <li> Categorize the class by its service interface
* </ul>
*
* <li> For each {@link AutoService} interface <ul>
* <li> Create a file named {@code META-INF/services/<interface>}
* <li> For each {@link AutoService} annotated class for this interface <ul>
* <li> Create an entry in the file
* </ul>
* </ul>
* </ol>
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//该方法数注解处理器的入口
try {
processImpl(annotations, roundEnv);
} catch (RuntimeException e) {
// We don't allow exceptions of any kind to propagate to the compiler
fatalError(getStackTraceAsString(e));
}
return false;
}
private void processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
//如果已经处理结束,则去生成注册文件
generateConfigFiles();
} else {
//处理注解
processAnnotations(annotations, roundEnv);
}
}
//处理注解
private void processAnnotations(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//获取所有添加AutoService注解的类
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoService.class);
log(annotations.toString());
log(elements.toString());
for (Element e : elements) {
// TODO(gak): check for error trees?
TypeElement providerImplementer = MoreElements.asType(e);
//获取AutoService注解value,即声明实现类时注解括号里指定的接口
AnnotationMirror annotationMirror = getAnnotationMirror(e, AutoService.class).get();
//获取value的集合
Set<DeclaredType> providerInterfaces = getValueFieldOfClasses(annotationMirror);
if (providerInterfaces.isEmpty()) {
//如果集合为空,也就是没有指定value,报错,不处理
error(MISSING_SERVICES_ERROR, e, annotationMirror);
continue;
}
//遍历所有value,获取value的完整类名
for (DeclaredType providerInterface : providerInterfaces) {
TypeElement providerType = MoreTypes.asTypeElement(providerInterface);
log("provider interface: " + providerType.getQualifiedName());
log("provider implementer: " + providerImplementer.getQualifiedName());
//判断是否为接口的实现类,是,则存入Multimap类型的providers缓存中,其中key为接口的全路径,value为实现类的全路径;否则报错
if (checkImplementer(providerImplementer, providerType, annotationMirror)) {
providers.put(getBinaryName(providerType), getBinaryName(providerImplementer));
} else {
String message = "ServiceProviders must implement their service provider interface. "
+ providerImplementer.getQualifiedName() + " does not implement "
+ providerType.getQualifiedName();
error(message, e, annotationMirror);
}
}
}
}
//生成spi文件
private void generateConfigFiles() {
Filer filer = processingEnv.getFiler();
//遍历providers的key值
for (String providerInterface : providers.keySet()) {
String resourceFile = "META-INF/services/" + providerInterface;
log("Working on resource file: " + resourceFile);
try {
SortedSet<String> allServices = Sets.newTreeSet();
try {
// would like to be able to print the full path
// before we attempt to get the resource in case the behavior
// of filer.getResource does change to match the spec, but there's
// no good way to resolve CLASS_OUTPUT without first getting a resource.
//"META-INF/services/**"文件夹下已经存在SPI文件
FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",
resourceFile);
log("Looking for existing resource file at " + existingFile.toUri());
// 该SPI文件中的service集合
Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
log("Existing service entries: " + oldServices);
//将spi文件中的service写入缓存
allServices.addAll(oldServices);
} catch (IOException e) {
// According to the javadoc, Filer.getResource throws an exception
// if the file doesn't already exist. In practice this doesn't
// appear to be the case. Filer.getResource will happily return a
// FileObject that refers to a non-existent file but will throw
// IOException if you try to open an input stream for it.
log("Resource file did not already exist.");
}
//根据providers缓存创建service集合
Set<String> newServices = new HashSet<>(providers.get(providerInterface));
//如果新建的都已经存在,则直接返回不处理
if (allServices.containsAll(newServices)) {
log("No new service entries being added.");
return;
}
//将新建的service也加入缓存
allServices.addAll(newServices);
log("New service file contents: " + allServices);
//将所有的service写入文件
FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
resourceFile);
try (OutputStream out = fileObject.openOutputStream()) {
ServicesFiles.writeServiceFile(allServices, out);
}
log("Wrote to: " + fileObject.toUri());
} catch (IOException e) {
fatalError("Unable to create " + resourceFile + ", " + e);
return;
}
}
}
...
}
写SPI的File工具类ServicesFiles.java
java
final class ServicesFiles {
public static final String SERVICES_PATH = "META-INF/services";
private ServicesFiles() { }
...
/**
* Writes the set of service class names to a service file.
*
* @param output not {@code null}. Not closed after use.
* @param services a not {@code null Collection} of service class names.
* @throws IOException
*/
static void writeServiceFile(Collection<String> services, OutputStream output)
throws IOException {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, UTF_8));
for (String service : services) {
writer.write(service);
writer.newLine();
}
writer.flush();
}
}
看了源码后,可以发现,AutoServiceProcessor的主要功能就是将加了AutoService注解的类写到SPI文件中去,而SPI文件的名称根据注解时value的内容来定的,即接口的全路径,SPI文件的内容是实现类的全路径,每个实现类占一行。
使用AutoService时需要注意:
- minSdkVersion最低为26
- 接口的实现类必须是public的
总结一下吧,SPI可以更好的实现组件化,结合AUtoService和ServiceLoader来实现,很方便。当然也可以自己开发一个sdk,把SPIUtils放进去,就可以更方便的使用了。
APT的应用,除了ButterKnife、AUtoService,还有阿里的ARouter,都是程序员的智慧结晶!佩服这些大牛。