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 colourLoader = ServiceLoader.load(ColourConfig.class);
        Iterator 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() {
                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() {
            public Void run() {

                ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator 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 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 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  ServiceLoader load(Class service) {
		//注意,这里的ClassLoader是AppClasLoader,我们获取类加载器一般是用getClassLoader(),这里为什么不是呢?请看3
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
public static  ServiceLoader load(Class service,ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }
private ServiceLoader(Class 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  ExtensionLoader getExtensionLoader(Class type) {
        ExtensionLoader loader = (ExtensionLoader) EXTENSION_LOADERS.get(type);
        if (loader == null) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type));
            loader = (ExtensionLoader) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }

2.ExtensionLoader#getExtension

public T getExtension(String name) {
		if ("true".equals(name)) {
		    return getDefaultExtension();
		}
		Holder 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> 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> getExtensionClasses() {
        cachedClasses.set( loadExtensionClasses());
	}
private Map> loadExtensionClasses() {
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        ...
        Map> extensionClasses = new HashMap>();
        //以下就是按顺序加载3个目录下的配置文件,并且是懒加载,key冲突则跳过,所以冲突则以前面的为准
        loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
        loadFile(extensionClasses, DUBBO_DIRECTORY);
        loadFile(extensionClasses, SERVICES_DIRECTORY);
        return extensionClasses;
    }
private void loadFile(Map> extensionClasses, String dir) {
	//IO流就省略了
	//cachedAdaptiveClass 这就对应着Adaptive注解在类上的场景
	if (clazz.isAnnotationPresent(Adaptive.class)) {
		  if(cachedAdaptiveClass == null) {
		       cachedAdaptiveClass = clazz;
		   }
		}else{
			try {
				//试图获取当前类的构造器,若能获取到带当前类型参数的构造器,则认为是包装类,否则进入Catch。所以Catch这里是有意义的
	            clazz.getConstructor(type);
	            Set> wrappers = cachedWrapperClasses;
	            if (wrappers == null) {
	                cachedWrapperClasses = new ConcurrentHashSet>();
	                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  List loadFactories(Class factoryClass, ClassLoader classLoader) {
		...
		//1.加载xml文件,获取到实现类的类名列表
		List factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
		
		List result = new ArrayList(factoryNames.size());
		for (String factoryName : factoryNames) {
			//实例化类
			result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
		}
		//排序
		AnnotationAwareOrderComparator.sort(result);
		return result;
}
public static List loadFactoryNames(Class factoryClass, ClassLoader classLoader) {
		String factoryClassName = factoryClass.getName();
		Enumeration urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
					ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
		List result = new ArrayList();
		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;
}

你可能感兴趣的:(spring,java,spi,dubbo)