幾天前和一位前輩聊起了Spring技術,大佬突然說了SPI,作為一個熟練使用Spring的民工,心中一緊,咱也不敢說不懂,而是在聊完之后趕緊打開了瀏覽器,開始的學習之路,所以也就有了這篇文章。廢話不多說,咱們開始正文。
定義
SPI的英文全稱就是Service Provider Interface,看到全稱,心里就有了底了,這是一種將服務接口與服務實現分離以達到解耦可拔插以最大提升了程序可擴展性的機制,
這個機制最大的優點就是無須在代碼里指定,進而避免了代碼污染,實現了模塊的可拔插。在JDK、Spring、Dubbo中都有着它的身影,畢竟框架最核心的作用之一就是解耦,下面詳細介紹SPI在JDK、Spring、Dubbo中具體的實現;
SPI基礎
java中加載類的方式使用的雙親委派,而在雙親委派模型中,子類加載器可以使用父類加載器已經加載的類,而父類加載器無法使用子類加載器已經加載的類。這就導致了雙親委派模型並不能解決所有的類加載器問題。
雙親委派案例
案例:Java 提供了很多服務提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實現。常見的 SPI 有 JDBC、JNDI、JAXP 等,這些SPI的接口由核心類庫提供,卻由第三方實現,這樣就存在一個問題:SPI 的接口是 Java 核心庫的一部分,是由BootstrapClassLoader加載的;SPI實現的Java類一般是由AppClassLoader來加載的。BootstrapClassLoader是無法找到 SPI 的實現類的,因為它只加載Java的核心庫。它也不能代理給AppClassLoader,因為它是最頂層的類加載器。也就是說,雙親委派模型並不能解決這個問題。 ----> 所以,使用SPI 打破雙親委派模式。
解決方式
使用線程上下文類加載器(ContextClassLoader)加載:如果不做任何的設置,Java應用的線程的上下文類加載器默認就是AppClassLoader。在核心類庫使用SPI接口時,傳遞的類加載器使用線程上下文類加載器,就可以成功的加載到SPI實現的類。線程上下文類加載器在很多SPI的實現中都會用到。
通常我們可以通過Thread.currentThread().getClassLoader()和Thread.currentThread().getContextClassLoader()獲取線程上下文類加載器。
JDK SPI
JDK提供了一種比較簡單SPI實現,其規范具體如下:
- 制定統一的規范(接口,比如 java.sql.Driver);
- 服務提供商提供這個規范具體的實現,在自己jar包的META-INF/services/目錄里創建一個以服務接口命名的文件,內容是實現類的全命名(比如:com.mysql.jdbc.Driver);
- 平台引入外部模塊的時候,就能通過該jar包META-INF/services/目錄下的配置文件找到該規范具體的實現類名,然后裝載實例化,完成該模塊的注入;
在java中使用spi最常見的場景就是連接數據庫時使用,下面從源碼層面對java中spi機制進行解析,下面關鍵代碼已經在代碼行后進行數字標識,閱讀代碼時大家可以參考。
DriverManager類:
static {
//加載初始驅動,跳到方法具體實現
loadInitialDrivers(); // 1
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() { // 2
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//這一步其實是初始化完成serviceLoader
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); // 3
//初始化完成,對加載的具體實現類進行遍歷加載
Iterator<Driver> driversIterator = loadedDrivers.iterator(); // 11
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
//迭代器后續判斷,可以調到13
while(driversIterator.hasNext()) { // 12
driversIterator.next(); // 15
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
ServiceLoader類
//根據服務類型初始化serviceloader
public static <S> ServiceLoader<S> load(Class<S> service) { //4
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader) // 5
{
return new ServiceLoader<>(service, loader); //6
}
// 構造器私有,每次new新對象,但進行lazy load
private ServiceLoader(Class<S> svc, ClassLoader cl) { // 7
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload(); // 8
}
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader); // 9
}
// ServiceLoader類中的內部類LazyIterator
private LazyIterator(Class<S> service, ClassLoader loader) { // 10
this.service = service;
this.loader = loader;
}
//固定路徑前綴META-INF/services下的文件
private boolean hasNextService() { // 14
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;
}
//使用class.forname進行類的加載初始化
private S nextService() { // 18
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
}
public boolean hasNext() {
if (acc == null) {
return hasNextService(); // 13
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() { // 16
if (acc == null) {
return nextService(); // 17
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
以上就是整個java spi的實現,只是簡單地的進行掃描加載,並沒有實現按需加載。
使用案例
common-logging apache最早提供的日志的門面接口。只有接口,沒有實現。具體方案由各提供商實現, 發現日志提供商是通過掃描 META-INF/services/org.apache.commons.logging.LogFactory配置文件,通過讀取該文件的內容找到日志提工商實現類。只要我們的日志實現里包含了這個文件,並在文件里制定 LogFactory工廠接口的實現類即可。
Spring SPI
Spring中接口BeanDefinitionDocumentReader是使用SPI機制解析包含spring bean 定義的xml文檔,在進行xml命名空間解析時使用默認實現類DefaultNamespaceHandlerResolver,會懶加載spring.handler文件內配置的實現類進內存,加載邏輯如下:
private Map<String, Object> getHandlerMappings() {
Map<String, Object> handlerMappings = this.handlerMappings;
// 多線程二次驗證
if (handlerMappings == null) {
synchronized (this) {
handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
if (logger.isTraceEnabled()) {
logger.trace("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
}
try {
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
if (logger.isTraceEnabled()) {
logger.trace("Loaded NamespaceHandler mappings: " + mappings);
}
handlerMappings = new ConcurrentHashMap<>(mappings.size());
CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
this.handlerMappings = handlerMappings;
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
}
}
}
}
return handlerMappings;
}
讀取META-INF/spring.handlers目錄下的實現類進jvm,spring.handlers的結構如下:
http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler
http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler
http\://www.springframework.org/schema/task=org.springframework.scheduling.config.TaskNamespaceHandler
http\://www.springframework.org/schema/cache=org.springframework.cache.config.CacheNamespaceHandler
基本結構就是映射關系,前面是具體xsd路徑,后面接具體實現類,有人會有疑問,那我要接的接口類哪里去了,是不用找了嗎,那怎么對應呢?這里的命名空間解析類對應的接口為NamespaceHandler,因為只有一個,所有不需要進行額外指定。
spring中自定義標簽的加載過程會和以上過程重度相關,后續會專門分析spring中自定義標簽的過程。
Spring boot SPI
在springboot的自動裝配過程中,最終會加載META-INF/spring.factories
文件,而加載的過程是由SpringFactoriesLoader加載的。從CLASSPATH下的每個Jar包中搜尋所有META-INF/spring.factories
配置文件,然后將解析properties文件,找到指定名稱的配置后返回。需要注意的是,其實這里不僅僅是會去ClassPath路徑下查找,會掃描所有路徑下的Jar包,只不過這個文件只會在Classpath下的jar包中。
自動裝配jar中的spring.factory的結構如下,由於沒有在文件名上指定接口的名稱,所有在每個第一行都會對要實現的接口進行申明
# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer
# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
......
不同場景對應不同的文件格式,但其基本原理都是一樣的。
dubbo SPI
由於對dubbo的接觸比較少,這里暫時空起,后續會補齊,哈哈哈。//TODO
總結
SPI這個很神奇的機制,解耦神器,如果要進行代碼重構,分離,我覺得可以重點考慮這個東東,同時也是大框架必備機制。