Dubbo的SPI机制

278 阅读8分钟

参考资料:敖丙_dubbo的spi

什么是SPI

SPI:Service Provider Interface,服务提供者接口。从字面上,很难理解这玩意有啥用。

举个栗子,市面上有五花八门的数据库,每种数据库底层协议各不相同,我们总不能针对每个数据库都开发出不同的api访问吧。所以,需要有一个接口,定义一些访问数据库的规范协议,所有数据库都按照这个接口开发,这个接口就是java.sql.Driver。既然数据库厂商都根据接口开发对应的实现类,但是存在一个问题,真正使用时,到底用哪个实现类,从哪里找到这个实现类?

答案是,大家约定好把实现类的配置写在某个地方,要使用时去约定位置查找。

Java SPI

Java SPI就是约定在classpath的META-INF/services/目录下创建一个以服务接口命名的文件,文件内容是具体实现类的全限定名

以下是MySQL的做法

image-20201205220733130

image-20201205220818061

Java SPI示例

我们定义一个接口,接着创建两个实现类,并在类路径的META-INF/services目录下创建服务接口全限定名的文件。

image-20201205221958182

public interface IHelloSPI {
    void say();
}


@Slf4j
public class HelloSPIImpl1 implements IHelloSPI {
    @Override
    public void say() {
        log.info(" --- >> 我是实现类1");
    }
}


@Slf4j
public class HelloSPIImpl2 implements IHelloSPI {
    @Override
    public void say() {
        log.info(" --- >> 我是实现类2");
    }
}
com.drh.springboot_hello.spi.java.HelloSPIImpl1
com.drh.springboot_hello.spi.java.HelloSPIImpl2
public class JavaSPITest {
    public static void main(String[] args) {
        // load方法会创建一个ServiceLoader和LazyIterator对象
        ServiceLoader<IHelloSPI> serviceLoader = ServiceLoader.load(IHelloSPI.class);
        // 获取到LazyIterator对象
        Iterator<IHelloSPI> iterator = serviceLoader.iterator();
        // hasNext方法会解析接口文件内容,遍历每行,获取到实现类的全限定名
        while (iterator.hasNext()) {
            // next方法,根据全限定名,创建class对象,然后反射创建实例对象
            iterator.next().say();
        }
    }
}

输出结果

image-20201205222108588

Java SPI源码分析

在上面的代码可以看到,ServiceLoader#load是Java SPI的入口,我们跟踪代码看下做了什么操作。

image-20201205222642845

从上面的调用过程可以看到,先找到当前线程绑定的类加载器,如果没有就使用系统加载器。然后清除一下缓存,接着创建一个ServiceLoader和LazyIterator。

查看LazyIterator发现,它是Iterator的子类。因此,可以推断serviceLoader#iterator获取到的实际上是LazyIterator实例对象。

我们继续来看,LazyIterator#hasNext方法

image-20201205223432542

private static final String PREFIX = "META-INF/services/";

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            // 找到文件位置
            String fullName = PREFIX + service.getName();
            // 加载文件
            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,等会nextService方法会用到这个属性
    nextName = pending.next();
    return true;
}

可以看到,LazyIterator#hasNextService方法先根据约定路径找到接口文件,接着按照行遍历解析文件内容

接着,我们看下LazyIterator#next方法

image-20201205224021460

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    // 获取hasNextService解析文件内容得到行数据
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        // 实现类的class对象
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service,
             "Provider " + cn  + " not a subtype");
    }
    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
}

LazyIterator#nextService根据全限定名获取到实现类class对象,通过反射创建实例对象,并放入缓存,最后返回结果。

总结: Java SPI依靠ServiceLoader实现,调用load方法时,会创建一个ServiceLoader对象,以及其内部属性LazyIterator对象。接着,ServiceLoader获取迭代器,即LazyIterator。

调用迭代器#hasNext方法,到META-INF/services目录查找以服务接口全限定名来命名的文件,按行遍历,获取到实现类的全限定名。最后,调用next方法,根据类全限定名调用Class.forName和newInstance等方法,创建实例对象并存储到缓存。

image-20201205225024831

Duboo SPI

由于Java SPI会把所有实现类都实例化对象,如果系统中不需要用到这个对象,会造成资源浪费。为了实现按需加载,dubbo自己实现了一个SPI,通过指定的名称来加载对应的具体实现类。

我们先来看下dubbo对配置文件目录的约定,分为三类目录:

  • META-INF/services/:该目录用来兼容Java SPI
  • META-INF/dubbo/:该目录存放用户自定义的配置文件
  • META-INF/dubbo/internal/:该目录存放dubbo内部使用的SPI配置文件

Dubbo SPI示例

指定一个服务接口,并使用@SPI注解表示使用dubbo的SPI。接着在META-INF/dubbo/目录下创建以服务接口命名的文件,内容以key-value形式表示参数与实现类的关系。

image-20201206164326699

@SPI
public interface IHelloDubboSPI {
    void say();
}

@Slf4j
public class HelloDubboSPIImpl implements IHelloDubboSPI {
    @Override
    public void say() {
        log.info(" --- >> dubbo spi 示例。。。");
    }
}

image-20201206164405689

impl1=spi.dubbo.HelloDubboSPIImpl
@SpringBootTest
public class DubboSPITest {
    @Test
    public void testDubboSPI() {
        ExtensionLoader<IHelloDubboSPI> extensionLoader = ExtensionLoader.getExtensionLoader(IHelloDubboSPI.class);
        IHelloDubboSPI impl1 = extensionLoader.getExtension("impl1");
        impl1.say();
    }
}

image-20201206164435682

Dubbo源码分析

从上面的测试代码看到,大致流程是先通过接口类对象找到对应的ExtensionLoader,然后再通过getExtension方法得到指定名称的具体实现类对象。

先看下ExtensionLoader#getExtensionLoader方法

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    // 忽略一堆参数验证
    
    ExtensionLoader<T> loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
    if (loader == null) {
        // 注意:new ExtensionLoader(type) -> 这个方法里面用到了自适应扩展
        EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type));
        loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
    }

    return loader;
}


private ExtensionLoader(Class<?> type) {
    this.type = type;
    this.objectFactory = type == ExtensionFactory.class ? 
        null : (ExtensionFactory)getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension();
}

getExtensionLoader方法很简单,先从缓存里面找是否已经存在指定类型的ExtensionLoader,如果没有就创建一个并放到缓存,key是class对象,最后返回ExtensionLoader对象。

接着,我们看下获取具体实现类对象,ExtensionLoader#getExtension

public T getExtension(String name) {
    // 省略部分代码
    
    // 从缓存中获取指定实现类对象
    Holder<Object> holder = (Holder)this.cachedInstances.get(name);
    if (holder == null) {
        this.cachedInstances.putIfAbsent(name, new Holder());
        holder = (Holder)this.cachedInstances.get(name);
    }

    Object instance = holder.get();
    if (instance == null) { // 双重检查
        synchronized(holder) {
            instance = holder.get();
            if (instance == null) {
                // 关键!!创建实例对象
                instance = this.createExtension(name);
                holder.set(instance);
            }
        }
    }
    return instance;
}

从代码中看到,重点是this.createExtension(name),继续往下看

private T createExtension(String name) {
    // 获取实现类的class对象
    Class<?> clazz = (Class)this.getExtensionClasses().get(name);
    if (clazz == null) {
        throw this.findException(name);
    } else {
        try {
            // 判断缓存是否已经有实例对象
            T instance = EXTENSION_INSTANCES.get(clazz);            
            if (instance == null) {
                // 没有的话,调用newInstance方法创建并放入缓存
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
                instance = EXTENSION_INSTANCES.get(clazz);
            }
			// setter 依赖注入
            this.injectExtension(instance);
            
            Set<Class<?>> wrapperClasses = this.cachedWrapperClasses;
            Class wrapperClass;
            // 如果有包装类,就包装下
            if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
                for(Iterator var5 = wrapperClasses.iterator(); var5.hasNext(); 
                    	instance = this.injectExtension(wrapperClass.getConstructor(this.type).newInstance(instance))) {
                    wrapperClass = (Class)var5.next();
                }
            }

            return instance;
        } catch (Throwable var7) {
            throw new IllegalStateException("xxx");
        }
    }
}

方法逻辑很清晰,就是先找到实现类,然后看下缓存有没有实例对象,如果没有,就通过反射创建,然后执行set方法依赖注入。

image-20201206170806019

到这里,我们继续看下怎么找到实现类。

查找实现类

我们继续看下ExtensionLoader#getExtensionClasses

private Map<String, Class<?>> getExtensionClasses() {
    Map<String, Class<?>> classes = (Map)this.cachedClasses.get();
    if (classes == null) {
        synchronized(this.cachedClasses) {
            classes = (Map)this.cachedClasses.get();
            if (classes == null) {
                // 关键!!先从缓存中查找,没有则调用loadExtensionClasses方法
                classes = this.loadExtensionClasses();
                this.cachedClasses.set(classes);
            }
        }
    }

    return classes;
}

private Map<String, Class<?>> loadExtensionClasses() {
    // dubbo的SPI注解,读取它的值
    SPI defaultAnnotation = (SPI)this.type.getAnnotation(SPI.class);
    if (defaultAnnotation != null) {
        String value = defaultAnnotation.value();
        // 检验SPI接口定义的名称
        if ((value = value.trim()).length() > 0) {
            String[] names = NAME_SEPARATOR.split(value);
            if (names.length > 1) {
                throw new IllegalStateException(
                    "more than 1 default extension name on extension " + this.type.getName() + ": " + 			
                    Arrays.toString(names));
            }

            if (names.length == 1) {
                // SPI的value作为服务接口的默认实现类
                this.cachedDefaultName = names[0];
            }
        }
    }
	// 查找约定的三个目录
    Map<String, Class<?>> extensionClasses = new HashMap();
    this.loadDirectory(extensionClasses, "META-INF/dubbo/internal/");
    this.loadDirectory(extensionClasses, "META-INF/dubbo/");
    this.loadDirectory(extensionClasses, "META-INF/services/");
    return extensionClasses;
}

loadDirectory会扫描约定的三个目录,找到服务接口文件,解析读取文件内容,把这些实现类对象都加载进来,通过loadClass做缓存处理。

在执行loadClass方法前,已经加载实现类,Class.forName(xxx),loadClass方法只是根据实现类上面的注解做不同的缓存操作,分别有Adaptive、WrapperClass和普通类三种。

loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);


private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) {
    // 省略部分代码
    
    clazz.getConstructor();
    String[] names = NAME_SEPARATOR.split(name);
    if (names != null && names.length > 0) {
        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);
            }
        }
    }
    
}

总结

Dubbo SPI的分析就到此结束吧,至于Adaptive、WrapperClass的分析可以看敖丙的文章,暂时消化不了。最后,我们来个总结吧

要得到指定名称的实例对象,需要先创建接口对应的ExtensionLoader,接着调用getExtension方法,根据方法参数中的名字去找到指定的子类对象。过程是,先到约定的META-INF/serviceMETA-INF/dubboMETA-INF/dubbo/internal目录下,找到以服务接口命名的文件,解析文件内容,把所有的实现类都先加载进来(类加载),并使用HashMap存储name与实现类class对象的对应关系,key->name,value->class对象。最后,根据getExtension方法参数,从map里面找到对应的class对象,通过反射创建实例对象并返回。

总结:

dubbo SPI的实现原理和Java SPI很类似,都是约定目录,以服务接口作为文件名。不同点是dubbo SPI会指定名称与实现类的关系。

注意:加载类和实例化对象是两回事。Java SPI是加载实现类并实例化对象,而Dubbo SPI是加载类,按需实例化。