Android SPI的基础使用与原理

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中创建两个接口:

csharp 复制代码
public interface IFeatureAInterface {
    /*调用A组件*/
  void  getA();
  /*调用B组件*/
  void getB();
}
public interface IFeatureBInterface {
    /*调用B组件*/
    void getB();
    /*调用A组件*/
    void getA();
}

然后由两个业务组件分别对对应的接口去进行实现。

typescript 复制代码
A组件实现
public class FeatureAImpl implements IFeatureAInterface {
    @Override
    public void getA() {
        Log.e("wangyilei", "调用了A组件的A方法");
    }
    @Override
    public void getB() {
       
    }
}
B组件实现
public class FeatureBImpl implements IFeatureBInterface {
    @Override
    public void getB() {
        Log.e("wangyilei", "调用了B组件的B方法");
    }

    @Override
    public void getA() {
     
    }
}

然后在对应业务对接口实现进行声明,创建文件结构如下:

csharp 复制代码
│       ├── 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目录中创建声明文件,文件名称以接口的全路径命令,文件内容为接口实现的全路径名。

csharp 复制代码
com.xl.base.IFeatureAInterface : com.xl.featurea.FeatureAImpl
com.xl.base.IFeatureBInterface :  com.xl.featureb.FeatureBImpl

这样我们的接口实现就声明好了,接下来我们就需要借助 ServiceLoader 来 获取这个服务进行调用。

服务调用

想要使用java 的SPI 机制,是需要依赖ServiceLoader来实现的,具体使用如下:

vbnet 复制代码
//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,可以让它来帮我们生成。

首先引入依赖

arduino 复制代码
kapt 'com.google.auto.service:auto-service:1.0-rc4'
less 复制代码
@AutoService(IFeatureBInterface.class)
public class FeatureBImpl implements IFeatureBInterface {
    @Override
    public void getB() {
        ......
    }
    @Override
    public void getA() {
       ......
    }
}

使用方式也非常简单,就是在实现类上加上AutoService注解,参数为对应接口的class即可。

经过编译之后在可以在对应module 的 build/intermediates/runtime_library_classes_dir/debug/META-INF/services/ 目录下 看到生成的声明文件。

实现原理

我们回过头来看下上文提到的ServiceLoader,因为我们的调用都是通过它来实现的,我们来看下它是如何完成服务发现的。

ServiceLoader 是一个被final修饰的类,不可被继承修改,同时它实现了Iterable接口,这是为了方便后续可以通过迭代循环的方式找到对应的服务实现类。

我们先从ServiceLoader#load方法看:

scss 复制代码
    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();
    }

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

这里主要是ServiceLoader对象的创建,最后的找到服务实现的工作是在一个内部实现类 LazyIterator 中完成的。

接下来我们看下ServiceLoader#iterator 方法做了些什么

typescript 复制代码
    public Iterator<S> iterator() {
        return new Iterator<S>() {
            Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();// 调用 LazyIterator
            }

            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();// 调用 LazyIterator
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

这里主要是迭代循环查找实现类的时候先从 providers 缓存中查找,如果缓存没有命中 就在 LazyIterator 中查找。

在调用LazyIterator 迭代的时候主要如下:

typescript 复制代码
        public boolean hasNext() {
                return hasNextService();
        }

        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    //通过PREFIX(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;
                }
                pending = parse(service, configs.nextElement());
            }
            //获取到实现类的名称
            nextName = pending.next();
            return true;
        }
typescript 复制代码
        public S next() {
                return nextService();
        }
        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) {
                ....
            }
            ....
            try {
                //创建实现类的对象
                S p = service.cast(c.newInstance());
                //放入到缓存集合中
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                .....
            }
            throw new Error();          // This cannot happen
        }

简单来说,就是通过声明文件的名称来加载这个服务的声明文件,通过这个声明文件来获取到实现服务的完整名称,再通过反射来创建实现服务的实例,然后存到缓存中供调用者使用。

AutoService原理

AutoService的原理主要分为三步:

1.在开发阶段,使用注解对接口实现类修饰修饰。

2.在编译期阶段,注解处理器去遍历被注解修饰的类的信息,然后收集到写入到META-INF/services文件夹下的文件中。

3.在使用阶段,通过ServiceLoader去MATE-INF/services文件加下查找指定文件,然后解析获取到需要加载的类信息。

这里AutoService 使用的是APT技术,APT全称 Annotation Processing Tool ,翻译为注解处理器,是一种处理注解的工具,它对源码文件进行检测找出其中注解,并进行额外的处理。

APT能够在编译期阶段,帮助我们自动生成代码,简化使用。

AutoService中的 AutoServiceProcessor 继承了抽象类 AbstractProcessor :

typescript 复制代码
public class AutoServiceProcessor extends AbstractProcessor {
 @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){
    try {
      return processImpl(annotations, roundEnv);
    } catch (Exception e) {
      ....
      return true;
    }
  }

private boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    //注解是否处理完成
    if (roundEnv.processingOver()) {
      //生成声明配置文件
      generateConfigFiles();
    } else {
      //解析注解信息
      processAnnotations(annotations, roundEnv);
    }
    return true;
  }
}

所有的注解操作都是在 process 方法中完成的。

scss 复制代码
  private void processAnnotations(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    //获取被注解修饰的类的集合
    Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoService.class);
    for (Element e : elements) {
      TypeElement providerImplementer = (TypeElement) e;
      //获取注解信息
      AnnotationMirror annotationMirror = getAnnotationMirror(e, AutoService.class).get();
      //获取注解中的值
      Set<DeclaredType> providerInterfaces = getValueFieldOfClasses(annotationMirror);
      //注解中没有值 抛出异常 
      if (providerInterfaces.isEmpty()) {
        error(MISSING_SERVICES_ERROR, e, annotationMirror);
        continue;
      }
      //遍历注解上的值
      for (DeclaredType providerInterface : providerInterfaces) {
        TypeElement providerType = 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值一致的话存入缓存中。

ini 复制代码
  private void generateConfigFiles() {
    Filer filer = processingEnv.getFiler();
    //遍历缓存中的信息
    for (String providerInterface : providers.keySet()) {
      //根据缓存信息 拼接文件名,一个接口对应一个文件。
      String resourceFile = "META-INF/services/" + providerInterface;
      try {
        SortedSet<String> allServices = Sets.newTreeSet();
        try {
          //META-INF/services/下 已经存在的声明配置文件
          FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",  resourceFile);
          //声明配置文件中 声明的服务集合
          Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
          allServices.addAll(oldServices);
        } catch (IOException e) {
          ......
        }
        Set<String> newServices = new HashSet<String>(providers.get(providerInterface));
        //如果要创建的服务已经存在就略过 
        if (allServices.containsAll(newServices)) {
          return;
        }
        allServices.addAll(newServices);
        //创建对应的声明配置文件
        FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",resourceFile);
        OutputStream out = fileObject.openOutputStream();
        ServicesFiles.writeServiceFile(allServices, out);
        out.close();
      } catch (IOException e) {
        ....
        return;
      }
    }
  }

流程小结:

1.遍历缓存的信息

2.根据缓存的信息拼接文件名,一个接口对应一个文件

3.读取 META-INF/services/下 已经存在的声明配置文件,如果对应的声明文件已经存在就直接返回。

4.创建对应的声明文件

至此,AutoService的原理就分析完了,简答来说就是利用APT技术来帮助我们生成声明文件,大大简化了我们的使用。

总结

其实不难发现,SPI的本质是通过反射来完成的,我们按照规定将暴露对外使用的接口和实现类在META-INF.service下进行声明,这些操作由AutoService利用APT技术来帮助我们生成了。

通过SPI技术能够大大地提高接口灵活性。

相关推荐
松仔log9 小时前
Java中级——组合和继承
android·java·开发语言
我命由我1234513 小时前
Jetpack Compose - MaterialExpressiveTheme 与 MaterialTheme、ColorScheme
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
lilian23314 小时前
Harmony os 技术实战|拼豆制图06:收藏 ID、生成记录与重启恢复怎么不打架
android·java·数据库·harmonyos
2601_9557596214 小时前
ClaudeAPI成本中心与业务标签设计指南
android·java·数据库
余衫马14 小时前
2026最新版!Android Studio 极速安装与配置指南
android·ide·android studio
AFinalStone14 小时前
Android 7系统异常问题排查(五)Framework层(下)—System Server崩溃
android·tombstone·系统异常
AFinalStone14 小时前
Android 7系统异常问题排查(八)系统追踪—Trace机制与性能诊断
android·系统异常
极客猴子18 小时前
Android录音转写频繁卡顿?多款APP长时间会议场景稳定性实测
android·人工智能·智能手机·飞书
Kapaseker18 小时前
Boolean 变量到底该怎么命名?
android·kotlin
AFinalStone18 小时前
Android 7系统异常问题排查(六)应用异常(上)—ANR机制全解
android·系统异常