一、前言

​ 在之前的dubbo源码分析我们以及了解了dubbo相关的架构、用法和原理,但是提到dubbo我们就不得不提其中spi机制,dubbo源码中使用了大量的spi机制,其所有的核心组件都做成了基于spi的实现方式,比如Protocol层=>DubboProtocol,Cluster层=>FailoverCluster等,spi这种机制可以实现这个组件的插拔式使用(一个接口多种实现,可以随时调整实现方式)同时支持我们进行定制化扩展。

下图为dubbo使用spi形式引入的内部组件

下面就让我们一起来探究一下dubbo的spi相关机制。

二、java SPI

​ 在介dubbo的spi之前我们需要先了解一下什么是spi以及jdk默认支持的spi机制。

2.1、什么是SPI

​ SPI的全名为Service Provider Interface,模块之间相互调用基于接口编程,需要为某个接口寻找服务实现将装配的控制权移到程序之外的机制。

要使用Java SPI,需要遵循如下约定:

  • 1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
  • 2、接口实现类所在的jar包放在主程序的classpath中;
  • 3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
  • 4、SPI的实现类必须携带一个不带参数的构造方法;

像我们熟悉的java的jdbc,jdk定义了一套接口规范,不同的数据库厂商提供不同的实现。

mysql数据厂商实现:

2.2、java SPI 示范例

项目结构

spi-api: 提供接口规范

public interface SpiService {

    void say();
}

**spi-impl1、spi-impl2: 提供不同实现 **

public class SpiServiceImplOne implements SpiService {
    public void say() {
        System.out.println("i am SpiServiceImplOne");
    }
}

其他实现类类似

spi配置 不同实现都有相似配置


@Test
public void testSpi(){
        ServiceLoader<SpiService> serviceLoader = ServiceLoader.load(SpiService.class);
        for (SpiService o : serviceLoader) {
            o.say();
    }
}

测试结果

介绍java提供的spi机制、实例、源码分析,优缺点 引申出dubbo的spi机制

2.3、java spi源码解析

public static <S> ServiceLoader<S> load(Class<S> service) {
    //获取classLoader
    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;
    //安全管理
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    //重新家在
    reload();
}
public void reload() {
    //清空缓存中的接口对应的实例对象
    providers.clear();
    //创建LazyIterator 该对象是用来循环遍历实例化接口实现类的
    lookupIterator = new LazyIterator(service, loader);
}

从测试类ServiceLoader的load()开始溯源,最终其使用两个参数对象 服务接口的class和类加载器创建LazyIterator对象,该对象是实际进行服务实例化的,其实现了Iterator迭代器hasNext()=>hasNextService()next()=>nextService().

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            //获取spi配置文件路径 classPath /META-INF/service/${接口全限定类名}
            String fullName = PREFIX + service.getName();
            //根据配置文件路径记载配置文件到configs
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    //读取并解析配置文件获取其中的配置信息(配置信息可以是多个实现类是一个列表)则pending为接口实现名的列表迭代对象
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        pending = parse(service, configs.nextElement());
    }
    //从迭代器获取一个接口实现 使用nextName接收
    nextName = pending.next();
    return true;
}

hasNextSerive实现判断是否还有需要实例化的接口实现,其中有三个熟悉关注

  1. nextName: 需要进行实例化的接口实现类全限定类名 从pending迭代器中获取到
  2. pending:从配置文件中解析出来的所有实现接口全限定类名列表集合(迭代器形式)
  3. configs: 根据classPath /META-INF/service/${接口全限定类名}夹在的URL对象
//获取服务实例对象
private S nextService() {
    //省略相关校验
    try {
        //使用实现类的空构造器创建服务接口对象
        S p = service.cast(c.newInstance());
        //放入缓存中
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
                "Provider " + cn + " could not be instantiated",
                x);
    }
    throw new Error();          // This cannot happen

}

该方法简单主要是使用接口实现类的空构造器(所以java SPI接口实现类需要遵循提供一个空构造器约束)

2.4、java SPI的优劣

​ 在使用和源码分析后,我们能大概总结出其优缺点

优点:

使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件,从而很好的支持可插拔和变更扩展。

缺点:

ServiceLoader只能完全遍历并全部进行实例化,不能很好的按需加载(遇到接口实现类实例化特别耗费资源情况)会有性能损失和不灵活

多个并发多线程使用ServiceLoader类的实例是不安全的。

所以dubbo并没有完全采用javaSPI(只是做了兼容),而是自己实现类一套SPI机制,下面我们来看看Dubbo的spi机制。

三、dubbo SPI

​ dubbo在原有的spi基础上主要有以下的改变,①配置文件采用键值对配置的方式,使用起来更加灵活和简单通过@SPI实现按需加载 增强了原本SPI的功能,使得SPI具备ioc和aop的功能,这在原本的java中spi是不支持的。dubbo的spi是通过ExtensionLoader来解析的,通过ExtensionLoader来加载指定的实现类,

配置文件的路径在META-INF/dubbo路径下

我们先来看一下 Dubbo 对配置文件目录的约定,不同于 Java SPI ,Dubbo 分为了三类目录。

  1. META-INF/services/ 目录:该目录下的 SPI 配置文件是为了用来兼容 Java SPI 。

  2. META-INF/dubbo/ 目录:该目录存放用户自定义的 SPI 配置文件。

  3. META-INF/dubbo/internal/ 目录:该目录存放 Dubbo 内部使用的 SPI 配置文件。

3.1、示例

​ Dubbo的SPI例子的和Java的项目架构相似,接口以及实现类似只是需要在接口中使用dubbo的SPI相关注解

  1. 接口
@SPI("two")
public interface SpiDemo {
    //动态自适应扩展注解
    @Adaptive
    void getSpi(URL url);
}
  1. 配置

  2. 测试

@Test
public void testSpi(){
    ExtensionLoader<SpiDemo> extensionLoader = ExtensionLoader.getExtensionLoader(SpiDemo.class);
    URL url = URL.valueOf("test://123.5.5.5/test");
    
    //普通扩展
    //从全部的实现类中根据spi名字获取一个
    SpiDemo extension = extensionLoader.getExtension("two");
    extension.getSpi(url); //输出SpiDemoImpl two
  
    //自适应扩展
    //默认获取 @SPI中的value属性作为参数
    SpiDemo adaptiveExtension = extensionLoader.getAdaptiveExtension();
    adaptiveExtension.getSpi(url);  //输出SpiDemoImpl two

    //@SPI中参数为two,url中设置的参数为one 则使用one对应的SpiDemoImplOne
    url = URL.valueOf("test://123.5.5.5/test?spi.demo=one");
    adaptiveExtension = extensionLoader.getAdaptiveExtension();
    adaptiveExtension.getSpi(url); //输出SpiDemoImpl one
  
   //@Activate 会获取满足条件注解中条件的一组接口实现  一般在Dubbo的过滤器中使用较多
   List<SpiDemo> myWorks = extensionLoader.getActivateExtension(url, "", "myWork");

   for(SpiDemo spiDemo:myWorks){
      spiDemo.getSpi(url);
   }

}
@Adaptive注解根据Dubbo 的URL相关参数动态的选择具体的实现 通过该getAdaptiveExtension获取
该项目默认参数设置 spi.demo(接口名SpiDemo的驼峰逆转为spi.demo)
如果没有设置则默认参数为@Spi注解的value值即 spi.demo=two  即使用SpiDemoImplOne实现类
如果设置protocol://host:port/name?spi.demo=one 即使用SpiDemoImplTwo实现类
Activate注解表示一个扩展是否被激活(使用),可以放在类定义和方法(本文不讲)上,dubbo将它标注在spi的扩展类上,表示这个扩展实现激活条件和时机。它有两个设置过滤条件的字段,group,value 都是字符数组。 用来指定这个扩展类在什么条件下激活

有关DUBBO相关SPI使用可参考:https://www.jianshu/p/dc616814ce98

3.2、dubbo原理解析

dubbo扩展的核心代码如下:

1.ExtensionLoader.getExtensionLoader(xxx.class).getExtension(name);

2.ExtensionLoader.getExtensionLoader(xxx.class).getAdaptiveExtension();

3.ExtensionLoader.getExtensionLoader(xxx.class).getActivateExtension(url, key);

**getExtensionLoader方法 **

Dubbo SPI为每一个SPI接口都创建一个ExtensionLoader并放入对应的缓存中,每次获取都先从缓存中获取对应的Loader 没有则创建一个新的并放入缓存中,其中获取扩展有三种类型

  1. 一种是普通类型的扩展实现类获取,直接通过class实例化

  2. 一种是自适应的扩展实现类获取,主要是通过DubboURL中的参数动态获取实现类,对应的类或者方法会使用@Adaptive注解修饰,使用该注解修饰的类dubbo编译过程中会生成XXXx$adaptive代理类。

  3. 一种是选择符合dub boUrl的某种条件的一组接口实现类,这些类使用@Adative注解修饰,主要在dubbo的过滤器中使用较多。

3.2.1、getExtension方法

和getExtensionLoader类似 先从缓存中获取@SPI注解value对应的接口实现,没有则调用createExtension()创建

createExtension方法

private T createExtension(String name) {
    //从我们上面说的三个目录中加载接口实现类的Class 并按照名字获取
    //获取class属性比较复杂此处额外讲解
    Class<?> clazz = getExtensionClasses().get(name);
    //找不到抛出异常
    if (clazz == null) {
        throw findException(name);
    }
    try {
        //从缓存中获取实例对象 没有反射创建并放入缓存中
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        //使用IOC的形式为该是实例通过setXXX()形式进行依赖注入
        injectExtension(instance);
        //如果有包装类(以Wrapper结尾)获取进行对该实力进行包装(一些通用的逻辑可以放在包装类中 因为包装类都会持有
        // 原始对象执行完后会继续调用原始对象)
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
            for (Class<?> wrapperClass : wrapperClasses) {
                //对包装类进行实例化和依赖注入
                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        return instance;
    } catch (Throwable t) {
        //异常处理
    }
}

该方法中包含了获取接口实例的主要流程。包括class实例获取缓存获取,反射实例化,依赖注入,包装类包装整体流程会在源码分析完成后整体属性。下面我们来关注一下如何获取到符合条件的class的getExtensionClasses,这个是我们进行接口实例化的基础。

instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));

WrapperClass - AOP

包装类是因为一个扩展接口可能有多个扩展实现类,而这些扩展实现类会有一个相同的或者公共的逻辑,如果每个实现类都写一遍代码就重复了,此处dubbo设置出来了包装类统一的通用逻辑在此处实现类似于spring的AOP思想。

**injectExtension -IOC **

该方法主要用于将实例化的接口实现类的相关依赖给注入进来,类似于spring的IOC。

private T injectExtension(T instance) {
    try {
        if (objectFactory != null) {
            for (Method method : instance.getClass().getMethods()) {
                //针对实现类的使用公有的setXXX(xxx)方法注入相关依赖
                if (method.getName().startsWith("set")
                        && method.getParameterTypes().length == 1
                        && Modifier.isPublic(method.getModifiers())) {
                    Class<?> pt = method.getParameterTypes()[0];
                    try {
                        //通过setXXX 获取对应的属性名xxx
                        String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
                        //调用Objecttfactory去获取属性值(objectFactory获取也使用了SPI机制)
                        Object object = objectFactory.getExtension(pt, property);
                        if (object != null) {
                           //反射调用填充数据
                            method.invoke(instance, object);
                        }
                    } catch (Exception e) {
                      //省略错误异常处理
                       }
                }
            }
        }
    } catch (Exception e) {
      //省略错误异常处理
    }
    return instance;
}

injectExtension 方法将属性通过set方法注入,获取属性值的方式是使用Objectfactory.getExtension() 如果和spring整合则会使用实现类SpringExtensionFactory对象从spring容器中获取对象

public <T> T getExtension(Class<T> type, String name) {
    for (ApplicationContext context : contexts) {
        if (context.containsBean(name)) {
            Object bean = context.getBean(name);
            if (type.isInstance(bean)) {
                return (T) bean;
            }
        }
    }
    return null;
}

getExtensionClasses方法

套路一样缓存中获取没有则调用loadExtensionClasses()方法加载,该方法加载会调用loadDirectory()方法从我们上面说的三个目录分别加载对应目录下的配置信息并通过loadResource()方法进行全部配置文件加载最终通过loadClass()方法加载具体的class

loadClass()

/**
 * 加载所有使用@SPI注解修饰在配置文件中声明的接口实现class
 * @param extensionClasses 解析出来的扩展类缓存集合
 * @param resourceURL 对应某个spi配置文件(在该方法中没作用只是用来日志打印)
 * @param clazz 扩展类实现
 * @param name 扩展类的名字 key(name)=value(clazz的名字)
 */
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) {
    if (!type.isAssignableFrom(clazz)) {
        throw new IllegalStateException("Error when load extension class(interface: " +
                type + ", class line: " + clazz.getName() + "), class "
                + clazz.getName() + "is not subtype of interface.");
    }
    //如果该类被Adaptive注解修饰,则将该类存放cachedAdaptiveClass中
    // 这种机制应该是@Adaptive注解只能修饰一个接口类型实现类
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        if (cachedAdaptiveClass == null) {
            cachedAdaptiveClass = clazz;
        } else if (!cachedAdaptiveClass.equals(clazz)) {
            throw new IllegalStateException("More than 1 adaptive class found: "
                    + cachedAdaptiveClass.getClass().getName()
                    + ", " + clazz.getClass().getName());
        }
        //如果是包装类则wrappers中加入
    } else if (isWrapperClass(clazz)) {
        Set<Class<?>> wrappers = cachedWrapperClasses;
        if (wrappers == null) {
            cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
            wrappers = cachedWrapperClasses;
        }
        wrappers.add(clazz);
    } else {
        //普通类
        clazz.getConstructor();
        //获取名字,没有生成(怎么可能没有名字,不都是键值对吗?)
        if (name == null || name.length() == 0) {
            name = findAnnotationName(clazz);
            if (name == null || name.length() == 0) {
                if (clazz.getSimpleName().length() > type.getSimpleName().length()
                        && clazz.getSimpleName().endsWith(type.getSimpleName())) {
                    name = clazz.getSimpleName().substring(0, clazz.getSimpleName().length() - type.getSimpleName().length()).toLowerCase();
                } else {
                    throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
                }
            }
        }
        String[] names = NAME_SEPARATOR.split(name);
        if (names != null && names.length > 0) {
            //如果类上使用@Activate注解修饰则将该类页也放入cachedActivates缓存中
            //之后将name 和class作为key-value存放到extensionClasses集合中
            Activate activate = clazz.getAnnotation(Activate.class);
            if (activate != null) {
                cachedActivates.put(names[0], activate);
            }
            for (String n : names) {
                if (!cachedNames.containsKey(clazz)) {
                    cachedNames.put(clazz, n);
                }
                Class<?> c = extensionClasses.get(n);
                if (c == null) {
                    extensionClasses.put(n, clazz);
                } else if (c != clazz) {
                    //省略异常
                }
            }
        }
    }
}

private boolean isWrapperClass(Class<?> clazz) {
    try {
        //type 是对应的接口类对象class clazz的属性中包含接口对象则说明该类是一个包装类
        //在dubbo中所有的Wrapper类都会持有一个接口类对象
        clazz.getConstructor(type);
        return true;
    } catch (NoSuchMethodException e) {
        return false;
    }
}

该方法中主要针对class的不同类型将其放入不同的缓存对象中,有四种缓存class类型。

  1. 对于使用@Adaptive注解修饰的类放入cachedAdaptiveClass对象中,每一个接口类只能有一个实现类使用@Adaptive注解修饰,使用该注解修饰getAdaptiveExtensions则不会根据dubbo URL中参数选择实现类,而是使用该注解修饰的类实现。
  2. 对于包装类将其放入cachedWrapperClasses列表中
  3. 普通类存放到extensionClasses中,如果该类也被@Activate注解修饰也会将其放入cachedActivates map集合中

3.2.2、getAdaptiveExtension()方法

​ 与getExtension()方法类似先从其对应的缓存中获取,没有调用createAdaptiveExtension()方法

return injectExtension((T) getAdaptiveExtensionClass().newInstance());


private Class<?> getAdaptiveExtensionClass() {
      getExtensionClasses();
     if (cachedAdaptiveClass != null) {
          return cachedAdaptiveClass;
      }
     return cachedAdaptiveClass = createAdaptiveExtensionClass();
 }

getAdaptiveExtensionClass() 获取对应的class类,获取的类有两种方式一种是之前使用@Adaptive注解修饰在类上 通过getExtensionClasses()方法存放到cachedAdaptiveClass中的class,另一种是使用使用@Adaptive注解修饰在方法上,这里Dubbo框架会自动生成XXXX#Adaptive代理类 如下:

public class SpiDemo$Adaptive implements com.xiu.dubbo.service.SpiDemo {
    public void getSpi(com.alibaba.dubbo.common.URL arg0) {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        com.alibaba.dubbo.common.URL url = arg0;
        //根据dubboURL中的参数test://123.5.5.5/test?spi.demo=two  生成一个名字 根据参数类型动态获取自适应的扩展实现
        String extName = url.getParameter("spi.demo", "two");
        if(extName == null) {
            throw
                    new IllegalStateException("Fail to get extension(com.xiu.dubbo.service.SpiDemo)" +
                            " name from url(" + url.toString() + ") use keys([spi.demo])");
        }
        com.xiu.dubbo.service.SpiDemo extension = (com.xiu.dubbo.service.SpiDemo)ExtensionLoader.getExtensionLoader(
                com.xiu.dubbo.service.SpiDemo.class).getExtension(extName);
        extension.getSpi(arg0);
    }
}

这里就体现出了dubbo提供的自适应扩展,它获取实现类可以通过Dubbo 的URL中的参数动态选择实现类,从而更加灵活。后面的流程也和getExtension类似,根据class空构造器创建实例对象,injectExtension()spring形式注入相关依赖对象。

3.2.2、getActivateExtension()方法

​ 获取使用@Activate注解修饰的符合条件的一组接口实现。

/**
     * 根据dubboURl的参数和分组信息 筛选使用@Activate注解修饰的一组接口实现
     * @param url dubboURL
     * @param values 参数对应的值列表 
     * @param group  分组信息
     * @return 符合条件的一组接口实现列表
     */
public List<T> getActivateExtension(URL url, String[] values, String group) {
    List<T> exts = new ArrayList<T>();
    //将DUBBO的URL中查询条件的值作为names列表用于查找符合的实现
    List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);
    //先根据分组获取符合条件的(且不在nams中的防止重复获取)
    if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
        //所有使用注解@Activate修饰的类会缓存到cachedActivates中
        getExtensionClasses();
        for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) {
            String name = entry.getKey();
            Activate activate = entry.getValue();
            //符合分组且不在names中
            if (isMatchGroup(group, activate.group())) {
                T ext = getExtension(name);
                if (!names.contains(name)
                        && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
                        && isActive(activate, url)) {
                    exts.add(ext);
                }
            }
        }
        //排序
        Collections.sort(exts, ActivateComparator.COMPARATOR);
    }
    List<T> usrs = new ArrayList<T>();
    //查找符合names属性的实现
    for (int i = 0; i < names.size(); i++) {
        String name = names.get(i);
        if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX)
                && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) {
            if (Constants.DEFAULT_KEY.equals(name)) {
                if (!usrs.isEmpty()) {
                    exts.addAll(0, usrs);
                    usrs.clear();
                }
            } else {
                T ext = getExtension(name);
                usrs.add(ext);
            }
        }
    }
    //汇总到一起返回
    if (!usrs.isEmpty()) {
        exts.addAll(usrs);
    }
    return exts;
}

3.3、DUBBO spi执行流程

![请添加图片描述](https://img-blog.csdnimg/ee8a33f3bd13433cafa301eebf60c6d3.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBAbGl1c2hhbmd6YWliZWlqaW5n,size_20,color_FFFFFF,t_70,g_se,x_16)

更多推荐

dubbo源码分析之八-dubbo的spi机制