SPI的全稱是Service Provider Interface, 直譯過來就是"服務提供接口", 聽起來挺彆扭的, 因此我試着去就將它翻譯爲"服務提供商接口"吧.
java
咱們都知道, 一個接口是能夠有不少種實現的. 例如搜索,能夠是搜索系統的硬盤,也能夠是搜索數據庫.系統的設計者爲了下降耦合,並不想在硬編碼裏面寫死具體的搜索方式,而是但願由服務提供者來選擇使用哪一種搜索方式, 這個時候就能夠選擇使用SPI機制.程序員
SPI機制被大量應用在各類開源框架中,例如:web
SPI估計你們都有所瞭解,讓咱們經過一個很是簡單的例子,來溫習一下java裏面的SPI機制吧.
spring
package com.north.spilat.service;
import java.util.List;
public interface Search {
List<String> search(String keyword);
}
複製代碼
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;
}
}
複製代碼
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;
}
}
複製代碼
在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
整個工程目錄以下:
數據庫
新建一個main方法測試一下bootstrap
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");
}
}
}
複製代碼
運行一下,輸出以下:
api
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的大致流程.緩存
回到上面的main方法,其實沒有什麼特別的,除了一句
ServiceLoader.load(Search.class);
springboot
ServiceLoader.class是一個工具類,根據META-INF/services/xxxInterfaceName下面的文件名,加載具體的實現類.
bash
從load(Search.class)進去,咱們來扒一下這個類,下面主要是貼代碼,分析都在代碼註釋內.
/*
*入口, 獲取一下當前類的類加載器,而後調用下一個靜態方法
*/
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);
}
複製代碼
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;
}
複製代碼
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的魅力.
做爲一個程序員,沒事能夠多點研究開源框架,由於這些開源代碼天天都不知道被人擼幾遍,因此他們的代碼從設計到實現,都是很是優秀的,咱們能夠從中學到很多東西.
而spring框架這些年來,基本上能夠說是開源界扛把子,江湖上無人不知無人不曉.其源碼的設計也是出了名的優雅,超高拓展性超低耦合性.
那它是怎麼解耦的呢? 拓展點機制即是其中法寶之一
剛剛接觸springboot的時候, 真的以爲各類spring-xx-starter和xx-spring-starter很是的神奇. 爲何在pom文件添加一個依賴就能引入一個複雜的插件了呢? 帶着這個疑問,我開始了個人走進科學之旅.
dubbo框架在國內用的公司挺多的,因此這裏, 咱們就以dubbo-spring-boot-starter爲例,來看看springboot中是如何高效解耦的.
回想一下, 若是咱們要在springboot工程裏面引入dubbo模塊, 須要怎麼作.
<dependency>
<groupId>com.alibaba.spring.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
複製代碼
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
複製代碼
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);
}
}
複製代碼
接口
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/com.alibaba.dubbo.rpc.InvokerListener
dubbosubscribe=com.alibaba.dubbo.spring.boot.listener.ConsumerSubscribeListener
複製代碼
這個目錄下的文件只有一行,看着和上面的jdk的SPI真的是像.沒錯, 這的確是一種拓展點, 是dubbo裏面的一種拓展點約定, 就是咱們開篇說的ExtensionLoader啦
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文件
provides: dubbo-spring-boot-starter
複製代碼
spring.providers就這麼簡單的一句, 有點失望了.因此咱們仍是來關注一下spring.factories吧.
物理學家在作實驗以前, 老是喜歡推理一番, 獲得一個預測的結論, 而後再經過實驗結果來證明或推翻預測的結論.
所以, 基於JDK裏面的SPI機制, 在這裏咱們也能夠作一個大膽的預測:spring框架裏面必定是有一個相似於ServiceLoader的類, 專門從META-INF/spring.factories裏面的配置,加載特定接口的實現.
結果不用說, 這個預測確定是準確, 否則我上面這麼多字不就白寫啦. 可是怎麼證實咱們的預測是準確的呢. 讓咱們也來作一次"實驗".
要弄清楚springboot的啓動過程, 最好的辦法就研讀它的源碼了.
而springboot的代碼仍是很是"人性化"的,springboot明明確確地告訴你了, 它的入口就是main方法.所以, 讀springboot的代碼, 還算是比較愜意的,從main方法一路看下去就能夠了.
上圖就是一個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還少. 那咱們仔細看一下他裏面都有啥.
這個類就是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);
這兩行代碼,等下咱們還會看到它的.
上面說了不少, 可是爲何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配置了,它都會給你加載進來
清楚了原理以後, 要實現一個本身的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
複製代碼
這樣就完成了,項目結構以下圖所示:
那該怎麼接入呢? 咱們在剛剛的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%,運行結果以下:
複製代碼
<dependency>
<groupId>com.north.lat</groupId>
<artifactId>niubility-spring-starter</artifactId>
<version>2.0-SNAPSHOT</version>
</dependency>
複製代碼
再看看運行結果, 就很是完美啦
複製代碼
在上面的例子中, 無論是咱們接入仍是升級組件, 都是簡單的依賴jar包就能夠了,真正的實現了可拔插,低耦合. 固然, 實際的應用場景中, 可能還須要咱們增長少量的配置,例如上面的spring-boot-starter-dubbo, 以及咱們常常用的druid-spring-boot-starter,spring-boot-starter-disconf等等
解耦,能夠說是數代程序員都窮極一輩子都在追求的東西, 這些年來提出和實現了無數的工具和思想, SPI即是沉澱出來的一種。
SPI機制在各類開源框架中都是很是常見的,而各類框架的SPI機制又各有不一樣, 或多或少都有一些演變;可是其實背後的原理都是大同小異.
所以, 瞭解一下這些機制, 一方面可讓咱們更清楚開源框架的運行原理,少走彎路; 另外一方面,也能夠做爲咱們平常寫代碼和系統設計的一種參考,從而寫出更加優雅的代碼.