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技术能够大大地提高接口灵活性。

相关推荐
dgiij26 分钟前
AutoX.js向后端传输二进制数据
android·javascript·websocket·node.js·自动化
SevenUUp1 小时前
Android Manifest权限清单
android
高林雨露1 小时前
Android 检测图片抓拍, 聚焦图片后自动完成拍照,未对准图片的提示请将摄像头对准要拍照的图片
android·拍照抓拍
wilanzai1 小时前
Android View 的绘制流程
android
INSBUG3 小时前
CVE-2024-21096:MySQLDump提权漏洞分析
android·adb
Mercury Random4 小时前
Qwen 个人笔记
android·笔记
苏苏码不动了4 小时前
Android 如何使用jdk命令给应用/APK重新签名。
android
aqi005 小时前
FFmpeg开发笔记(五十三)移动端的国产直播录制工具EasyPusher
android·ffmpeg·音视频·直播·流媒体
xiaoduyyy6 小时前
【Android】ToolBar,滑动菜单,悬浮按钮和可交互提示等的使用方法
android
liyy6146 小时前
Android架构组件:MVVM模式的实战应用与数据绑定技巧
android