Dubbo源码阅读前夜-SPI的本质

简介: Dubbo源码阅读前夜-SPI的本质

前言


近日,在浏览Dubbo官网时看到了Dubbo SPI 这个词。搜了搜,原来JAVA有个SPI机制。好奇心驱使我想知道,这到底是个什么东西。


JAVA SPI机制


如果我们要动态加载一个类,会怎么办?

  • 调用 Class.forName("cn.test.Hello") 方法
  • 调用某个 ClassLoader. loadClass("cn.test.Hello") 方法

动态加载的好处,就是能在运行期按需加载,需要什么类,就加载什么类,编译期不报错。这样带来的好处,就是我们可以动态配置运行期加载什么类。

SPI ,全称为 Service Provider Interface,是一种服务发现机制。它能够加载ClassPath路径下的META-INF/services文件夹下的文件中,配置的类。


1.如何使用

先定义一个接口

public interface HellloService {
    public void sayHello();
}

定义两个实现类:

public class ChineseHello implements HellloService {
    @Override
    public void sayHello() {
        System.out.println("中文说你好");
    }
}
public class EnglishHello implements HellloService {
    @Override
    public void sayHello() {
        System.out.println("English  hello");
    }
}

接着接着我们建一个META-INF/services的文件夹,在文件夹内新建一个以接口全限定名为名字的文件


image.png


并在文件中配置接口的实现类。

com.service.hi.servicehi.spi.ChineseHello
com.service.hi.servicehi.spi.EnglishHello

然后使用ServiceLoader 在运行期动态的加载接口的实现类,调用其方法

public class SpiMain {
    public static void main(String[] args) {
        ServiceLoader<HellloService> services = ServiceLoader.load(HellloService.class);
        for (HellloService hellloService: services){
            hellloService.sayHello();
        }
    }
}
中文说你好
English  hello

由此看出: SPI就是一个“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制. ServiceLoader 就是动态加载动态的工具类。

所以:

  • 往大了说SPI是一种服务发现机制,
  • 往小了说SPI就是一个可配置化动态加载类的工具类。


2.源码分析

2.1ServiceLoader

我们再从源码的层面解开他的面目 ServiceLoader#load静态方法。

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

可以看出

  • ServiceLoader#load静态方法调用另一个重载的load方法并默认把当前线程的 ClassLoader 作为参数传递过去。
  • 从两个参数的load可以看出,我们可以指定其ClassLoader
  • load静态方法最终是new 一个 ServiceLoader实例出来。

下面看看ServiceLoader的构造方法。

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

发现没有加载配置文件的过程啊?

其实ServiceLoader使用懒加载的方式,也就是当我们在遍历的时候才去加载配置文件。LazyIterator 就是懒加载迭代器。


2.2LazyIterator
public S next() {
       if (acc == null) {
          return nextService();
       } 
}
private S nextService() {
            if (!hasNextService())//判断是否又下一个元素
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;//下一个实现类的全限定名
            Class<?> c = null;
            //使用反射获取实现类的Class对象
            c = Class.forName(cn, false, loader);  
            //创建一个对象
            S p = service.cast(c.newInstance());
            //放到缓存中
            providers.put(cn, p);
            返回
            return p;      
 }
 private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                  //获取文件名
                    String fullName = PREFIX + service.getName();
                    //加载文集URL
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                //解析文件
                pending = parse(service, configs.nextElement());
            }
            //赋值下一个实现类的全限定名
            nextName = pending.next();
            return true;
}

流程:

  1. 根据接口全限定名,结合META-INF/services/ ,拼接文件位置。这个是定死
  2. 使用ClassLoader 加载文件资源
  3. 解析出 对应的文件中配置了接口的哪些实现类
  4. 使用反射 根据解析出的类全限定名,实例化
  5. 放到缓存中。
  6. 返回

再次验证了:

  • SPI 本质 就是动态加载类机制
  • ServiceLoader 就是一个动态加载类的工具类
  • 底层还是使用了我们常见的Class ,ClassLoader


3.熟悉又陌生的应用场景存在问题

SPI 其实对于我们来说一定不陌生。 以前我们需要手写Class.forName("com.mysql.jdbc.Driver")加载驱动。

现在不用写了,其实就是使用了SPI技术。


4.存在问题

  • 当我们想找某个类时,需要遍历,没有做到真正的按需加载,
  • 多线程下不安全


似曾相似(Spring SPI)


SpringFactoriesLoader

其实当首次看到SPI的时候,突然看着很熟悉的感觉,好像在spring见过。

思索一番,最最经典不就是SpringFactoriesLoader

SpringFactoriesLoader

配置文件的文件夹目录
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
//加载META-INF/spring.factories 中的所有配置。
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
    String factoryClassName = factoryClass.getName();
    return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
  }
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    MultiValueMap<String, String> result = cache.get(classLoader);
    if (result != null) {
      return result;
    }
    try {
      //加载文件资源URL
      Enumeration<URL> urls = (classLoader != null ?
          classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
          ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
      result = new LinkedMultiValueMap<>();
      while (urls.hasMoreElements()) {
        URL url = urls.nextElement();
        UrlResource resource = new UrlResource(url);
        Properties properties = PropertiesLoaderUtils.loadProperties(resource);
        for (Map.Entry<?, ?> entry : properties.entrySet()) {
          List<String> factoryClassNames = Arrays.asList(
              StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));
          result.addAll((String) entry.getKey(), factoryClassNames);
        }
      }
      //放入缓存
      cache.put(classLoader, result);
      return result;
    }
    catch (IOException ex) {
      throw new IllegalArgumentException("Unable to load factories from location [" +
          FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
  }

spring.factories

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

与JAVA SPI 不同的是,

  • Spring spi 获取的是 固定META-INF/spring.factories文件中的配置。JAVA SPI是以某个接口的全限定路径为名的文件,需要开发人员自己定义
  • Spring Spi 是以K-V的形式配置,
  • Spring  SPI 首次加载配置文件时,会把所有spring.factories配置文件中配置解析出来放到缓存中,以后获取时直接从缓存中按Key值,取Value

由此看出: Spring spi 比 JAVA spi 设计的更好。其本质也是 Class , ClassLoader的高级封装


Dubbo SPI

看了JAVA SPI ,想了想Sprng SPI , 我似乎知道了 Dubbo SPI 是什么样子了。

ExtensionLoader

  • Dubbo使用ExtensionLoader 做为动态加载配的工具。
  • Dubbo的配置文件 放到"META-INF/dubbo/"目录下,并以具体扩展接口全名命名,类似 Java spi
  • Dubbo SPI 也是采用了K-V形式的配置,类似spring spi
  • ExtensionLoader 提供了更多的方法,提供丰富的获取功能
  • Dubbo SPI 还增加了 IOC 和 AOP 等特性
  • 其本质也是 Class , ClassLoader的高级封装。

看出Dubbo 跟JAVA SPI  ,Spring  SPI 都哦相似之处,也许Dubbo设计之初就是参考了 JAVA SPI  ,Spring  SPI

本文并不讲Dubbo SPI 的更多内容,只想讲讲我对 SPI的理解,为以后读Dubbo源码打个前站。


总结


万变不离其宗,不管是 JAVA SPI ,Spring SPI ,Dubbo SPI 。其本质都是对反射的高级封装,Class, ClassLoader 才是核心。


相关文章
|
3月前
|
存储 负载均衡 Dubbo
深入理解Dubbo-4.Dubbo扩展SPI
深入理解Dubbo-4.Dubbo扩展SPI
76 1
|
4月前
|
缓存 Dubbo Java
趁同事上厕所的时间,看完了 Dubbo SPI 的源码,瞬间觉得 JDK SPI 不香了
趁同事上厕所的时间,看完了 Dubbo SPI 的源码,瞬间觉得 JDK SPI 不香了
|
7月前
|
监控 Dubbo Java
由浅入深Dubbo核心源码剖析SPI机制 2
由浅入深Dubbo核心源码剖析SPI机制
31 0
|
7月前
|
缓存 Dubbo Java
由浅入深Dubbo核心源码剖析SPI机制 1
由浅入深Dubbo核心源码剖析SPI机制
53 0
|
3月前
|
缓存 Dubbo Java
Dubbo 第三节_ Dubbo的可扩展机制SPI源码解析
Dubbo会对DubboProtocol对象进⾏依赖注⼊(也就是⾃动给属性赋值,属性的类型为⼀个接⼝,记为A接⼝),这个时候,对于Dubbo来说它并不知道该给这个属性赋什么值,换句话说,Dubbo并不知道在进⾏依赖注⼊时该找⼀个什么的的扩展点对象给这个属性,这时就会预先赋值⼀个A接⼝的⾃适应扩展点实例,也就是A接⼝的⼀个代理对象。在调⽤getExtension去获取⼀个扩展点实例后,会对实例进⾏缓存,下次再获取同样名字的扩展点实例时就会从缓存中拿了。Protocol是⼀个接。但是,不是只要在⽅法上加了。
|
2月前
|
XML 缓存 Dubbo
Dubbo的魔法之门:深入解析SPI扩展机制【八】
Dubbo的魔法之门:深入解析SPI扩展机制【八】
30 0
|
5月前
|
Dubbo Java 应用服务中间件
阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别
大家好,我是三友~~ 今天来跟大家聊一聊Java、Spring、Dubbo三者SPI机制的原理和区别。 其实我之前写过一篇类似的文章,但是这篇文章主要是剖析dubbo的SPI机制的源码,中间只是简单地介绍了一下Java、Spring的SPI机制,并没有进行深入,所以本篇就来深入聊一聊这三者的原理和区别。
|
5月前
|
缓存 Dubbo Java
Dubbo2.7的Dubbo SPI实现原理细节
Dubbo2.7的Dubbo SPI实现原理细节
32 0
|
6月前
|
Dubbo Java 应用服务中间件
JDK SPI、Spring SPI、Dubbo SPI三种机制的细节与演化
Java SPI(Service Provider Interface)是JDK提供的一种服务发现机制,用于在运行时动态加载和扩展应用程序中的服务提供者。
169 0
|
6月前
|
存储 Dubbo Java
Dubbo第三讲:Dubbo的可扩展机制SPI源码解析
Dubbo第三讲:Dubbo的可扩展机制SPI源码解析