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