SPI ,全稱爲 Service Provider Interface
,是一種服務發現機制。它經過在ClassPath路徑下的META-INF/services
文件夾查找文件,自動加載文件裏所定義的類。java
這一機制爲不少框架的擴展提供了可能,好比在Dubbo、JDBC、SpringBoot
中都使用到了SPI機制。雖然他們之間的實現方式不一樣,但原理都差很少。今天咱們就來看看,SPI究竟是何方神聖,在衆多開源框架中又扮演了什麼角色。mysql
咱們先從JDK開始,經過一個很簡單的例子來看下它是怎麼用的。spring
首先,咱們須要定義一個接口,SpiServicesql
public interface SpiService {
void println();
}
複製代碼
而後,定義一個實現類,沒別的意思,只作打印。數據庫
public class SpiServiceImpl implements SpiService {
@Override
public void println() {
System.out.println("-------------");
}
}
複製代碼
最後呢,要在resources路徑下配置添加一個文件。文件名字是接口的全限定類名,內容是實現類的全限定類名,多個實現類用換行符分隔。bash
文件內容就是實現類的全限定類名:com.youyouxunyin.service.impl.SpiServiceImpl
複製代碼
而後咱們就能夠經過ServiceLoader.load
方法拿到實現類的實例,並調用它的方法。mybatis
public static void main(String[] args){
ServiceLoader<SpiService> load = ServiceLoader.load(SpiService.class);
Iterator<SpiService> iterator = load.iterator();
while (iterator.hasNext()){
SpiService service = iterator.next();
service.println();
}
}
複製代碼
首先,咱們先來了解下ServiceLoader
,看看它的類結構。框架
public final class ServiceLoader<S> implements Iterable<S>{
//配置文件的路徑
private static final String PREFIX = "META-INF/services/";
//加載的服務類或接口
private final Class<S> service;
//已加載的服務類集合
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
//類加載器
private final ClassLoader loader;
//內部類,真正加載服務類
private LazyIterator lookupIterator;
}
複製代碼
當咱們調用load
方法時,並無真正的去加載和查找服務類。而是調用了ServiceLoader
的構造方法,在這裏最重要的是實例化了內部類LazyIterator
,它纔是接下來的主角。ide
private ServiceLoader(Class<S> svc, ClassLoader cl) {
//要加載的接口
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//類加載器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
//訪問控制器
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
//先清空
providers.clear();
//實例化內部類
LazyIterator lookupIterator = new LazyIterator(service, loader);
}
複製代碼
查找實現類和建立實現類的過程,都在LazyIterator
完成。當咱們調用iterator.hasNext和iterator.next
方法的時候,實際上調用的都是LazyIterator
的相應方法。spring-boot
public Iterator<S> iterator() {
return new Iterator<S>() {
public boolean hasNext() {
return lookupIterator.hasNext();
}
public S next() {
return lookupIterator.next();
}
.......
};
}
複製代碼
因此,咱們重點關注lookupIterator.hasNext()
方法,它最終會調用到hasNextService
,在這裏返回實現類名稱。
private class LazyIterator implements Iterator<S>{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private boolean hasNextService() {
//第二次調用的時候,已經解析完成了,直接返回
if (nextName != null) {
return true;
}
if (configs == null) {
//META-INF/services/ 加上接口的全限定類名,就是文件服務類的文件
//META-INF/services/com.viewscenes.netsupervisor.spi.SPIService
String fullName = PREFIX + service.getName();
//將文件路徑轉成URL對象
configs = loader.getResources(fullName);
}
while ((pending == null) || !pending.hasNext()) {
//解析URL文件對象,讀取內容,最後返回
pending = parse(service, configs.nextElement());
}
//拿到第一個實現類的類名
nextName = pending.next();
return true;
}
}
複製代碼
而後當咱們調用next()
方法的時候,調用到lookupIterator.nextService
。它經過反射的方式,建立實現類的實例並返回。
private S nextService() {
//全限定類名
String cn = nextName;
nextName = null;
//建立類的Class對象
Class<?> c = Class.forName(cn, false, loader);
//經過newInstance實例化
S p = service.cast(c.newInstance());
//放入集合,返回實例
providers.put(cn, p);
return p;
}
複製代碼
到這爲止,已經獲取到了類的實例。
咱們開頭說,SPI機制爲不少框架的擴展提供了可能,其實JDBC就應用到了這一機制。
在之前,須要先設置數據庫驅動的鏈接,再經過DriverManager.getConnection
獲取一個Connection
。
String url = "jdbc:mysql:///consult?serverTimezone=UTC";
String user = "root";
String password = "root";
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(url, user, password);
複製代碼
而如今,設置數據庫驅動鏈接,這一步驟就再也不須要,那麼它是怎麼分辨是哪一種數據庫的呢?答案就在SPI。
咱們把目光回到DriverManager
類,它在靜態代碼塊裏面作了一件比較重要的事。很明顯,它已經經過SPI機制, 把數據庫驅動鏈接初始化了。
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
}
複製代碼
具體過程還得看loadInitialDrivers
,它在裏面查找的是Driver接口的服務類,因此它的文件路徑就是:
META-INF/services/java.sql.Driver
private static void loadInitialDrivers() {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//很明顯,它要加載Driver接口的服務類,Driver接口的包爲:java.sql.Driver
//因此它要找的就是META-INF/services/java.sql.Driver文件
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
//查到以後建立對象
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
}
複製代碼
那麼,這個文件哪裏有呢?咱們來看MySQL的jar包,就是這個文件,文件內容爲:com.mysql.cj.jdbc.Driver
。
上一步已經找到了MySQL中的com.mysql.cj.jdbc.Driver
全限定類名,當調用next方法時,就會建立這個類的實例。它就完成了一件事,向DriverManager
註冊自身的實例。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
//註冊、調用DriverManager類的註冊方法
//往registeredDrivers集合中加入實例
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
複製代碼
DriverManager.getConnection()
方法就是建立鏈接的地方,它經過循環已註冊的數據庫驅動程序,調用其connect方法,獲取鏈接並返回。
private static Connection getConnection(String url, Properties info, Class<?> caller) throws SQLException {
//registeredDrivers中就包含com.mysql.cj.jdbc.Driver實例
for(DriverInfo aDriver : registeredDrivers) {
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
//調用connect方法建立鏈接
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
return (con);
}
}catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println("skipping: " + aDriver.getClass().getName());
}
}
}
複製代碼
既然咱們知道JDBC是這樣建立數據庫鏈接的,咱們能不能再擴展一下呢?若是咱們本身也建立一個java.sql.Driver
文件,自定義實現類MySQLDriver,那麼,在獲取鏈接的先後就能夠動態修改一些信息。
仍是先在項目resources下建立文件,文件內容爲自定義驅動類com.youyouxunyin.driver.MySQLDriver
咱們的MySQLDriver
實現類,繼承自MySQL中的NonRegisteringDriver
,還要實現java.sql.Driver
接口。這樣,在調用connect方法的時候,就會調用到此類,但實際建立的過程還靠MySQL完成。
public class MySQLDriver extends NonRegisteringDriver implements Driver{
static {
try {
DriverManager.registerDriver(new MySQLDriver());
} catch (SQLException e) {
e.printStackTrace();
}
}
public MySQLDriver() throws SQLException {}
@Override
public Connection connect(String url, Properties info) throws SQLException {
System.out.println("準備建立數據庫鏈接.url:"+url);
System.out.println("JDBC配置信息:"+info);
info.setProperty("user", "root");
Connection connection = super.connect(url, info);
System.out.println("數據庫鏈接建立完成!"+connection.toString());
return connection;
}
}
複製代碼
這樣的話,當咱們獲取數據庫鏈接的時候,就會調用到這裏。
--------------------輸出結果---------------------
準備建立數據庫鏈接.url:jdbc:mysql:///consult?serverTimezone=UTC
JDBC配置信息:{user=root, password=root}
數據庫鏈接建立完成!com.mysql.cj.jdbc.ConnectionImpl@7cf10a6f
複製代碼
Spring Boot提供了一種快速的方式來建立可用於生產環境的基於Spring的應用程序。它基於Spring框架,更傾向於約定而不是配置,而且旨在使您儘快啓動並運行。
即使沒有任何配置文件,SpringBoot的Web應用都能正常運行。這種神奇的事情,SpringBoot正是依靠自動配置來完成。
說到這,咱們必須關注一個東西:SpringFactoriesLoader
,自動配置就是依靠它來加載的。
SpringFactoriesLoader
來負責加載配置。咱們打開這個類,看到它加載文件的路徑是:META-INF/spring.factories
筆者在項目中搜索這個文件,發現有4個Jar包都包含它:
spring-boot-2.1.9.RELEASE.jar
spring-beans-5.1.10.RELEASE.jar
spring-boot-autoconfigure-2.1.9.RELEASE.jar
mybatis-spring-boot-autoconfigure-2.1.0.jar
那麼它們裏面都是些啥內容呢?其實就是一個個接口和類的映射。在這裏筆者就不貼了,有興趣的小夥伴本身去看看。
好比在SpringBoot啓動的時候,要加載全部的ApplicationContextInitializer
,那麼就能夠這樣作:
SpringFactoriesLoader.loadFactoryNames(ApplicationContextInitializer.class, classLoader)
loadSpringFactories
就負責讀取全部的spring.factories
文件內容。
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
//獲取全部spring.factories文件的路徑
Enumeration<URL> urls = lassLoader.getResources("META-INF/spring.factories");
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
//加載文件並解析文件內容
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
複製代碼
能夠看到,它並無採用JDK中的SPI機制來加載這些類,不過原理差很少。都是經過一個配置文件,加載並解析文件內容,而後經過反射建立實例。
假如你但願參與到SpringBoot
初始化的過程當中,如今咱們又多了一種方式。
咱們也建立一個spring.factories
文件,自定義一個初始化器。
org.springframework.context.ApplicationContextInitializer=com.youyouxunyin.config.context.MyContextInitializer
public class MyContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
System.out.println(configurableApplicationContext);
}
}
複製代碼
咱們熟悉的Dubbo也不例外,它也是經過 SPI 機制加載全部的組件。一樣的,Dubbo 並未使用 Java 原生的 SPI 機制,而是對其進行了加強,使其可以更好的知足需求。在 Dubbo 中,SPI 是一個很是重要的模塊。基於 SPI,咱們能夠很容易的對 Dubbo 進行拓展。
關於原理,若是有小夥伴不熟悉,能夠參閱筆者文章:
它的使用方式一樣是在META-INF/services
建立文件並寫入相關類名。
關於使用場景,能夠參考: SpringBoot+Dubbo集成ELK實戰