Dubbo源代码实现四:Dubbo中的扩展点与SPI

    xiaoxiao2021-08-20  96

           SPI的全称是ServiceProviderInterface,即服务提供商接口。直白的说,它主要用来实现一个可扩展的Java应用。有人会觉得这就是建立在面向接口编程下的一种为了使组件可扩展或动态变更实现的规范,常见的类SPI的设计有JDBC、JNDI和JAXP等。例如JDBC的架构是由一套API组成,用于给Java应用提供访问不同数据库的能力,而数据库提供商的驱动软件各不相同,JDBC通过提供一套通用行为的API接口,底层可以由提供商自由实现,虽然JDBC的设计没有指明是SPI,但也和SPI的设计类似。这里有兴趣的读者可以参看2002年的一篇老文章:Replaceable Components andthe Service Provider Interface

          JDK为SPI的实现提供了工具类,即java.util.ServiceLoader,ServiceLoader中定义的SPI规范没有什么特别之处,只需要有一个提供者配置文件(provider-configuration file),该文件需要在resource目录META-INF/services下,文件名就是服务接口的全限定名。文件内容是提供者Class的全限定名列表,显然提供者Class都应该实现服务接口。文件必须使用UTF-8编码。

             动嘴不如动手,直接结合例子理解起来更简单,先新建我们服务接口类,该接口只有一个方法,那就是获取提供者名称:

     

    package com.manzhizhen.study.spi;

    /**

     * SPI服务接口  */

    public interface SpiService {

    /**

     * 获取提供商名称

     * @return 提供商名称

     */

         String getProviderName();

    }

     

    这里我们假设有两家服务提供商,StandardSpiService和MzzSpiService,他们当然都需要实现SpiService接口,代码如下:

     

    package com.manzhizhen.study.spi;

     

    public class StandardSpiService implements SpiService {

        @Override

        public String getProviderName() {

           return"Thisis StandardSpiService";

        }

    }

     

    package com.manzhizhen.study.spi;

     

    public class MzzSpiService implements SpiService {

        @Override

        public String getProviderName() {

           return"Thisis MzzSpiService";

        }

    }

     

    万事俱备,现在我们可以创建服务提供者文件了,在META-INF/services下新建名称为com.manzhizhen.study.spi.SpiService的服务提供者文件,因为目前只有两家提供商,所以该文件内容如下:

     

    com.manzhizhen.study.spi.StandardSpiService

    com.manzhizhen.study.spi.MzzSpiService

     

    现在,我们写一个main方法来乐呵(测试)一下:

     

    public static void main(String[] args) {

       ServiceLoader<SpiService> spiServiceLoader = ServiceLoader.load(SpiService.class);

        while(true) {

           for (SpiService spiService : spiServiceLoader){

               System.out.println(spiService.getProviderName());

            }

     

           // 过段时间修改com.manzhizhen.study.spi.SpiService文件看是否能做到动态增减SPI的实现

            try {

               Thread.sleep(1000);

     // 为了验证动态加载功能,这里每隔一秒都重新reload

               spiServiceLoader.reload();

           } catch(InterruptedException e) {

               e.printStackTrace();

            }

        }

    }

     

    运行该main方法后,我们可以看到控制台输出如下:

     

    This isStandardSpiService

    This isMzzSpiService

    This isStandardSpiService

    This isMzzSpiService

    ...

     

    然后我们删除com.manzhizhen.study.spi.SpiService文件的第二行(即MzzSpiService提供商),结果输出变了:

     

    This isStandardSpiService

    This isMzzSpiService

    This isStandardSpiService

    This isMzzSpiService

    This isStandardSpiService

    This isStandardSpiService

    This isStandardSpiService

    ...

     

     

    有兴趣的读者可以看ServiceLoader内部的实现,其实整个流程就包含如下几步:

    1.读取服务提供配置文件。

    2.newInstance实例化配置文件中列举的服务提供类,所以服务提供类需要有默认的构造方法。

    3.将实例化的服务提供类存储起来,即LinkedHashMap<String, S> providers = new LinkedHashMap<>()。

     

             可见,SPI并没有什么神奇的地方,只不过是一种通过面向接口编程来实现服务透明变更的规范而已,带来的好处自然是我们业务系统变得扩展性更强。

             接下来我们看看Dubbo中利用SPI做了哪些事情。Dubbo中SPI进行了扩展,对服务提供者配置文件中的内容进行了改造,由原来的提供者类的全限定名列表改成了KV形式的列表,这也导致了Dubbo中无法直接使用ServiceLoader,所以,与之对应的,在Dubbo中有ExtensionLoader,ExtensionLoader是扩展点载入器,用于载入Dubbo中的各种可配置组件,比如动态代理方式(ProxyFactory)、负载均衡策略(LoadBalance)、RCP协议(Protocol)、拦截器(Filter)、容器类型(Container)、集群方式(Cluster)和注册中心类型(RegistryFactory)等,总之,Dubbo为了应对各种场景,它的所有内部组件都是通过这种SPI的方式来管理的,这也是为什么Dubbo需要将服务提供者配置文件设计成KV键值对形式,这个K就是我们在Dubbo配置文件或注解中用到的K,Dubbo直接通过服务接口(上面提到的ProxyFactory、LoadBalance、Protocol、Filter等)和配置的K从ExtensionLoader拿到服务提供的实现类。与ServiceLoader的load方法对应的是ExtensionLoader的getExtensionLoader方法:

     

    public static <T>ExtensionLoader<T> getExtensionLoader(Class<T> type) {

        if (type == null)

           thrownew IllegalArgumentException("Extension type ==null");

        if(!type.isInterface()) {

           thrownew IllegalArgumentException("Extensiontype("+ type + ") is notinterface!");

        }

        if(!withExtensionAnnotation(type)) {

           thrownew IllegalArgumentException("Extensiontype("+ type +

                    ") is not extension, because WITHOUT@"+ SPI.class.getSimpleName()+"Annotation!");

        }

       

       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;

    }

     

    其中的EXTENSION_LOADERS的定义如下:

     

    private static final ConcurrentMap<Class<?>,ExtensionLoader<?>>EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();

     

    可见,不同的服务接口类型都在EXTENSION_LOADERS中有对应一个ExtensionLoader对象,这样能方便的管理不同服务接口的扩展点。当得到对应服务接口的ExtensionLoader后,就直接通过服务提供配置文件中的K来拿对应的服务提供者实现类的实例了(通过ExtensionLoader#),所以,在Dubbo的源码中随处可见如下代码:

     

    ExtensionLoader.getExtensionLoader(Container.class).getExtension("spring");

    ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(Constants.DEFAULT_LOADBALANCE);

    ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(RoundRobinLoadBalance.NAME);

     

    当然,对于提供者配置文件来说,是可以重复配置的,只是多配置的会被忽略掉,这里ServiceLoader和ExtensionLoader采取的策略是类似的。

     

              看到这里,我们已经明白Dubbo框架内部是如何采用SPI来组装自己的功能的,那么如果某些扩展点是由使用方(即Dubbo的用户)来定义的,Dubbo是怎么加载它们的呢?前面说了ServiceLoader中默认的服务提供者配置文件的目录是MEAT-INF/services/,该目录由ServiceLoader的PREFIX来定义,而Dubbo中则会在三种目录下去加载服务提供者配置文件,由ExtensionLoader的三个成员变量来定义:

     

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

    private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";

    private static final String DUBBO_INTERNAL_DIRECTORY= DUBBO_DIRECTORY+"internal/";

     

     

    即对于每个CLASSPATH,Dubbo都会去扫描资源文件夹下的这三个目录来加载扩展点,加载过程可以参看ExtensionLoader#ExtensionLoader:

     

    private Map<String, Class<?>>loadExtensionClasses() {

        final SPI defaultAnnotation = type.getAnnotation(SPI.class);

        if(defaultAnnotation != null) {

            Stringvalue = defaultAnnotation.value();

           if(value != null&& (value = value.trim()).length() > 0) {

               String[] names = NAME_SEPARATOR.split(value);

               if(names.length> 1) {

                    throw new IllegalStateException("more than 1 default extension name onextension "+ type.getName()

                            + ": " + Arrays.toString(names));

                }

               if(names.length== 1) cachedDefaultName = names[0];

            }

        }

       

       Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();

       loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);

       loadFile(extensionClasses, DUBBO_DIRECTORY);

       loadFile(extensionClasses, SERVICES_DIRECTORY);

        return extensionClasses;

    }

     

    可以看到最后几行就是去这三个目录下去找服务提供者配置文件,loadFile的方法是通用的,因为很多开源框架都有资源查找的需求,比如说Spring,所以loadFile的实现这里就不给出了。从这个方法也可以看出,Dubbo中的所有服务接口都标有@SPI注解,这样就能轻易的区分出该接口下是否会有服务提供类。但Dubbo是怎么保证扫描到所有包含服务提供者配置文件的呢?因为我们知道,在Spring容器中,如果它需要扫描包含@Service注解的实现类时,它需要我们去指导Spring去哪些CLASSPATH路径下面找这些需要注册的服务Bean,但Dubbo好像并不需要我们显式的告诉它去哪些地方找这些服务提供者配置文件。后来才发现,在ClassLoader中已经把它所加载的class和其对应的package信息(packages)、对应的文件系统路径(pdcache)都存储了起来,由于我们实现Dubbo的服务接口来自定义Dubbo扩展点时,我们需要依赖Dubbo的包,所以业务类的类加载器只会是Dubbo容器的父类加载器或者是同一个类加载器,这样就能很容易找到所有的CLASSPATH了(双亲委派模型),所以Dubbo也是在所有的CLASS PATH下去查找这三个文件。

     

    可以看出,Java体系的SPI还是大有用途,是一种面向接口编程的经典案例。

    转载请注明原文地址: https://ju.6miu.com/read-676724.html

    最新回复(0)