Java

1、什么是 SPI

SPI 全称为 Service Provider Interface,在很多框架都有使用,例如 JDBC、SLF4J、Dubbo 等,是一种基于接口编程的方式。
SPI 将服务接口和具体的服务实现分离开,将服务使用者和服务实现者解耦,提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改服务使用者。
拿最常用的日志框架 SLF4J 举例:SLF4J 的后端可以是 Logback、Log4j2 等,而且可以换来换去,连项目里面的代码都不要更改,也就是换个 Jar 包的事情(Maven、Gradle 改一下依赖而已),而这种就是靠 SPI 实现的。
2021-06-25-21-00-58-095317.png
SLF4J 实现和具体的日志框架分离
SPI 的最重要的作用就是再无需修改原始代码库的情况下,通过使用“插件”的方式新增新的功能实现、修改或者移除旧的功能实现。
Service:服务,也就是为上层应用程序提供特定的功能的访问。服务可以自定义功能接口以及使用接口的方式,定义的一组公关接口和抽象类。
Service Provider Interface:服务提供的接口抽象类。
SLF4J : 就是一个服务 Service,为应用程序提供了使用日志功能的访问

  1. Logger log = LoggerFactory.getLogger(XX.class);
  2. log.info("输出 info 的日志")

同样, SLF4J 也提供了一组接口类,这就是 Service Provider Interface。
Service Provider:实现 Service Provider Interface,也就是实现具体的功能。
例如 Logback,实现了 SLF4J 的接口,这样子就可以通过 SLF4J 来输出日志。达到了应用程序和具体的日志输出框架的解耦。

2、SPI 示例

这里通过一个简单的 日志输出 的示例,来详解 SPI 以及 SPI 的工作原理。

2.1、实现 Service Provider Interface

在 IDEA 新建一个普通的 Java 项目,service-provider-interface, 目录结构如下

  1. └── src
  2. ├── META-INF
  3. └── MANIFEST.MF
  4. └── org
  5. └── spi
  6. └── service
  7. ├── Logger.java
  8. ├── LoggerService.java
  9. ├── Main.java
  10. └── MyServicesLoader.java

新建 Logger.java,这个就是 SPI,Service provider interface 就是服务提供的接口,Service Provider 服务提供实现这个接口。

  1. /**
  2. * 这个是 Service provider interface (SPI):服务提供商接口
  3. */
  4. public interface Logger {
  5. void info(String msg);
  6. void debug(String msg);
  7. }

新建 LoggerService.java , 这个就是服务 Service,为使用者提供特定功能的访问。
ServiceLoader 这个类后面会介绍

  1. public class LoggerService {
  2. private static final LoggerService SERVICE = new LoggerService();
  3. private final Logger logger;
  4. private final List<Logger> loggerList;
  5. private LoggerService(){
  6. ServiceLoader<Logger> loader = ServiceLoader.load(Logger.class);
  7. List<Logger> list = new ArrayList<>();
  8. for(Logger log: loader){
  9. list.add(log);
  10. }
  11. // LoggerList 是所有 ServiceProvider
  12. loggerList = list;
  13. if(!list.isEmpty()){
  14. // Logger 只取一个
  15. logger = list.get(0);
  16. }else {
  17. logger = null;
  18. }
  19. }
  20. public static LoggerService getService(){
  21. return SERVICE;
  22. }
  23. public void info(String msg){
  24. if(logger == null) {
  25. System.out.println("info 中没有发现 Logger 服务提供者");
  26. }else{
  27. logger.info(msg);
  28. }
  29. }
  30. public void debug(String msg){
  31. if(loggerList.isEmpty()){
  32. System.out.println("debug 中没有发现 Logger 服务提供者");
  33. }
  34. loggerList.forEach(log -> log.debug(msg));
  35. }
  36. }

新建 Main.java 类,也就是服务使用者,这里为了方便展示就没有新建一个新项目了

  1. public class Main {
  2. public static void main(String[] args) {
  3. LoggerService service = LoggerService.getService();
  4. service.info("哈哈哈");
  5. service.debug("嘻嘻嘻");
  6. }
  7. }

直接运行,会看到输出:

  1. info 中没有发现 Logger 服务提供者
  2. debug 中没有发现 Logger 服务提供者

将整个项目打包成 jar,这里通过 IDEA 的方式打包,打开 Project Structure 的设置界面,设置完成后运行 “Build” -> “Build Artifacts”。
2021-06-25-21-00-58-404491.png
IDEA 打包 Jar 设置
2021-06-25-21-01-01-595853.png
IDEA 设置打包 JAR

2.2、实现 Service Provider

这里将实现上文的 Logger 接口
新建项目 service-provider,目录结构如下,其中 service-provider-interface.jar 是上文项目打包成的 jar 包。需要在 “Project Structure” 设置中将这个 jar 包添加到项目。

  1. .
  2. ├── libs
  3. └── service-provider-interface.jar
  4. └── src
  5. ├── META-INF
  6. ├── MANIFEST.MF
  7. └── services
  8. └── org.spi.service.Logger
  9. └── org
  10. └── spi
  11. └── provider
  12. └── Logback.java

新建 Logback.java :实现 Logger.java

  1. public class Logback implements Logger {
  2. @Override
  3. public void info(String msg) {
  4. System.out.println("Logback info 的输出" + msg);
  5. }
  6. @Override
  7. public void debug(String msg) {
  8. System.out.println("Logback debug 的输出" + msg);
  9. }
  10. }

在 src 目录下新建 META-INF/services 的文件夹,新建 org.spi.service.Logger 的文件。
这个文件名就是上文 Logger 接口类的 包名+类名。
内容就是实现 Logger 接口的类的 包名+类名。
这个是 JDK 的 ServiceLoader 规定的

  1. org.spi.provider.Logback

参考上文,打包成 jar 包。

2.3、使用 Service Provider

回到 service-provider-interface 的项目,将 service-provider 的 jar 包通过 “Project Structure”引入,重新运行 Mian.java,此时输出变成了:

  1. Logback info 的输出哈哈哈
  2. Logback debug 的输出嘻嘻嘻

通过使用 SPI 的方式,可以发现服务(LoggerService)、服务提供者(Logback)两者之间的耦合度非常低:
例如需要替换 Logback,换一个新的实现 LogbackPlus,只需替换 jar 即可,就如同 SLF4J 一样。
也就是 LoggerService.info() 的方式,只使用一个 ServiceProvider
例如某天新的需求,需要(Logback)日志输出到 控制台 的同时,还需要发送到 Kafka,那么只需要新加一个发送到 Kafka 功能的 jar 即可,而无需修改原有的 Logback
也就是 LoggerService.debug() 的使用方式,同时调用所有 Service Provider 的实现。

3、ServiceLoader 的作用

前面提到过 ServiceLoader 这个类的作用会在后面解析,这是 JDK 提供的一个类,其功能就是从读取 所有 jar 包下的 META-INF/services/ 的文件,然后通过反射的方式实例化 ServiceProvider 。
这也是为什么上文的 service-provider 需要 org.spi.service.Logger 文件的原因。
这里实现一个简易版的 ServiceLoader 。

  1. public class MyServicesLoader<S> {
  2. // 对应服务的接口
  3. private final Class<S> service;
  4. // 暂存所有的 Provider 的实例
  5. private final List<S> providers = new ArrayList<>();
  6. private final ClassLoader classLoader;
  7. public static <S> MyServicesLoader<S> load(Class<S> service){
  8. return new MyServicesLoader<>(service);
  9. }
  10. private MyServicesLoader(Class<S> service){
  11. this.service = service;
  12. this.classLoader = Thread.currentThread().getContextClassLoader();
  13. doLoad();
  14. }
  15. private void doLoad(){
  16. try {
  17. // 读取所有 jar 包的 META-INF/services/" + service.getName() 文件
  18. Enumeration<URL> urls = classLoader.getResources("META-INF/services/" + service.getName());
  19. // 遍历
  20. while(urls.hasMoreElements()) {
  21. URL u = urls.nextElement();
  22. // 打印路径 path
  23. System.out.println("File =" + u.getPath());
  24. // 读取 path 路径下的内容
  25. URLConnection uc = u.openConnection();
  26. uc.setUseCaches(false);
  27. InputStream inputStream = uc.getInputStream();
  28. BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
  29. String className = reader.readLine();
  30. while (className != null){
  31. Class<?> clazz = Class.forName(className, false, classLoader);
  32. if(service.isAssignableFrom(clazz)){
  33. // 反射出实例
  34. Constructor<? extends S> ctor = (Constructor<? extends S>) clazz.getConstructor();
  35. S instance = ctor.newInstance();
  36. providers.add(instance);
  37. }
  38. className = reader.readLine();
  39. }
  40. }
  41. } catch (Exception ignored){ }
  42. }
  43. public List<S> getProviders() {
  44. return providers;
  45. }
  46. }

如何使用

  1. MyServicesLoader<Logger> loggers = MyServicesLoader.load(Logger.class);
  2. loggers.getProviders().forEach(provider -> provider.info("这个是 MyServicesLoader "));