JDK ,DUBBO , SPRING 的SPI機制
SPI 全稱為 Service Provider Interface,是一種服務發現機制。SPI
的本質是将接口實作類的全限定名配置在檔案中,并由服務加載器讀取配置檔案,加載實作類。這樣可以在運作時,動态為接口替換實作類。正是以特性,我們可以很容易的通過
SPI 機制為我們的程式提供拓展功能。
摘要自 https://segmentfault.com/a/1190000039812642
SPI能解決什麼問題?
I hava a ColourConfig ,我怎麼根據不同人的不同需要,擷取到不同的顔色呢?
public interface ColourConfig {
String myColour();
}
public class BlueConfig implements ColourConfig{
@Override
public String myColour() {
return "blue";
}
}
public class RedConfig implements ColourConfig {
@Override
public String myColour() {
return "red";
}
}
普通程式員說:我管你怎麼使用,我就給你開接口就完事了
public class ColourFactory {
ColourConfig redColor = new RedConfig();
ColourConfig blueColor = new BlueConfig();
public String getBlueColor() {
return blueColor.myColour();
}
public String getRedColor() {
return redColor.myColour();
}
}
産品經理說:我預計接下來要開個五顔六色花裡胡哨的顔色工廠,滿足所有使用者的需求。
普通程式員卒。
對于需要擴充的業務本身,我們是能接受水準擴充。但是面向的對象的設計裡,我們一般推薦子產品之間基于接口程式設計,業務内的水準擴充,調用方是不感覺的。
那初步修改方案就是,對外隻提供一個接口,給不同的調用方定制ColourFactory 。
public class ColourFactory {
ColourConfig colorConfig= new RedConfig();
public String getColor() {
return colorConfig.myColour();
}
}
但是這還是很麻煩啊,能不能有個方案,我對外都是一套代碼,但是能實作針對不同調用方裝配不同的Config呢?
有,基于配置。無論JDK 的SPI,還是DUBBO,Spring的SPI,核心都是基于配置。先來看看他們,是如何做的。
SPI的使用
JDK SPI
1.代碼
public class JavaColorColourFactory {
public static String getColor(){
ServiceLoader<ColourConfig> colourLoader = ServiceLoader.load(ColourConfig.class);
Iterator<ColourConfig> colourIterator = colourLoader.iterator();
ColourConfig colourConfig = null;
while (colourIterator.hasNext()){
colourConfig = colourIterator.next();
}
return colourConfig == null ? null : colourConfig.myColour();
}
}
2.配置:
在META-INF\services目錄下,建立以ColourConfig全類名命名的檔案,配置的内容就是所需實作類的全類名。
3.注意事項:
3.1 maven項目,配置檔案需要放置在resources目錄下,網上很多比較老的資料,顯示是放置在java目錄下,是不能生效的~
3.2 以上實作類隻能掃描到正常的類,編寫文檔時,為了友善,将實作類寫為接口的内部類,發現是掃描不到的!
3.3 配置檔案可配多個實作類,上述代碼可知,JDK 的SPI機制是周遊檔案,取出所有配置的實作類,是以實際使用場景中,jar包本身所配置的檔案,與調用方所配置的檔案,無法确定加載順序!是以使用JDK的SPI時,確定加載指定配置的方式是,確定隻會引入一個配置檔案,并且本身不指定任何預設配置。資料庫驅動就是使用的這種方式,java.sql.Driver隻是定義了一個規範,并沒有任何預設實作,java.sql.DriverManager中會加載所有的配置。
代碼如下:注釋中也有提到,Get all the drivers through the classloader,是以說,JDK的SPI機制,更加适用于加載所有配置,而不是加載指定配置!(若隻有一個配置,當然就隻加載一個了)
private static void loadInitialDrivers() {
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.
//注意下面這行注釋,加載所有的drivers !
// 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<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} 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);
}
}
}
DUBBO SPI
1.普通用法
@SPI("blue")
public interface ColourConfig {
String myColour();
}
public class BlueConfig implements ColourConfig {
@Override
public String myColour() {
return "blue";
}
}
public class RedConfig implements ColourConfig {
@Override
public String myColour() {
return "red";
}
}
使用:
public class DubboColourFactory {
@Test
public void testColor(){
String color = getColor();
System.out.println(color);
}
public String getColor(){
//red
String red = ExtensionLoader.getExtensionLoader(ColourConfig.class).getExtension("red").myColour();
//接口中SPI指定的預設,為blue
String defaultColour = ExtensionLoader.getExtensionLoader(ColourConfig.class).getDefaultExtension().myColour();
return red;
}
}
2.配置
META-INF/dubbo/internal/
META-INF/dubbo/
META-INF/services/
以上三個檔案夾都可以,dubbo會按順序加載,需要注意的是,同名會跳過!就是說同樣的配置,如果META-INF/dubbo/和META-INF/services/都有,最終生效的是META-INF/dubbo/中的。
檔案名為接口全類名,内容為全部實作類的key=全類名
如:
red=cn.com.test.dubbo.RedConfig
3.進階版(配置檔案不變)
@SPI("blue")
public interface ColourConfig {
//Adaptive注解修飾的方法必須使用URL作為傳參,Adaptive修飾方法時,Dubbo架構會生成預設擴充卡。
//該注解也可修飾類,修飾類時需自定義Adaptive擴充卡
@Adaptive
String myColour(URL url);
}
public class BlueConfig implements ColourConfig {
@Override
public String myColour(URL url) {
return "blue";
}
}
public class RedConfig implements ColourConfig {
@Override
public String myColour(URL url) {
return "red";
}
}
使用
public class DubboColourFactory {
@Test
public void testColor(){
String color = getColor();
System.out.println(color);
}
public String getColor(){
//這個map的作用是路由,key是Adaptive所指定的value,如果沒有指定,預設為所修飾的類名,駝峰改為點連接配接
Map<String,String> paramMap = new HashMap<>();
paramMap.put("colour.config","red");
URL red = new URL("", null, 0,paramMap);
String adaptiveColour = ExtensionLoader.getExtensionLoader(ColourConfig.class).getAdaptiveExtension().myColour(red);
return adaptiveColour;
}
}
4.終極進階版
@SPI("blue")
public interface ColourConfig {
String myColour();
}
@Adaptive
public class AdaptiveColourConfig implements ColourConfig{
private static volatile String DEFAULT_COLOUR;
public static void setDefaultColour(String colour) {
DEFAULT_COLOUR = colour;
}
@Override
public String myColour() {
if (StringUtils.isBlank(DEFAULT_COLOUR)){
return "blank";
}
return ExtensionLoader.getExtensionLoader(ColourConfig.class).getExtension(DEFAULT_COLOUR).myColour();
}
}
public class BlueConfig implements ColourConfig {
@Override
public String myColour() {
return "blue";
}
}
public class RedConfig implements ColourConfig {
@Override
public String myColour() {
return "red";
}
}
public class DubboColourFactory {
@Test
public void testColor(){
String color = getColor();
System.out.println(color);
}
public String getColor(){
AdaptiveColourConfig.setDefaultColour("red");
String adaptiveColour = ExtensionLoader.getExtensionLoader(ColourConfig.class).getAdaptiveExtension().myColour();
return adaptiveColour;
}
}
配置檔案需新增:
//這個key可以随便寫,都會生效的,注意不要和其他key一樣就好
adaptive=cn.com.test.dubbo.AdaptiveColourConfig
SPRING SPI
1.代碼:ColourConfig相關代碼我就不貼了,就是簡單的接口+實作類。
public class SpringColourFactory extends BaseTest {
@Test
public void test(){
List<ColourConfig> colourConfigs = SpringFactoriesLoader.loadFactories(ColourConfig.class, this.getClass().getClassLoader());
colourConfigs.stream().forEach(colourConfig -> System.out.println(colourConfig.myColour()));
}
}
2.配置檔案
META-INF檔案夾下:
檔案名:spring.factories
檔案内容:(\代表換行,也可以删除\,隻占一行)
cn.com.test.spring.ColourConfig=\
cn.com.test.spring.BlueConfig,\
cn.com.test.spring.RedConfig
JDK,DUBBO,SPRING的SPI實作的對比
使用難度&學習難度 | 多個配置檔案 | 配置檔案 | 适用場景 |
---|---|---|---|
JDK | 簡單 | load全部 | 一個接口一個 |
SPRING | 簡單 | load全部 | 隻有一個配置檔案 |
DUBBO | 中等 | 根據别名決定加載誰,同名會去重 | 一個接口一個 |
SPI的原理
開始看源碼啦~以下都省略了一部分,隻展示關鍵步驟(我能看懂的步驟)
JDK SPI
1.ServiceLoader colourLoader = ServiceLoader.load(ColourConfig.class);
簡單來說,就是建立了一個ServiceLoader和LazyIterator。
public static <S> ServiceLoader<S> load(Class<S> service) {
//注意,這裡的ClassLoader是AppClasLoader,我們擷取類加載器一般是用getClassLoader(),這裡為什麼不是呢?請看3
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);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//儲存類加載器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
reload();
}
public void reload() {
providers.clear();
//這裡建立了一個疊代器
lookupIterator = new LazyIterator(service, loader);
}
2.iterator
以下其實邏輯非常簡單,就是讀取到配置檔案,然後去根據配置的全類名執行個體化對象,
public boolean hasNext() {
return hasNextService();
}
private boolean hasNextService() {
//拼出類路徑
String fullName = PREFIX + service.getName();
configs = loader.getResources(fullName);
//這裡面是位元組流去讀取檔案
pending = parse(service, configs.nextElement());
//讀取到的實作類全類名
nextName = pending.next();
return true;
}
public S next() {
return nextService();
}
private S nextService() {
String cn = nextName;
nextName = null;
Class<?> c = null;
c = Class.forName(cn, false, loader);
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
}
3.小知識
類加載大家都耳熟能詳了,這裡就不贅述了。大家都知道,雙親委派模式,即Bootstrap ClassLoader 、Extension ClassLoader、App ClassLoader的順序去加載,子ClassLoader委托父ClassLoader加載(當然,子父級關系并不是那麼明朗,隻是為了友善采用這種說法)。
假設本例中,ColourConfig與ColourFactory都是由App ClassLoader加載,而實作類卻是由自定義類加載器(parent為App ClassLoader)加載,那麼要怎麼實作呢?雙親委派模型限制,子加載器可以委托父類加載器進行加載,但是父類加載器卻無法加載到子類加載所加載的類。
是以這裡就有了ClassLoader cl = Thread.currentThread().getContextClassLoader();将classLoader放入線程上下文中進行傳遞,這就打破了雙親模式的限制。父類加載器所加載到的類可以通過這種方式擷取到子類加載器。
(小聲BB:打算做個DEMO的,沒做出來,後面做出來再補上)
DUBBO SPI
1.ExtensionLoader.getExtensionLoader(ColourConfig.class)
很簡單,就是擷取ExtensionLoader,這裡采用了懶加載模式,調用時發現沒有才建立,并且放入緩存
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
2.ExtensionLoader#getExtension
public T getExtension(String name) {
if ("true".equals(name)) {
return getDefaultExtension();
}
Holder<Object> holder = cachedInstances.get(name);
...
Object instance = holder.get();
...
instance = createExtension(name);
...
return (T) instance;
}
private T createExtension(String name) {
//這就是加載配置檔案的過程,
Class<?> clazz = getExtensionClasses().get(name);
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
...
//執行個體化擴充點的過程
injectExtension(instance);
//以下不是這個文章的重點,這裡貼出來用文字大概總結一下,具體細節就不看了
//以DubboProtocol為例,我們需要對其做監聽,過濾等操作,Dubbo的實作機制,是基于包裝類
//Dubbo會将包裝類加載到cachedWrapperClasses中,這裡是一個Set,是以最終調用時,是一個不保證順序的鍊式調用.Wrapper的别名也沒啥用(或許是我沒發現用處),就單純的一個辨別
//dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
//filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
//listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && wrapperClasses.size() > 0) {
for (Class<?> wrapperClass : wrapperClasses) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
...
}
}
private Map<String, Class<?>> getExtensionClasses() {
cachedClasses.set( loadExtensionClasses());
}
private Map<String, Class<?>> loadExtensionClasses() {
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
...
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
//以下就是按順序加載3個目錄下的配置檔案,并且是懶加載,key沖突則跳過,是以沖突則以前面的為準
loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
loadFile(extensionClasses, DUBBO_DIRECTORY);
loadFile(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;
}
private void loadFile(Map<String, Class<?>> extensionClasses, String dir) {
//IO流就省略了
//cachedAdaptiveClass 這就對應着Adaptive注解在類上的場景
if (clazz.isAnnotationPresent(Adaptive.class)) {
if(cachedAdaptiveClass == null) {
cachedAdaptiveClass = clazz;
}
}else{
try {
//試圖擷取目前類的構造器,若能擷取到帶目前類型參數的構造器,則認為是包裝類,否則進入Catch。是以Catch這裡是有意義的
clazz.getConstructor(type);
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz);
} catch (NoSuchMethodException e) {
clazz.getConstructor();
String[] names = NAME_SEPARATOR.split(name);
if (names != null && names.length > 0) {
//這個是更複雜一點的擴充機制,支援分組,排序等,不展開細說了,有時間再更上
Activate activate = clazz.getAnnotation(Activate.class);
...
for (String n : names) {
//将目前name和class放入緩存
//extensionClasses最終将放入cachedClasses中
//cachedClasses和cachedNames分别以name和Class為key存了兩份不同的資料,就是為了查詢更快
if (! cachedNames.containsKey(clazz)) {
cachedNames.put(clazz, n);
}
Class<?> c = extensionClasses.get(n);
if (c == null) {
extensionClasses.put(n, clazz);
}
}
}
}
}
}
private T injectExtension(T instance) {
//這裡面就是反射擷取instance的屬性,然後注入。
}
3.ExtensionLoader#getAdaptiveExtension
public T getAdaptiveExtension() {
instance = createAdaptiveExtension();
}
private T createAdaptiveExtension() {
//1.擷取到adaptiveExtensionClass
//2.執行個體化并初始化
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
}
private Class<?> getAdaptiveExtensionClass() {
//這個方法前面講過,
//1.配置檔案中load所有的class
//2.初始化這三個緩存:cachedAdaptiveClass ,cachedClasses,cachedWrapperClasses
//需要注意的是,這裡用到的cachedAdaptiveClass 是類上有Adaptive注解的類,上述說到的方法上标注的注解第一次進來的時候這裡是掃描不到的
getExtensionClasses();
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
//方法注解首次解析是在這裡
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
private Class<?> createAdaptiveExtensionClass() {
//這裡就是建立預設的AdaptiveExtension,具體方式就是字元串拼接,不細看了,文字寫下流程
//1.輪詢目前類的方法,若沒有方法有Adaptive注解,則報錯
//2.字元串拼接包名,引入ExtensionLoader全類名,拼接$Adpative字尾的一個Class,輪詢method,給有Adaptive注解的方法以字元串拼接的形式拼接成代碼。
//3.需要注意的細節是,Adaptive注解修飾的接口必須有URL類型的參數,或者參數中包含URL對象,否則報錯。Adaptive注解的value,就是最終路由所用的key,官方注釋:(沒有設定Key,則使用“擴充點接口名的點分隔 作為Key)
String code = createAdaptiveExtensionClassCode();
ClassLoader classLoader = findClassLoader();
//這個我也還沒看過。。。
com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
}
SPRING SPI
1.SpringFactoriesLoader#loadFactories
public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) {
...
//1.加載xml檔案,擷取到實作類的類名清單
List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
List<T> result = new ArrayList<T>(factoryNames.size());
for (String factoryName : factoryNames) {
//執行個體化類
result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
}
//排序
AnnotationAwareOrderComparator.sort(result);
return result;
}
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
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();
//這裡就是用IO流去加載檔案
Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
String factoryClassNames = properties.getProperty(factoryClassName);
//将加載到的字元串用逗号分隔
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
return result;
}