天天看點

JDK ,DUBBO , SPRING 的SPI機制

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;
}