深入瞭解Spring與Java的SPI機制

SPI(service provider interface)機制是JDK內置的一種服務發現機制,可以動態的發現服務

JDK的SPI機制

jdk 的 spi 機制是通過 ServiceLoader 類來加載擴展的服務提供類

爲什麼需要這種機制呢?

大家可以從類加載器文章中雙親委派的破壞了解原因
雙親委派模型破壞

原因

簡單的說,類加載器中的雙親委派模型的工作原理是對於類的加載,先交給父類加載器完成,如果父類加載器無法完成加載,子類加載器才進行加載。JDK中的一些基礎類接口,例如 JDBC,如果按照雙親委派模型,JDBC只能用啓動類加載器完成驅動的加載,但是驅動的實現是在用戶目錄的 jar 包實現,所以啓動類加載器就無法加載用戶的目錄的驅動類了(下面可以從代碼實例瞭解一下)

ServiceLoader 的源碼中的介紹如下

A service is a well-known set of interfaces and (usually abstract) classes. 
A service provider is a specific implementation of a service.  The classes 
in a provider typically implement the interfaces and subclass the classes 
defined in the service itself.  Service providers can be installed in an 
implementation of the Java platform in the form of extensions, that is, 
jar files placed into any of the usual extension directories.  Providers 
can also be made available by adding them to the application's class 
path or by some other platform-specific means.

大體上的意思是

服務是一組衆所周知的接口和類(通常是抽象的)。
服務提供者是一個服務的特定實現。提供者中的類通常是實現服務接口
和子類的具體實現。服務提供者可以以擴展的形式在 Java 平臺中實現,
也就是放在任何常用jar文件擴展目錄中。提供者也可以通過將它們添加
到應用程序的 classpath 或其他特定於平臺的方法。

ServiceLoader 的作用是通過接口或抽象類,找到 class path 的 META-INF/services/ 路徑下對應的文件,把文件中描述全路徑的實現類(服務提供者)加載起來

案例分析

SpiInterface接口

package spi;

public interface SpiInterface {
    String getInfo();
}

文件路徑,文件命名是接口的路徑

在這裏插入圖片描述

spi.SpiInterface文件內容爲兩個接口的實現類

spi.SpiInfo
spi.SpiMessage

SpiInfo 和 SpiMessage實現類

package spi;

public class SpiInfo implements SpiInterface {

    @Override
    public String getInfo() {
        return "SpiInfo";
    }
}
package spi;

public class SpiMessage implements SpiInterface {
    @Override
    public String getInfo() {
        return "SpiMessage";
    }
}

再來看看實際使用的輸出結果

package spi;

import java.util.ServiceLoader;

public class Main {

    public static void main(String[] args) {
        ServiceLoader<SpiInterface> loader = ServiceLoader.load(SpiInterface.class);
        for (SpiInterface spi : loader) {
            System.out.println(spi.getInfo());
        }
        // 輸出:
        // SpiInfo
        // SpiMessage
    }
}

通過 JDK 提供的 SPI 機制,就可以通過 SpiInterface 接口和實現類文件的描述,把具體的實現類加載了

SPI 與 雙親委派模型破壞

這種 SPI 機制又與類加載器的雙親委派模型破壞有什麼關係的(大家可以想一下)

SPi 機制對於雙親委派模型的破壞在於增加了線程上下文類加載器,它的作用是可以逆向加載,例如需要用啓動類加載器目錄下的類,可以通過線程上下文類加載器來加載

假如沒有線程上下文類加載器,加載的方式如下

package spi;

public class Main {

    public static void main(String[] args) throws Exception {
    		// 假如 SpiInterface 在 rt.jar 中,那麼類加載器是啓動類加載器,
    		// 啓動類加載器是無法加載用戶路徑("spi.SpiInfo")的類的
        Class<SpiInterface> spiInfo = (Class<SpiInterface>) 
        			SpiInterface.class.getClassLoader().loadClass("spi.SpiInfo");
        System.out.println(spiInfo.newInstance().getInfo());
    }
}

但自從有了線程上下文類加載器,加載方式可以如下

package spi;

public class Main {

    public static void main(String[] args) throws Exception {
        Class<SpiInterface> spiInfo = (Class<SpiInterface>) 
        		Thread.currentThread().getContextClassLoader().loadClass("spi.SpiInfo");
        System.out.println(spiInfo.newInstance().getInfo());
        // 無論 SpiInterface 在哪個路徑,線程上下文類加載器都可以加載類
        // 完美輸出:SpiInfo
    }
}

這樣大家是不是對於 JDK 的 SPi 機制很熟悉了呢,如果對於類加載過程與類加載器還不熟悉的,建議看看上面鏈接的文章(重點在於類加載,類加載器),肯定大有收穫

Sping 的 SPi 機制

介紹完了 JDK 的 SPi 機制,我們還可以在 Sping 中發現這種機制的應用,Spring 的 SPi 機制可以把 spring.factories 文件中的類實例化,註冊到 Spring 容器中,該機制在 Sping 是這樣應用的:

  1. 服務:接口或抽象類

  2. 服務提供者:實現服務接口的實現類

  3. 服務與服務提供者的文件描述:在 class path 的 META-INF/spring.factories 文件中,該文件可以在任意 jar 包存在,所以不同的 jar 包不會存在衝突,spring 啓動的時候會掃描所有 jar 包的 spring.factories 文件

  4. 加載類:SpringFactoriesLoader(相當於 JDK 的 ServiceLoader)

源碼分析

從 SpringFactories 的源碼可以看出

public abstract class SpringFactoriesLoader {
	/**
	 * The location to look for factories.
	 * <p>Can be present in multiple JAR files.
	 */
	public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
  
  /**
   * @param factoryClass 工廠類接口或抽象類
   * @param classLoader 類加載器
   * @return 實現 factoryClass 的所有實例
   */
  public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) {
		Assert.notNull(factoryClass, "'factoryClass' must not be null");
		ClassLoader classLoaderToUse = classLoader;
		if (classLoaderToUse == null) {
			classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
		}
		List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
		if (logger.isTraceEnabled()) {
			logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames);
		}
		List<T> result = new ArrayList<T>(factoryNames.size());
		for (String factoryName : factoryNames) {
			result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
		}
		AnnotationAwareOrderComparator.sort(result);
		return result;
	}
}

其中 spring.factories 是這樣定義的,實現類通過逗號分隔

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.actuate.autoconfigure.AuditAutoConfiguration

org.springframework.boot.actuate.autoconfigure.ManagementContextConfiguration=\
org.springframework.boot.actuate.autoconfigure.EndpointWebMvcManagementContextConfiguration,\
org.springframework.boot.actuate.autoconfigure.EndpointWebMvcHypermediaManagementContextConfiguration

dubbo 的 SPI 機制

有興趣的還可以瞭解 dubbo 的 spi 機制,來自 Dubbo 官方文檔

Dubbo SPI 實現了一套功能更強的 SPI 機制

  1. 通過鍵值對方式進行配置,這樣可以按需加載指定類
  2. 可以實現實例的依賴注入
  3. 可以對實例進行包裝,採用裝飾者模式增強類

喜歡小K的請多多關注微信公衆號
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章