SPI機制的原理和應用

前言

SPI ,全稱爲 Service Provider Interface,是一種服務發現機制。它經過在ClassPath路徑下的META-INF/services文件夾查找文件,自動加載文件裏所定義的類。java

這一機制爲不少框架的擴展提供了可能,好比在Dubbo、JDBC、SpringBoot中都使用到了SPI機制。雖然他們之間的實現方式不一樣,但原理都差很少。今天咱們就來看看,SPI究竟是何方神聖,在衆多開源框架中又扮演了什麼角色。mysql

1、JDK中的SPI

咱們先從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; 
}
複製代碼

到這爲止,已經獲取到了類的實例。

2、JDBC中的應用

咱們開頭說,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!");
        }
    }
}
複製代碼

三、建立Connection

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
複製代碼

3、SpringBoot中的應用

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

而後定義一個MyContextInitializer類

public class MyContextInitializer implements ApplicationContextInitializer {
    @Override
    public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
        System.out.println(configurableApplicationContext);
    }
}
複製代碼

4、Dubbo中的應用

咱們熟悉的Dubbo也不例外,它也是經過 SPI 機制加載全部的組件。一樣的,Dubbo 並未使用 Java 原生的 SPI 機制,而是對其進行了加強,使其可以更好的知足需求。在 Dubbo 中,SPI 是一個很是重要的模塊。基於 SPI,咱們能夠很容易的對 Dubbo 進行拓展。

關於原理,若是有小夥伴不熟悉,能夠參閱筆者文章:

Dubbo中的SPI和自適應擴展機制

它的使用方式一樣是在META-INF/services建立文件並寫入相關類名。

關於使用場景,能夠參考: SpringBoot+Dubbo集成ELK實戰

相關文章
相關標籤/搜索