springboot-starter中的SPI 機制

NorthWard發表於2019-07-15

SPI的全稱是Service Provider Interface, 直譯過來就是"服務提供介面", 聽起來挺彆扭的, 所以我試著去就將它翻譯為"服務提供商介面"吧.

我們都知道, 一個介面是可以有很多種實現的. 例如搜尋,可以是搜尋系統的硬碟,也可以是搜尋資料庫.系統的設計者為了降低耦合,並不想在硬編碼裡面寫死具體的搜尋方式,而是希望由服務提供者來選擇使用哪種搜尋方式, 這個時候就可以選擇使用SPI機制.

SPI機制被大量應用在各種開源框架中,例如:

  1. 大家都熟悉的dubbo中的ExtensionLoader,可以通過這些擴充點增加一些自定義的外掛功能,比如增加filter實現白名單訪問, 實現介面限流等功能;或者還可以直接替換它原生的protocol, transport等
  2. 在進行idea intellij的外掛開發的時候,需要定義一個/META-INF/plugin.xml檔案, 這個plugin.xml中有很多地方可以配置serviceInterface和 serviceImplementation,這也是一種SPI機制,通過這種機制, idea能使得外掛開發者既能使用到它底層SDK提供的api,又能讓開發者具備定製化的功能,耦合相當的低.intellij的外掛開發的時候直接用了JDK中的ServiceLoader
  3. spring中也是大量用到了SPI機制,本文要分析的就是其中的一部分.

JDK中的SPI

SPI估計大家都有所瞭解,讓我們通過一個非常簡單的例子,來溫習一下java裡面的SPI機制吧.

  1. 定義一個搜尋介面Search
 package com.north.spilat.service;
 import java.util.List;
 public interface Search {
     List<String> search(String keyword);
 }
複製程式碼
  1. 實現介面從資料庫查詢
    package com.north.spilat.service.impl;
    import com.north.spilat.service.Search;
    import java.util.List;
    /**
     * @author lhh
     */
    public class DatabaseSearch implements Search {
        @Override
        public List<String> search(String keyword) {
            System.out.println("now use database search. keyword:" + keyword);
            return null;
        }
    
    }
複製程式碼
  1. 實現介面從檔案系統查詢
 package com.north.spilat.service.impl;
 import com.north.spilat.service.Search;
 import java.util.List;
 /**
  * @author lhh
  */
 public class FileSearch implements Search {
 
     @Override
     public List<String> search(String keyword) {
         System.out.println("now use file system search. keyword:" + keyword);
         return null;
     }
 
 }
複製程式碼
  1. 在src\main\resources建立一個目錄 META-INF\services\com.north.spilat.service.Search,然後在com.north.spilat.service.Search下面建立兩個檔案,以上面介面的具體實現類的全限定名稱為檔名,即:
    com.north.spilat.service.impl.DatabaseSearch
    com.north.spilat.service.impl.FileSearch
    整個工程目錄如下:

    image

  2. 新建一個main方法測試一下

 package com.north.spilat.main;
 import com.north.spilat.service.Search;
 import java.util.Iterator;
 import java.util.ServiceLoader;
 public class Main {
     public static void main(String[] args) {
         System.out.println("Hello World!");
         ServiceLoader<Search> s = ServiceLoader.load(Search.class);
         Iterator<Search> searchList = s.iterator();
         while (searchList.hasNext()) {
             Search curSearch = searchList.next();
             curSearch.search("test");
         }
     }
 }

複製程式碼

執行一下,輸出如下:

Hello World!
now use database search. keyword:test
now use file system search. keyword:test
複製程式碼

如你所見, SPI機制已經定義好了載入服務的流程框架, 你只需要按照約定, 在META-INF/services目錄下面, 以介面的全限定名稱為名建立一個資料夾(com.north.spilat.service.Search), 資料夾下再放具體的實現類的全限定名稱(com.north.spilat.service.impl.DatabaseSearch), 系統就能根據這些檔案,載入不同的實現類.這就是SPI的大體流程.

ServiceLoader類分析

回到上面的main方法,其實沒有什麼特別的,除了一句
ServiceLoader.load(Search.class);

ServiceLoader.class是一個工具類,根據META-INF/services/xxxInterfaceName下面的檔名,載入具體的實現類.

從load(Search.class)進去,我們來扒一下這個類,下面主要是貼程式碼,分析都在程式碼註釋內.

  1. 可以看到,裡面並沒有很多邏輯,主要邏輯都交給了LazyIterator這類
 /*
 *入口, 獲取一下當前類的類載入器,然後呼叫下一個靜態方法
 */
 public static <S> ServiceLoader<S> load(Class<S> service) {
     ClassLoader cl = Thread.currentThread().getContextClassLoader();
     return ServiceLoader.load(service, cl);
 }
 /*
 *這個也沒有什麼邏輯,直接呼叫構造方法
 */
 public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader)
 {
     return new ServiceLoader<>(service, loader);
 }
 /**
 * 也沒有什麼邏輯,直接呼叫reload
 */
 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;
     reload();
 }
 /**
 * 直接例項化一個懶載入的迭代器
 */
 public void reload() {
     providers.clear();
     lookupIterator = new LazyIterator(service, loader);
 }
複製程式碼
  1. LazyIterator這個迭代器只需要關心hasNext()和next(), hasNext()裡面又只是單純地呼叫hasNextService(). 不用說, next()裡面肯定也只是單純地呼叫了nextService();
 private boolean hasNextService() {
     if (nextName != null) {
         // nextName不為空,說明載入過了,而且服務不為空 
         return true;
     }
     // configs就是所有名字為PREFIX + service.getName()的資源
     if (configs == null) {
         try {
             // PREFIX是 /META-INF/services
             // service.getName() 是介面的全限定名稱
             String fullName = PREFIX + service.getName();
             // loader == null, 說明是bootstrap類載入器
             if (loader == null)
                 configs = ClassLoader.getSystemResources(fullName);
             else
                 // 通過名字載入所有檔案資源
                 configs = loader.getResources(fullName);
             } catch (IOException x) {
                 fail(service, "Error locating configuration files", x);
             }
     }
     //遍歷所有的資源,pending用於存放載入到的實現類
     while ((pending == null) || !pending.hasNext()) {
             if (!configs.hasMoreElements()) {
                 //遍歷完所有的檔案了,直接返回
                 return false;
             }
             
             // parse方法主要呼叫了parseLine,功能:
             // 1. 分析每個PREFIX + service.getName() 目錄下面的所有檔案
             // 2. 判斷每個檔案是否是合法的java類的全限定名稱,如果是就add到pending變數中
             pending = parse(service, configs.nextElement());
     }
     // 除了第一次進來,後面每次呼叫都是直接到這一步了
     nextName = pending.next();
     return true;
 }
複製程式碼
  1. 再來看看nextService幹了啥
 private S nextService() {
     // 校驗一下
     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");
     }
     // 是不是service的子類,或者同一個類
     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
 }
複製程式碼

從上面的程式碼就可以看出來, 所謂的懶載入,就是等到呼叫hasNext()再查詢服務, 呼叫next()才例項化服務類.

JDK的SPI大概就是這麼一個邏輯了, 服務提供商按照約定,將具體的實現類名稱放到/META-INF/services/xxx下, ServiceLoader就可以根據服務提供者的意願, 載入不同的實現了, 避免硬編碼寫死邏輯, 從而達到解耦的目的.

當然, 從上面這個簡單的例子可能大家會看不出來,SPI是如何達到解耦的效果的. 所以下面, 我們一起來看看,開源框架中是怎麼利用SPI機制來解耦的. 體會一下SPI的魅力.

springboot 中的SPI

作為一個程式設計師,沒事可以多點研究開源框架,因為這些開原始碼每天都不知道被人擼幾遍,所以他們的程式碼從設計到實現,都是非常優秀的,我們可以從中學到不少東西.

而spring框架這些年來,基本上可以說是開源界扛把子,江湖上無人不知無人不曉.其原始碼的設計也是出了名的優雅,超高擴充性超低耦合性.

那它是怎麼解耦的呢? 擴充點機制便是其中法寶之一

從神奇的starter說起

剛剛接觸springboot的時候, 真的覺得各種spring-xx-starter和xx-spring-starter非常的神奇. 為什麼在pom檔案新增一個依賴就能引入一個複雜的外掛了呢? 帶著這個疑問,我開始了我的走進科學之旅.

dubbo框架在國內用的公司挺多的,所以這裡, 我們就以dubbo-spring-boot-starter為例,來看看springboot中是如何高效解耦的.

回想一下, 如果我們要在springboot工程裡面引入dubbo模組, 需要怎麼做.

  1. 在pom檔案引入dubbo-spring-boot-starter的依賴.
        <dependency>
            <groupId>com.alibaba.spring.boot</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>
複製程式碼
  1. 在application.properties檔案配置好dubbo相關引數
    image
spring.dubbo.server=true
spring.dubbo.application.name=north-spilat-server

#
spring.dubbo.registry.id=defaultRegistry
#
spring.dubbo.registry.address=127.0.0.1
#
spring.dubbo.registry.port=2181
#
spring.dubbo.registry.protocol=zookeeper
#
spring.dubbo.protocol.name=dubbo
#
spring.dubbo.protocol.port=20881
#
spring.dubbo.module.name=north-spilat-server
#
spring.dubbo.consumer.check=false
#
spring.dubbo.provider.timeout=3000
#
spring.dubbo.consumer.retries=0
#
spring.dubbo.consumer.timeout=3000
複製程式碼
  1. 在spring-boot的啟動類加上對應的註解
package com.north.spilat.main;

import com.alibaba.dubbo.spring.boot.annotation.EnableDubboConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

/**
 * @author lhh
 */
@SpringBootApplication
@ComponentScan(basePackages = {"com.north.*"})
@EnableDubboConfiguration
public class SpringBootMain {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootMain.class, args);
    }
}
複製程式碼
  1. 定義介面, 實現並呼叫

介面

package com.north.spilat.service;
/**
 * @author lhh
 */
public interface DubboDemoService {
    String test(String params);
}
複製程式碼

實現介面

package com.north.spilat.service.impl;

import com.alibaba.dubbo.config.annotation.Service;
import com.north.spilat.service.DubboDemoService;
import org.springframework.stereotype.Repository;

/**
 * @author lhh
 */
@Service
@Repository("dubboDemoService") 
public class DubboDemoServiceImpl implements DubboDemoService {
    @Override
    public String test(String params) {
        return System.currentTimeMillis() + "-" + params ;
    }
}
複製程式碼

寫個controller呼叫dubbo介面

package com.north.spilat.controller;

import com.north.spilat.service.DubboDemoService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @author lhh
 */
@RestController
public class HelloWorldController {
    @Resource
    private DubboDemoService dubboDemoService;

    @RequestMapping("/saveTheWorld")
    public String index(String name) {
        return   dubboDemoService.test(name);
    }
}
複製程式碼

做完以上4步(zookeeper等環境自己裝一下)後, 啟動SpringBootMain類, 一個帶有dubbo模組的springboot工程就這樣搭好了, 真的就這麼簡單.

然而, 世界上哪有什麼歲月靜好,只不過是有人替你負重前行而已, 這個替你負重的人就是"dubbo-spring-boot-starter"

dubbo-spring-boot-starter的奧祕

image
上圖是dubbo-spring-boot-starter.jar包的結構. 內容還真不少, 但是聰明的你肯定想到了, 既然我們上一節說到了SPI是跟META-INF息息相關的,那我們這一節也必然是這樣.
因此, 這裡我們先看一下META-INF目錄下面有什麼.

dubbo/com.alibaba.dubbo.rpc.InvokerListener

dubbosubscribe=com.alibaba.dubbo.spring.boot.listener.ConsumerSubscribeListener
複製程式碼

這個目錄下的檔案只有一行,看著和上面的jdk的SPI真的是像.沒錯, 這的確是一種擴充點, 是dubbo裡面的一種擴充點約定, 就是我們開篇說的ExtensionLoader啦

  1. spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.dubbo.spring.boot.DubboAutoConfiguration,\
com.alibaba.dubbo.spring.boot.DubboProviderAutoConfiguration,\
com.alibaba.dubbo.spring.boot.DubboConsumerAutoConfiguration

org.springframework.context.ApplicationListener=\
com.alibaba.dubbo.spring.boot.context.event.DubboBannerApplicationListener

複製程式碼

哇哇哇,檔案就是以spring命名,檔案內容還涉及到這麼多spring類. 確認過眼神, 我遇上對的...檔案. 但是別急, 下面還有一個spring.providers檔案

  1. spring.providers
provides: dubbo-spring-boot-starter
複製程式碼

spring.providers就這麼簡單的一句, 有點失望了.所以我們還是來關注一下spring.factories吧.

imager
在IDEA裡面搜一下spring.factories這個檔案. 不搜不知道, 一搜嚇一跳. 原來基本上每一個springboot相關的jar包裡面都會有一個這樣的檔案.

物理學家在做實驗之前, 總是喜歡推理一番, 得到一個預測的結論, 然後再通過實驗結果來證實或推翻預測的結論.

因此, 基於JDK裡面的SPI機制, 在這裡我們也可以做一個大膽的預測:spring框架裡面一定是有一個類似於ServiceLoader的類, 專門從META-INF/spring.factories裡面的配置,載入特定介面的實現.

結果不用說, 這個預測肯定是準確, 不然我上面這麼多字不就白寫啦. 但是怎麼證明我們的預測是準確的呢. 讓我們也來做一次"實驗".

springboot的啟動過程

要弄清楚springboot的啟動過程, 最好的辦法就研讀它的原始碼了.

而springboot的程式碼還是非常"人性化"的,springboot明明確確地告訴你了, 它的入口就是main方法.因此, 讀springboot的程式碼, 還算是比較愜意的,從main方法一路看下去就可以了.

image

上圖就是一個springboot工程的啟動過程.首先是連續兩個過載的靜態run方法, 靜態run方法內部會呼叫構造方法例項化SpringApplication物件, 構造方法內部是呼叫initialiaze()進行初始化的,例項化,再呼叫一個成員方法run()來正式啟動.

可見,整個啟動過程主要的邏輯都在initialiaze方法和成員run方法內部了.

看一下initialiaze()的邏輯, 下面也是老規矩,主要貼程式碼,分析都在程式碼註釋中

   @SuppressWarnings({ "unchecked", "rawtypes" })
   private void initialize(Object[] sources) {
       // sources一般是Configuration類或main方法所在類
       // 可以有多個
   	if (sources != null && sources.length > 0) {
   		this.sources.addAll(Arrays.asList(sources));
   	}
   	// 判斷是否是web環境
   	// classLoader能載入到
   	// "javax.servlet.Servlet",
   	//	"org.springframework.web.context.ConfigurableWebApplicationContext"
   	// 這兩個類就是web環境	
   	this.webEnvironment = deduceWebEnvironment();
   	// 載入initializers 和listeners
   	// getSpringFactoriesInstances顧名思義,
   	// 就是載入某個介面的工廠例項,
   	// 看起來像是我們要找的"ServiceLoader"setInitializers((Collection) getSpringFactoriesInstances(
   			ApplicationContextInitializer.class));
   	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
   	// 找到main方法所在的類
   	this.mainApplicationClass = deduceMainApplicationClass();
   }
複製程式碼

運氣還算不錯,"嫌疑犯"getSpringFactoriesInstances就露出水面了, 來看看它的邏輯

    /**
    * 引數type就是要載入的介面的class
    */
    private <T> Collection<? extends T>
    getSpringFactoriesInstances(Class<T> type) {
        // 直接呼叫過載方法getSpringFactoriesInstances
		return getSpringFactoriesInstances(type, new Class<?>[] {});
	}

	private <T> Collection<? extends T>
	        getSpringFactoriesInstances(Class<T> type,
			Class<?>[] parameterTypes, 
			Object... args) {
		// 獲取當前執行緒的classLoader	
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
		// Use names and ensure unique to protect against duplicates
		// 翻譯一下原文註釋就是用names來去重
		// 注意這裡, 我們尋找的"ServiceLoader"終於出現了
		// 就是SpringFactoriesLoader
		Set<String> names = new LinkedHashSet<String>(
				SpringFactoriesLoader.loadFactoryNames(type, classLoader));
		// 是用java反射來例項化		
		List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
				classLoader, args, names);
		// 根據@Order註解來排一下序		
		AnnotationAwareOrderComparator.sort(instances);
		// 返回這個介面的所有實現例項
		return instances;
	}
複製程式碼

然後很快就找到了我們想找的SpringFactoriesLoader, 而且這個類非常小, 程式碼比JDK的ServiceLoader還少. 那我們仔細看一下他裡面都有啥.

  1. FACTORIES_RESOURCE_LOCATION 正是指向我們上面所說的META-INF/spring.factories
  2. loadFactories, 從META-INF/spring.factories查詢指定的介面實現類並例項化, 其中查詢是通過呼叫loadFactoryNames
  3. loadFactoryNames從指定的位置查詢特定介面的實現類的全限定名稱
  4. instantiateFactory 例項化

這個類就是springboot裡面的"ServiceLoader",它提供了一種機制,可以讓服務提供商指定某種介面的實現(可以是多個),例如上面的ApplicationContextInitializer.class和ApplicationListener.class介面, 如果我們想在我們的模組裡面指定我們的實現,或者想在現有的程式碼上加上我們的某個實現,就可以在/META-INF/spring.factories裡面指定. 等一下下面我會寫一個具體的例子, 可以讓大家更好的理解一下.

/**
* 省略import
**/
public abstract class SpringFactoriesLoader {

	private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);

	/**
	 * The location to look for factories.
	 *  查詢工廠實現類的位置
	 * <p>Can be present in multiple JAR files.
	 *   可以在多個jar包中
	 * 這不就是我們一直在尋找的META-INF/spring.factories嘛
	 * 終於找到了
	 */
	public static final String FACTORIES_RESOURCE_LOCATION =
	"META-INF/spring.factories";


	/**
	 * 查詢並例項化指定的工廠類實現
	 */
	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();
		}
		// 最終是呼叫loadFactoryNames
		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;
	}

	/**
	 * 從META-INF/spring.factories查詢指定介面的實現類的
	 * 全限定類名稱
	 */
	public static List<String> loadFactoryNames(
	Class<?> factoryClass, ClassLoader classLoader) {
	    // 介面的類名稱
		String factoryClassName = factoryClass.getName();
		try {
		    //載入所有的META-INF/spring.factories檔案資源
			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代表一個spring.factories檔案
				URL url = urls.nextElement();
				// 載入所有的屬性, 一般是 xxx介面=impl1,impl2 這種形式的
				Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
				// 根據介面名獲取的類似"impl1,impl2"的字串
				String factoryClassNames = properties.getProperty(factoryClassName)
				// 以逗號分隔,轉化成列表
				result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
			}
			// 返回實現類名的列表
			return result;
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() +
					"] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
		}
	}

    /**
    * 根據類名的全限定名稱例項化
    */
	@SuppressWarnings("unchecked")
	private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {
		try {
		    // 查詢類
			Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader);
			// 校驗是不是該介面類或該介面類的實現類
			if (!factoryClass.isAssignableFrom(instanceClass)) {
				throw new IllegalArgumentException(
						"Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]");
			}
			Constructor<?> constructor = instanceClass.getDeclaredConstructor();
			ReflectionUtils.makeAccessible(constructor);
			// 反射例項化
			return (T) constructor.newInstance();
		}
		catch (Throwable ex) {
			throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), ex);
		}
	}

}

複製程式碼

看完SpringFactoriesLoader這個類, initialize()方法的邏輯也就看完了. 接著再看另外一個重要方法run(String... args)

/**
	 * Run the Spring application, creating and refreshing a new
	 * {@link ApplicationContext}.
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return a running {@link ApplicationContext}
	 */
	public ConfigurableApplicationContext run(String... args) {
	    // 用於監測啟動時長等等
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		// springboot的上下文
		ConfigurableApplicationContext context = null;
		FailureAnalyzers analyzers = null;
		// 配置headless模式
		configureHeadlessProperty();
		// 啟動監聽器, 可以配置到spring.factories中去
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
		    // 封裝引數
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(
					args);
			// 	配置environment	
			ConfigurableEnvironment environment = prepareEnvironment(listeners,
					applicationArguments);
			// 列印banner		
			Banner printedBanner = printBanner(environment);
			// 建立上下文
			context = createApplicationContext();
			analyzers = new FailureAnalyzers(context);
			// 先初始化上下文
			prepareContext(context, environment, listeners, applicationArguments,
					printedBanner);
			// spring 經典的refresh()過程, 大部分的邏輯都在裡面
			// 這裡不再深入, 讀者可以自行研讀程式碼或搜尋引擎
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			listeners.finished(context, null);
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass)
						.logStarted(getApplicationLog(), stopWatch);
			}
			return context;
		}
		catch (Throwable ex) {
			handleRunFailure(context, listeners, analyzers, ex);
			throw new IllegalStateException(ex);
		}
	}
複製程式碼

這個方法就是springboot啟動的主要邏輯了,內容很多,如果要全部說清楚的話, 恐怕再寫幾遍文章也說不完(給人家springboot一點最起碼的尊重好不好, 想一篇文章就理解透徹人家整個框架,人家不要面子的呀).所以這裡就不會再深入,對於本文,只要知道這個run()方法是啟動的主要邏輯就可以了, 另外記住
context = createApplicationContext();
refreshContext(context);
這兩行程式碼,等下我們還會看到它的.

dubbo-spring-boot-starter的原理

上面說了很多, 但是為什麼springboot引入一個starter的依賴,就能引入一個複雜的模組. 這裡通過dubbo-spring-boot-starter來研究一下.

我們檢視一下dubbo-spring-boot-starter裡面spring.factories. 可以發現裡面配置了兩個介面, 一個是EnableAutoConfiguration,一個是ApplicationListener.

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.dubbo.spring.boot.DubboAutoConfiguration,\
com.alibaba.dubbo.spring.boot.DubboProviderAutoConfiguration,\
com.alibaba.dubbo.spring.boot.DubboConsumerAutoConfiguration

org.springframework.context.ApplicationListener=\
com.alibaba.dubbo.spring.boot.context.event.DubboBannerApplicationListener
複製程式碼

監聽器看名稱就知道了是用於啟動的時候列印banner, 所以這裡暫時不看, 我們先來看一下EnableAutoConfiguration是哪裡用到的.

從main方法開始一路debug,終於在AutoConfigurationImportSelector類中發現了一行程式碼:
SpringFactoriesLoader.loadFactoryNames( getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader())

其中getSpringFactoriesLoaderFactoryClass()就是寫死了返回EnableAutoConfiguration.class

 protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
 		AnnotationAttributes attributes) {
 	List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
 			getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
 	Assert.notEmpty(configurations,
 			"No auto configuration classes found in META-INF/spring.factories. If you "
 					+ "are using a custom packaging, make sure that file is correct.");
 	return configurations;
 }

 /**
  * Return the class used by {@link SpringFactoriesLoader} to load configuration
  * candidates.
  * @return the factory class
  */
 protected Class<?> getSpringFactoriesLoaderFactoryClass() {
 	return EnableAutoConfiguration.class;
 }
複製程式碼

如下圖可以發現,EnableAutoConfiguration.class的實現會有很多, 只要你在spring.fatories配置了,它都會給你載入進來

image
載入了之後,又幹嘛呢, 往下看,可以發現大概流程是這樣:

  1. this.reader.loadBeanDefinitions(configClasses); configClasses就是所有的實現類,把這些類讀進來準備解析
  2. registerBeanDefinition註冊到beanDefinitionNames
  3. spring的refresh()操作中,最後有一步是finishBeanFactoryInitialization(beanFactory), 這一步時會初始化所有的單例物件, 最後會從beanDefinitionNames讀取所有的BeanDefinition,也包括了上面的所有EnableAutoConfiguration實現, 然後進行例項化
  4. 例項化EnableAutoConfiguration的具體實現的時候,會執行這些實現類裡面的具體邏輯, 以Dubbo為例,會初始化com.alibaba.dubbo.spring.boot.DubboAutoConfiguration,
    com.alibaba.dubbo.spring.boot.DubboProviderAutoConfiguration,
    com.alibaba.dubbo.spring.boot.DubboConsumerAutoConfiguration 這三個實現類, 就把dubbo啟動並註冊到spring容器中去了.

實現一個spring-boot-starter

清楚了原理之後, 要實現一個自己的starter就很簡單了.

假設我有一個元件,非常牛逼,具有拯救世界的能力, 你的系統接入後,也就具有了拯救世界的能力了. 那怎麼讓你的spring-boot系統可以快速接入這個牛逼的元件呢. 我來實現一個starter, 你依賴我這個starter就可以了

首先定義一個拯救世界的介面

package com.north.lat.service;

/**
* @author lhh
*/
public interface SaveTheWorldService {
 /**
  *  拯救世界
  * @param name 留名
  * @return
  */
 String saveTheWorld(String name);
}
複製程式碼

抽象類

package com.north.lat.service;

import lombok.extern.log4j.Log4j;

import java.util.Random;

/**
 * @author lhh
 */
@Log4j
public abstract  class AbstractSaveTheWorldService implements SaveTheWorldService {
    private final static Random RANDOM = new Random();
    private final static String SUCCESS_MSG = "WAOOOOOOO! 大英雄";
    private final static String FAIL_MSG = "拯救世界是個高風險行業";

    @Override
    public String saveTheWorld(String name) {
        int randomInt = RANDOM.nextInt(100);
        String msg;
        if((randomInt +  1) > getDieRate()){
            msg = SUCCESS_MSG +"," + name + "拯救了這個世界!";
        }else{
            msg = FAIL_MSG + "," + name + ",你失敗了,下輩子再來吧";

        }
        log.info(msg);
        return msg;
    }

    /**
     * 指定死亡率
     * @return
     */
    public abstract int getDieRate();
}
複製程式碼

普通人去拯救世界,一般失敗率是99%

package com.north.lat.service.impl;


import com.north.lat.service.AbstractSaveTheWorldService;

/**
 * 普通人拯救世界
 * @author lhh
 */
public class CommonSaveTheWorldServiceImpl extends AbstractSaveTheWorldService {
    private final static int DIE_RATE = 99;

    @Override
    public int getDieRate() {
        return DIE_RATE;
    }
}

複製程式碼

以英雄角色去拯救世界,成功率是99%

package com.north.lat.service.impl;

import com.north.lat.service.AbstractSaveTheWorldService;

/**
 * 英雄拯救世界
 * @author lhh
 */
public class HeroSaveTheWorldImpl extends AbstractSaveTheWorldService {
    private final static int DIE_RATE = 1;
    @Override
    public int getDieRate() {
        return DIE_RATE;
    }
}

複製程式碼

好, 我們這個超級牛逼的元件就誕生了, 下面為接入springboot準備一下, 實現一個NbAutoConfiguration如下:

package com.north.lat;

import com.north.lat.service.SaveTheWorldService;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.SpringFactoriesLoader;

import java.util.List;

/**
 * @author lhh
 * 注入environment和applicationContext 以便做一些後續操作
 */
@Configuration
@ConditionalOnClass(SaveTheWorldService.class)
public class NbAutoConfiguration implements EnvironmentAware,ApplicationContextAware,BeanDefinitionRegistryPostProcessor {
    private Environment environment;
    private ApplicationContext applicationContext;

    @Override
    public void setEnvironment(Environment environment) {
            this.environment = environment;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
           this.applicationContext = applicationContext;
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // 我這裡是從spring.factories載入了SaveTheWorldService的所有實現,
        List<SaveTheWorldService> saveTheWorldServices = SpringFactoriesLoader.loadFactories(SaveTheWorldService.class, this.getClass().getClassLoader());
        // 然後用BeanDefinitionRegistry 註冊到BeanDefinitions
        saveTheWorldServices.forEach(saveTheWorldService->{
            GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
            beanDefinition.setBeanClass(saveTheWorldService.getClass());
            beanDefinition.setLazyInit(false);
            beanDefinition.setAbstract(false);
            beanDefinition.setAutowireCandidate(true);
            beanDefinition.setScope("singleton");
            registry.registerBeanDefinition(saveTheWorldService.getClass().getSimpleName(), beanDefinition);
        });
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    }
}
複製程式碼

再配置一下spring.factories
在元件開發初期,英雄還沒找到,只能派個普通人去,所以niubility-spring-starter-1.0-SNAPSHOT.jar的spring.factories是這樣的

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.north.lat.NbAutoConfiguration
com.north.lat.service.SaveTheWorldService=\
com.north.lat.service.impl.CommonSaveTheWorldServiceImpl
複製程式碼

後來經過開發人員無數個日日夜夜的加班,終於找到了英雄,所以niubility-spring-starter-2.0-SNAPSHOT.jar的spring.factories變成了這樣

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.north.lat.NbAutoConfiguration
com.north.lat.service.SaveTheWorldService=\
com.north.lat.service.impl.HeroSaveTheWorldImpl
複製程式碼

這樣就完成了,專案結構如下圖所示:

image

那該怎麼接入呢? 我們在剛剛的spilat工程接入一下試試:

依賴jar包,這個時候是接入1.0版本的;這樣就完成接入了

        <dependency>
            <groupId>com.north.lat</groupId>
            <artifactId>niubility-spring-starter</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
複製程式碼

所謂的完成接入是指, spring中已經註冊了SaveTheWorldService的所有實現, 即CommonSaveTheWorldServiceImpl(1.0版本)或HeroSaveTheWorldImpl(2.0版本).

我們在controller中注入呼叫一下

package com.north.spilat.controller;

import com.north.lat.service.SaveTheWorldService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @author lhh
 */
@RestController
public class HelloWorldController {
    @Resource
    private SaveTheWorldService saveTheWorldService;


    @RequestMapping("/saveTheWorld")
    public String index(String name) {
        return  saveTheWorldService.saveTheWorld(name);
    }
}
複製程式碼

使用1.0版本的時候,果然是失敗率99%,執行結果如下:

image
等2.0版本出來後, 趕緊換上2.0版本, 在pom.xml更新一下版本號:

複製程式碼
    <dependency>
        <groupId>com.north.lat</groupId>
        <artifactId>niubility-spring-starter</artifactId>
        <version>2.0-SNAPSHOT</version>
    </dependency>
複製程式碼
再看看執行結果, 就非常完美啦
複製程式碼

image

在上面的例子中, 不管是我們接入還是升級元件, 都是簡單的依賴jar包就可以了,真正的實現了可拔插,低耦合. 當然, 實際的應用場景中, 可能還需要我們增加少許的配置,例如上面的spring-boot-starter-dubbo, 以及我們經常用的druid-spring-boot-starter,spring-boot-starter-disconf等等

總結

解耦,可以說是數代程式設計師都窮極一生都在追求的東西, 這些年來提出和實現了無數的工具和思想, SPI便是沉澱出來的一種。

SPI機制在各種開源框架中都是非常常見的,而各種框架的SPI機制又各有不同, 或多或少都有一些演變;但是其實背後的原理都是大同小異.

因此, 瞭解一下這些機制, 一方面可以讓我們更清楚開源框架的執行原理,少走彎路; 另一方面,也可以作為我們日常寫程式碼和系統設計的一種參考,從而寫出更加優雅的程式碼.

相關文章