SPI是Service Provider Interfaces的簡稱。根據Java的SPI規範,我們可以定義一個服務接口,具體的實現由對應的實現者去提供,即Service Provider(服務提供者)。然後在使用的時候只要根據SPI的規範去獲取對應的服務提供者的服務實現即可。爲了便於理解,我們先來看一個使用SPI的示例。下載
假設我們有一個日誌服務LogService,其只定義了一個info方法用於輸出日誌信息,我們希望把它作爲SPI,然後具體的實現由對應的服務提供者去實現。LogService的定義如下所示。
package com.elim.learn.basic.spi.service;
public interface LogService {
public void info(String msg);
}
然後基於這個服務我們會有自己的實現,示例中筆者用了三個實現,分別是ConsoleLogService、FileLogService和DBLogService,其實現都只是簡單的打印一下日誌類別信息,ConsoleLogService的實現如下所示,其它兩個是類似的。下載
package com.elim.learn.basic.spi.service.impl;
import com.elim.learn.basic.spi.service.LogService;
public class ConsoleLogService implements LogService {
@Override
public void info(String msg) {
System.out.println("----console log ----");
}
}
根據SPI的規範我們的服務實現類必須有一個無參構造方法。我們的SPI服務提供者需要將其在classpath下的META-INF/services目錄下以服務接口全路徑名命名的文件中寫對應的實現類的全路徑名稱,每一行代表一個實現,如果需要註釋信息可以使用“#”進行註釋,根據官方的要求,這個文件的編碼格式必須是UTF-8。我們示例中的LogService的全路徑名是com.elim.learn.basic.spi.service.LogService,所以我們需要在類路徑下的META-INF/services目錄下創建一個名稱爲com.elim.learn.basic.spi.service.LogService文件。在本示例中我們一個提供了三個實現,所以該文件的內容如下。下載
#console
com.elim.learn.basic.spi.service.impl.ConsoleLogService
#file
com.elim.learn.basic.spi.service.impl.FileLogService
#db
com.elim.learn.basic.spi.service.impl.DBLogService
至此,我們的服務定義和實現配置就完成了,接下來就是使用了。使用的時候核心是ServiceLoader類,我們需要通過這個工具類來加載服務提供者,即對應的服務實現,也需要通過它來獲得對應的服務實現。ServiceLoader類的核心入口是其提供的三個可以創建ServiceLoader實例的靜態方法,分別是load(Class<?> , ClassLoader)、load(Class<?>)和loadInstalled(Class<?>),三者的區別就在於使用的ClassLoader不一樣。load(Class<?>)方法將使用當前線程持有的ClassLoader,loadInstalled(Class<?>)方法將使用最頂級的ClassLoader。三者的實現分別如下。
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
ServiceLoader是實現了java.util.Iterator接口的,而且是基於我們所使用的服務的實現,所以可以通過ServiceLoader的實例來遍歷其中的服務實現者,從而調用對應的服務提供者。示例如下。下載
@Test
public void test() {
ServiceLoader<LogService> serviceLoader = ServiceLoader.load(LogService.class);
LogService logService = null;
for (Iterator<LogService> iter = serviceLoader.iterator();iter.hasNext(); ) {
logService = iter.next();
logService.info("Hello SPI");
}
//由於ServiceLoader是實現了java.util.Iterator接口的,也可以使用增強的for循環
for (LogService service : serviceLoader ) {
service.info("Hello SPI");
}
}
調用結果如下:
在上述示例中我們的基於SPI規範的服務定義和服務實現都是在一個工程裏面的,且都是可以看到源碼的,但在實際應用中我們的服務提供者往往是以jar包的形式來提供對應的服務實現的。ServiceLoader不是一實例化以後立馬就去讀配置文件中的服務實現者,並且進行對應的實例化工作的,而是會等到需要通過其Iterator實現獲取對應的服務提供者時纔會加載對應的配置文件進行解析,具體來說是在調用Iterator的hasNext方法時會去加載配置文件進行解析,在調用next方法時會將對應的服務提供者進行實例化並進行緩存。所有的配置文件只加載一次,服務提供者也只實例化一次,如需要重新加載配置文件可調用ServiceLoader的reload方法。