Java Spi是如何找到你的實現的? ——Java SPI原理與實踐

發表於2024-02-18

什麼是SPI

SPI的全稱是Service Provider Interface,顧名思義即服務提供者介面,相比API Application Programming Interface他們的不同之處在於API是應用提供給外部的功能,而SPI則更傾向於是規定好規範,具體實現由使用方自行實現。

為什麼要使用SPI

SPI提供方提供介面定義,使用方負責實現,這種方式更有利於解藕程式碼。在有統一標準,但是不確定使用場景的場合非常適用。

怎麼使用SPI

接下來我會用一個簡單的例子來介紹如何使用SPI

首先我們在二方包中定義一個介面Plugin

public interface Plugin {
    String getName();

    void execute();
}

然後將二方包編譯打包後在自己的應用專案中引入,之後實現二方包中的介面Plugin,下面我寫了三個不同的實現:

public class DBPlugin implements Plugin {
    @Override
    public String getName() {
        return "database";
    }

    @Override
    public void execute() {
        System.out.println("execute database plugin");
    }
}
public class MqPlugin implements Plugin {
    @Override
    public String getName() {
        return "mq";
    }

    @Override
    public void execute() {
        System.out.println("execute mq plugin");
    }
}
public class RedisPlugin implements Plugin {
    @Override
    public String getName() {
        return "redis";
    }

    @Override
    public void execute() {
        System.out.println("execute redis plugin");
    }
}

之後在resources目錄下的META-INF.services目錄中新增以介面全限定名命名的檔案。最後在這個檔案中新增上述三個實現的全限定名就完成了配置。

com.example.springprovider.spi.impl.DBPlugin
com.example.springprovider.spi.impl.MqPlugin
com.example.springprovider.spi.impl.RedisPlugin

然後我們編寫一段程式碼來看下我們的幾個SPI的實現是否已經裝載成功了。

public void spiTest() {
    ServiceLoader<Plugin> serviceLoader = ServiceLoader.load(Plugin.class);

    for (Plugin plugin : serviceLoader) {
        System.out.println(plugin.getName());
        plugin.execute();
    }
}

執行程式碼,結果已經正常輸出,上述配置成功!

SPI的原理

上述的例子是成功的執行起來了,但是大家應該還是會有問題,為什麼這麼配置就可以執行了?檔名或者路徑一定就需要按照上述的規定來配置嗎?

要了解這些問題,我們就需要從原始碼的角度來深入的看一下。

此處使用JDK8的原始碼來進行講解,JDK9之後引入了module機制導致這部分程式碼為了相容module也進行了大改,變得更為複雜不利於理解,因此如果有興趣可以自行了解

要了解SPI的實現,最主要的就是ServiceLoader,這個類是SPI的主要實現。

private static final String PREFIX = "META-INF/services/";

// The class or interface representing the service being loaded
private final Class<S> service;

// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;

// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;

// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// The current lazy-lookup iterator
private LazyIterator lookupIterator;

ServiceLoader定義了一系列成員變數,其中最重要的兩個,providers是一個快取搜尋結果的map,lookupIterator是用來搜尋指定類的自定義迭代器。除此之外我們還可以看到定義了一個固定的PREFIXMETA-INF/services/,這個就是SPI預設的搜尋路徑。

在自定義迭代器LazyIterator中定義了nextServicehasNextService,這兩個就是SPI搜尋實現類的核心方法。

hasNextService邏輯很簡單,主要是讀取META-INF/services/介面檔案中定義的實現類檔案,然後將這個檔案進行解析以求找到相應的實現類並載入

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            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;
}

nextService主要是裝載類,然後經過判斷後放置入快取的map中

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) {
        fail(service,
                "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service,
                "Provider " + cn  + " not a subtype");
    }
    try {
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
                "Provider " + cn + " could not be instantiated",
                x);
    }
    throw new Error();          // This cannot happen
}

接下來在parse函式中呼叫parseLine,在parseLine中解析最終的實現類並返回。至此完整的解析邏輯我們都已經清晰的看到了,回過頭再來看開始的問題應該也都能夠引刃而解了!

AutoService

很多人會覺得SPI的使用上會有一些麻煩,需要建立目錄並且配置相關的檔案,後續SPI產生變動還需要額外維護這個檔案會很頭疼。那麼我在這裡介紹一個SPI的便捷工具,由Google推出的AutoService工具。

使用方法很簡單,在程式碼中引入依賴:

<dependency>
    <groupId>com.google.auto.service</groupId>
    <artifactId>auto-service</artifactId>
    <version>1.0.1</version>
</dependency>

之後直接在實現類上新增註解@AutoService(MyProvider.class)MyProvider配置為介面類即可。

那麼這裡就又有問題了,為什麼AutoService一個註解就能夠實現了而不用像JDK標準那樣生成檔案呢?想知道答案的話我們就又又又需要來看原始碼了。

找到AutoService關鍵的核心原始碼:

private void generateConfigFiles() {
    Filer filer = processingEnv.getFiler();

    for (String providerInterface : providers.keySet()) {
      String resourceFile = "META-INF/services/" + providerInterface;
      log("Working on resource file: " + resourceFile);
      try {
        SortedSet<String> allServices = Sets.newTreeSet();
        try {
          FileObject existingFile =
              filer.getResource(StandardLocation.CLASS_OUTPUT, "", resourceFile);
          log("Looking for existing resource file at " + existingFile.toUri());
          Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
          log("Existing service entries: " + oldServices);
          allServices.addAll(oldServices);
        } catch (IOException e) {
          log("Resource file did not already exist.");
        }

        Set<String> newServices = new HashSet<>(providers.get(providerInterface));
        if (!allServices.addAll(newServices)) {
          log("No new service entries being added.");
          continue;
        }

        log("New service file contents: " + allServices);
        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;
      }
    }
  }

我們可以發現AutoService的核心思路其實很簡單,就是透過註解的形式簡化你的配置,然後將對應的資料夾以及檔案內容由AutoService程式碼來自動生成。如此的話就不會有相容性問題和後續的版本迭代的問題。

總結

SPI是一種便捷的可擴充套件方式,在實際的開源專案中也被廣泛運用,在本文中我們深入原始碼瞭解了SPI的原理,弄清楚了SPI使用過程中的一些為什麼。除此之外也找到了更加便捷的工具AutoService以及弄清楚了他的底層便捷的邏輯是什麼。雖然因為內容較多可能為能把所有細節展示出來,但是整體上大家也能夠有一個大致的瞭解。如果還有問題,可以在評論區和我互動哦~

相關文章