深刻理解SPI機制

1、什麼是SPI

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

這一機制爲不少框架擴展提供了可能,好比在Dubbo、JDBC中都使用到了SPI機制。咱們先經過一個很簡單的例子來看下它是怎麼用的。mysql

一、小栗子

首先,咱們須要定義一個接口,SPIServicesql

package com.viewscenes.netsupervisor.spi;
public interface SPIService {
	void execute();
}
複製代碼

而後,定義兩個實現類,沒別的意思,只輸入一句話。數據庫

package com.viewscenes.netsupervisor.spi;
public class SpiImpl1 implements SPIService{
	public void execute() {
		System.out.println("SpiImpl1.execute()");
	}
}
----------------------我是乖巧的分割線----------------------
package com.viewscenes.netsupervisor.spi;
public class SpiImpl2 implements SPIService{
	public void execute() {
		System.out.println("SpiImpl2.execute()");
	}
}
複製代碼

最後呢,要在ClassPath路徑下配置添加一個文件。文件名字是接口的全限定類名,內容是實現類的全限定類名,多個實現類用換行符分隔。 文件路徑以下: bash

SPI配置文件位置
內容就是實現類的全限定類名:

com.viewscenes.netsupervisor.spi.SpiImpl1
com.viewscenes.netsupervisor.spi.SpiImpl2
複製代碼

二、測試

而後咱們就能夠經過ServiceLoader.load或者Service.providers方法拿到實現類的實例。其中,Service.providers包位於sun.misc.Service,而ServiceLoader.load包位於java.util.ServiceLoader框架

public class Test {
	public static void main(String[] args) {	
		Iterator<SPIService> providers = Service.providers(SPIService.class);
		ServiceLoader<SPIService> load = ServiceLoader.load(SPIService.class);

		while(providers.hasNext()) {
			SPIService ser = providers.next();
			ser.execute();
		}
		System.out.println("--------------------------------");
		Iterator<SPIService> iterator = load.iterator();
		while(iterator.hasNext()) {
			SPIService ser = iterator.next();
			ser.execute();
		}
	}
}
複製代碼

兩種方式的輸出結果是一致的:ide

SpiImpl1.execute()
SpiImpl2.execute()
--------------------------------
SpiImpl1.execute()
SpiImpl2.execute()
複製代碼

2、源碼分析

咱們看到一個位於sun.misc包,一個位於java.util包,sun包下的源碼看不到。咱們就以ServiceLoader.load爲例,經過源碼看看它裏面到底怎麼作的。源碼分析

一、ServiceLoader

首先,咱們先來了解下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

load方法建立了一些屬性,重要的是實例化了內部類,LazyIterator。最後返回ServiceLoader的實例。ui

public final class ServiceLoader<S>	implements Iterable<S>
	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的相應方法。

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 class LazyIterator implements Iterator<S>{
	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; 
	}
}
複製代碼

看到這兒,我想已經很清楚了。獲取到類的實例,咱們天然就能夠對它隨心所欲了!

3、JDBC中的應用

咱們開頭說,SPI機制爲不少框架的擴展提供了可能,其實JDBC就應用到了這一機制。回憶一下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。

public class DriverManager {
	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 SPI文件

二、建立實例

上一步已經找到了MySQL中的com.mysql.cj.jdbc.Driver全限定類名,當調用next方法時,就會建立這個類的實例。它就完成了一件事,向DriverManager註冊自身的實例。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
	static {
        try {
			//註冊
			//調用DriverManager類的註冊方法
			//往registeredDrivers集合中加入實例
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}
複製代碼

三、建立Connection

在DriverManager.getConnection()方法就是建立鏈接的地方,它經過循環已註冊的數據庫驅動程序,調用其connect方法,獲取鏈接並返回。

private static Connection getConnection(
        String url, java.util.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文件,自定義實現類MyDriver,那麼,在獲取鏈接的先後就能夠動態修改一些信息。

仍是先在項目ClassPath下建立文件,文件內容爲自定義驅動類com.viewscenes.netsupervisor.spi.MyDriver

自定義數據庫驅動程序

咱們的MyDriver實現類,繼承自MySQL中的NonRegisteringDriver,還要實現java.sql.Driver接口。這樣,在調用connect方法的時候,就會調用到此類,但實際建立的過程還靠MySQL完成。

package com.viewscenes.netsupervisor.spi

public class MyDriver extends NonRegisteringDriver implements Driver{
	static {
		try {
            java.sql.DriverManager.registerDriver(new MyDriver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
	}
	public MyDriver()throws SQLException {}
	
	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
複製代碼
相關文章
相關標籤/搜索