Spring SPI 機制總結

ocean.wen發表於2021-05-07

1、概念:

    SPI(Service Provider Interface)服務提供介面,簡單來說就是用來解耦,實現外掛的自由插拔,具體實現方案可參考JDK裡的ServiceLoader(載入classpath下所有META-INF/services/目錄下的對應給定介面包路徑的檔案,然後通過反射例項化配置的所有實現類,以此將介面定義和邏輯實現分離)
    Spring在3.0.x的時候就已經引入了spring.handlers,很多部落格講Spring SPI的時候並沒有提到spring.handlers,但是通過我自己的分析對比,其實spring.handlers也是一種SPI的實現,只不過它是基於xml的,而且在沒有boot的年代,它幾乎是所有三方框架跟spring整合的必選機制。

在3.2.x又新引入了spring.factories,它的實現跟JDK的SPI就基本是相似的了。

spring.handlers和spring.factories我都把它歸納為Spring為我們提供的SPI機制,通過這兩種機制,我們可以在不修改Spring原始碼的前提下,非常輕鬆的做到對Spring框架的擴充套件開發。

2、實現:

2.1 先看看spring.handlers SPI

    在Spring裡有個介面NamespaceHandlerResolver,只有一個預設的實現類DefaultNamespaceHandlerResolver,而它的作用就是載入classpath下可能分散在各個jar包中的META-INF/spring.handlers檔案,resolve方法中關鍵程式碼如下:

//載入所有jar包中的META-INF/spring.handlers檔案
Properties mappings=
  PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);

//把META-INF/spring.handlers中配置的namespaceUri對應實現類例項化
NamespaceHandler namespaceHandler =
  (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);

DefaultNamespaceHandlerResolver.resolve()主要被用在BeanDefinitionParserDelegate的parseCustomElement和decorateIfRequired,所以spring.handlers SPI機制主要也是被用在bean的掃描和解析過程中。

2.2 再來看spring.factories SPI

// 獲取某個已定義介面的實現類,跟JDK的ServiceLoader SPI相似度為90%
List<BeanInfoFactory> beanInfoFactories = SpringFactoriesLoader.loadFactories(BeanInfoFactory.class, classLoader);
// spring.factories檔案的格式為:key=value1,value2,value3
// 從所有jar檔案中找到MET-INF/spring.factories檔案(注意是:classpath下的所有jar包,所以可插拔、擴充套件性超強)
Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
	ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
List<String> result = new ArrayList<String>();
while (urls.hasMoreElements()) {
	URL url = urls.nextElement();
	Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
	String propertyValue = properties.getProperty(factoryClassName);
	for (String factoryName : StringUtils.commaDelimitedListToStringArray(propertyValue)) {
		result.add(factoryName.trim());
	}
}
return result;

更多細節,大家可以參考SpringFactoriesLoader類,Spring自3.2.x引入spring.factories SPI後其實一直沒怎麼利用起來,只有CachedIntrospectionResults(初始化bean的過程中)用到了,而且在幾大核心jar包裡,也只有bean包裡才有用到。
真正把spring.factories發揚光大的,是到了Spring Boot,可以看到boot包裡配置了非常多的介面實現類。大家跟蹤boot的啟動類SpringApplication可以發現,有很多地方都呼叫了getSpringFactoriesInstances()方法,這些就是spring boot開給我們的擴充套件機會,就像一座寶藏一樣,大家可以自己去發掘。

3、應用:

    先來看看mybatis和dubbo早期跟Spring整合的實現,他們無一例外都用到了spring.handlers SPI機制,以此來向IOC容器注入自己的Bean。


進入boot時代後,spring.factories SPI機制應用得更加廣泛,我們可以在容器啟動、環境準備、初始化、上下文載入等等環節輕輕鬆鬆的對Spring做擴充套件開發(例如:我們專案中用到spring.factories SPI機制對配置檔案中的變數實現動態解密,以及前篇博文中提到的@Replace註解等)。

4、實踐(載入application.xyz配置檔案):

    Spring裡有兩種常見的配置檔案型別:application.properties 和 application.yml,其中yml是近年興起的,但說實話同事也包括我自己是被它坑過,沒有合適的編輯器時很容易把格式寫錯,導致上線出問題。所以我就在想有沒有辦法讓Spring支援一種新的配置檔案格式,既保留yml的簡潔優雅,有能夠有強制的格式校驗,暫時我想到了json格式。

# 這是spring.factories中的配置
org.springframework.boot.env.PropertySourceLoader=top.hiccup.json.MyJsonPropertySourceLoader
public class MyJsonPropertySourceLoader implements PropertySourceLoader {
    @Override
    public String[] getFileExtensions() {
        return new String[]{"xyz"};
    }
    @Override
    public List<PropertySource<?>> load(String name, Resource resource) throws IOException {

        BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()));
        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
        // 這裡只是做了簡單解析,沒有做巢狀配置的解析
        JSONObject json = JSONObject.parseObject(sb.toString());
        List<PropertySource<?>> propertySources = new ArrayList<>();
        MapPropertySource mapPropertySource = new MapPropertySource(resource.getFilename(), json);
        propertySources.add(mapPropertySource);
        return propertySources;
    }
}
        ConfigurableApplicationContext ctx = SpringApplication.run(BootTest.class, args);
        Custom custom = ctx.getBean(Custom.class);
        System.out.println(custom.name);
        System.out.println(custom.age);

具體程式碼可以參考(https://github.com/hiccup234/web-advanced/tree/master/configFile) ,執行得到結果如下:

可見我們在不修改Spring原始碼的前提下,輕鬆通過Spring開放給我們的擴充套件性實現了對新的配置檔案型別的載入和解析。

這就是Spring SPI的魅力吧。

相關文章