SPI介绍
在面向目标的规划中,一般推荐模块之间依据接口编程,通常情况下调用方模块是不会感知到被调用方模块的内部完结。一旦代码里边触及详细完结类,就违反了开闭准则。为了完结在模块安装的时分不用在程序里边动态指明,这就需求一种服务发现机制。Java SPI 便是提供了这样的一个机制:为某个接口寻觅服务完结的机制。
SPI 全称 Service Provider Interface, 字面意思便是:服务提供者的接口。SPI 将服务接口和详细的服务完结分离开来,将服务调用和服务完结者解耦,能够提升程序的拓展性,可维护行。修正或替换服务完结并不需求修正调用方。
截屏2024-03-05 21.14.36
SPI 和API 的差异
从广义上来说,他们都是归于接口,并且十分简略混杂。
img
一般模块之间都是经过接口进行通讯,在服务提供方和服务完结方之间引进一个接口。
当完结方提供了接口和完结,咱们就能够调用完结方的接口从而拥有完结的能力,这种便是API, 接口和完结 都在完结方。
当接口在调用方这边时,便是SPI 了,调用方提供一个接口,然后交给不同的完结方去完结,从而进行提供服务。
根底运用
咱们接下来用一个十分简略的实例来介绍一下SPI 的运用。
服务声明
截屏2024-03-05 21.28.20
项目的模块大概是这个结构,有一个根底 base module ,两个 事务组件 featureA 和 featureB 都依靠着base , app 壳工程引用着两个事务module。
首要咱们在base中创立两个接口:
publicinterfaceIFeatureAInterface{
/*调用A组件*/
voidgetA();
/*调用B组件*/
voidgetB();
}
publicinterfaceIFeatureBInterface{
/*调用B组件*/
voidgetB();
/*调用A组件*/
voidgetA();
}
然后由两个事务组件分别对对应的接口去进行完结。
A组件完结
publicclassFeatureAImplimplementsIFeatureAInterface{
@Override
publicvoidgetA(){
Log.e("wangyilei","调用了A组件的A办法");
}
@Override
publicvoidgetB(){
}
}
B组件完结
publicclassFeatureBImplimplementsIFeatureBInterface{
@Override
publicvoidgetB(){
Log.e("wangyilei","调用了B组件的B办法");
}
@Override
publicvoidgetA(){
}
}
然后在对应事务对接口完结进行声明,创立文件结构如下:
│├──main
││├──AndroidManifest.xml
││├──java
│││└──com
│││└──xl
│││└──featurea
│││└──FeatureAImpl.java
││└──resources
││└──META-INF
││└──services
││└──com.xl.base.IFeatureAInterface
│├──main
││├──AndroidManifest.xml
││├──java
│││└──com
│││└──xl
│││└──featureb
│││└──FeatureBImpl.java
││└──resources
││└──META-INF
││└──services
││└──com.xl.base.IFeatureBInterface
在main下,创立与java 同级的 resources 目录,在然后再创立MATE-INF ,services 目录,然后在services目录中创立声明文件,文件称号以接口的全途径命令,文件内容为接口完结的全途径名。
com.xl.base.IFeatureAInterface:com.xl.featurea.FeatureAImpl
com.xl.base.IFeatureBInterface:com.xl.featureb.FeatureBImpl
这样咱们的接口完结就声明好了,接下来咱们就需求借助 ServiceLoader 来 获取这个服务进行调用。
服务调用
想要运用java 的SPI 机制,是需求依靠ServiceLoader来完结的,详细运用如下:
//App壳工程
ServiceLoader<IFeatureAInterface>load=ServiceLoader.load(IFeatureAInterface.class);
Iterator<IFeatureAInterface>iterator=load.iterator();
while(iterator.hasNext()){
iterator.next().getA();
}
调用方法十分简略,这样咱们就能够在App的壳工程中调用到featureA模块的办法。
配合AutoService运用
假如咱们每次新建一个接口和完结的,都需求向META-INF.services中写一次声明的话,太过于繁琐了,而咱们借助AutoService,能够让它来帮咱们生成。
首要引进依靠
kapt'com.google.auto.service:auto-service:1.0-rc4'
@AutoService(IFeatureBInterface.class)
publicclassFeatureBImplimplementsIFeatureBInterface{
@Override
publicvoidgetB(){
......
}
@Override
publicvoidgetA(){
......
}
}
运用方法也十分简略,便是在完结类上加上AutoService注解,参数为对应接口的class即可。
经过编译之后在能够在对应module 的 build/intermediates/runtime_library_classes_dir/debug/META-INF/services/ 目录下 看到生成的声明文件。
完结原理
咱们回过头来看下上文提到的ServiceLoader,由于咱们的调用都是经过它来完结的,咱们来看下它是如何完结服务发现的。
ServiceLoader 是一个被final润饰的类,不可被继承修正,同时它完结了Iterable接口,这是为了便利后续能够经过迭代循环的方法找到对应的服务完结类。
咱们先从ServiceLoader#load办法看:
publicstatic<S>ServiceLoader<S>load(Class<S>service){
ClassLoadercl=Thread.currentThread().getContextClassLoader();
returnServiceLoader.load(service,cl);
}
publicstatic<S>ServiceLoader<S>load(Class<S>service,ClassLoaderloader){
returnnewServiceLoader<>(service,loader);
}
privateServiceLoader(Class<S>svc,ClassLoadercl){
service=Objects.requireNonNull(svc,"Serviceinterfacecannotbenull");
loader=(cl==null)?ClassLoader.getSystemClassLoader():cl;
reload();
}
publicvoidreload(){
providers.clear();
lookupIterator=newLazyIterator(service,loader);
}
这儿首要是ServiceLoader目标的创立,最终的找到服务完结的作业是在一个内部完结类 LazyIterator 中完结的。
接下来咱们看下ServiceLoader#iterator 办法做了些什么
publicIterator<S>iterator(){
returnnewIterator<S>(){
Iterator<Map.Entry<String,S>>knownProviders=providers.entrySet().iterator();
publicbooleanhasNext(){
if(knownProviders.hasNext())
returntrue;
returnlookupIterator.hasNext();//调用LazyIterator
}
publicSnext(){
if(knownProviders.hasNext())
returnknownProviders.next().getValue();
returnlookupIterator.next();//调用LazyIterator
}
publicvoidremove(){
thrownewUnsupportedOperationException();
}
};
}
这儿首要是迭代循环查找完结类的时分先从 providers 缓存中查找,假如缓存没有射中 就在 LazyIterator 中查找。
在调用LazyIterator 迭代的时分首要如下:
publicbooleanhasNext(){
returnhasNextService();
}
privatebooleanhasNextService(){
if(nextName!=null){
returntrue;
}
if(configs==null){
try{
//经过PREFIX(META-INF/services/)和类名获取对应的配置文件,得到详细的完结类
StringfullName=PREFIX+service.getName();
if(loader==null)
//加载服务的声明文件
configs=ClassLoader.getSystemResources(fullName);
else
configs=loader.getResources(fullName);
}catch(IOExceptionx){
fail(service,"Errorlocatingconfigurationfiles",x);
}
}
while((pending==null)||!pending.hasNext()){
if(!configs.hasMoreElements()){
returnfalse;
}
pending=parse(service,configs.nextElement());
}
//获取到完结类的称号
nextName=pending.next();
returntrue;
}
publicSnext(){
returnnextService();
}
privateSnextService(){
if(!hasNextService())
thrownewNoSuchElementException();
Stringcn=nextName;
nextName=null;
Class<?>c=null;
try{
c=Class.forName(cn,false,loader);
}catch(ClassNotFoundExceptionx){
....
}
....
try{
//创立完结类的目标
Sp=service.cast(c.newInstance());
//放入到缓存调集中
providers.put(cn,p);
returnp;
}catch(Throwablex){
.....
}
thrownewError();//Thiscannothappen
}
简略来说,便是经过声明文件的称号来加载这个服务的声明文件,经过这个声明文件来获取到完结服务的完整称号,再经过反射来创立完结服务的实例,然后存到缓存中供调用者运用。
AutoService原理
AutoService的原理首要分为三步:
1.在开发阶段,运用注解对接口完结类润饰润饰。
2.在编译期阶段,注解处理器去遍历被注解润饰的类的信息,然后收集到写入到META-INF/services文件夹下的文件中。
3.在运用阶段,经过ServiceLoader去MATE-INF/services文件加下查找指定文件,然后解析获取到需求加载的类信息。
这儿AutoService 运用的是APT技能,APT全称 Annotation Processing Tool
,翻译为注解处理器,是一种处理注解的工具,它对源码文件进行检测找出其间注解,并进行额外的处理。
APT能够在编译期阶段,帮助咱们自动生成代码,简化运用。
AutoService中的 AutoServiceProcessor 继承了抽象类 AbstractProcessor :
publicclassAutoServiceProcessorextendsAbstractProcessor{
@Override
publicbooleanprocess(Set<?extendsTypeElement>annotations,RoundEnvironmentroundEnv){
try{
returnprocessImpl(annotations,roundEnv);
}catch(Exceptione){
....
returntrue;
}
}
privatebooleanprocessImpl(Set<?extendsTypeElement>annotations,RoundEnvironmentroundEnv){
//注解是否处理完结
if(roundEnv.processingOver()){
//生成声明配置文件
generateConfigFiles();
}else{
//解析注解信息
processAnnotations(annotations,roundEnv);
}
returntrue;
}
}
所有的注解操作都是在 process 办法中完结的。
privatevoidprocessAnnotations(Set<?extendsTypeElement>annotations,RoundEnvironmentroundEnv){
//获取被注解润饰的类的调集
Set<?extendsElement>elements=roundEnv.getElementsAnnotatedWith(AutoService.class);
for(Elemente:elements){
TypeElementproviderImplementer=(TypeElement)e;
//获取注解信息
AnnotationMirrorannotationMirror=getAnnotationMirror(e,AutoService.class).get();
//获取注解中的值
Set<DeclaredType>providerInterfaces=getValueFieldOfClasses(annotationMirror);
//注解中没有值抛出反常
if(providerInterfaces.isEmpty()){
error(MISSING_SERVICES_ERROR,e,annotationMirror);
continue;
}
//遍历注解上的值
for(DeclaredTypeproviderInterface:providerInterfaces){
TypeElementproviderType=MoreTypes.asTypeElement(providerInterface);
//判断该子类的类型与AutoService.value中的值是否一致,假如一致添加到providers缓存中。
if(checkImplementer(providerImplementer,providerType)){
providers.put(getBinaryName(providerType),getBinaryName(providerImplementer));
}else{
........
}
}
}
}
流程小结:
1.获取被注解润饰的类的调集
2.遍历被AutoService注解润饰的类
3.获取注解的value值
4.遍历获取到的value值。
5.假如该类与注解中的value值一致的话存入缓存中。
privatevoidgenerateConfigFiles(){
Filerfiler=processingEnv.getFiler();
//遍历缓存中的信息
for(StringproviderInterface:providers.keySet()){
//依据缓存信息拼接文件名,一个接口对应一个文件。
StringresourceFile="META-INF/services/"+providerInterface;
try{
SortedSet<String>allServices=Sets.newTreeSet();
try{
//META-INF/services/下现已存在的声明配置文件
FileObjectexistingFile=filer.getResource(StandardLocation.CLASS_OUTPUT,"",resourceFile);
//声明配置文件中声明的服务调集
Set<String>oldServices=ServicesFiles.readServiceFile(existingFile.openInputStream());
allServices.addAll(oldServices);
}catch(IOExceptione){
......
}
Set<String>newServices=newHashSet<String>(providers.get(providerInterface));
//假如要创立的服务现已存在就略过
if(allServices.containsAll(newServices)){
return;
}
allServices.addAll(newServices);
//创立对应的声明配置文件
FileObjectfileObject=filer.createResource(StandardLocation.CLASS_OUTPUT,"",resourceFile);
OutputStreamout=fileObject.openOutputStream();
ServicesFiles.writeServiceFile(allServices,out);
out.close();
}catch(IOExceptione){
....
return;
}
}
}
流程小结:
1.遍历缓存的信息
2.依据缓存的信息拼接文件名,一个接口对应一个文件
3.读取 META-INF/services/下 现已存在的声明配置文件,假如对应的声明文件现已存在就直接返回。
4.创立对应的声明文件
至此,AutoService的原理就剖析完了,简答来说便是使用APT技能来帮助咱们生成声明文件,大大简化了咱们的运用。
总结
其实不难发现,SPI的实质是经过反射来完结的,咱们依照规定将暴露对外运用的接口和完结类在META-INF.service下进行声明,这些操作由AutoService使用APT技能来帮助咱们生成了。
经过SPI技能能够大大地进步接口灵活性。