Dubbo源码分析-微内核插件式开发(SPI介绍)

1、前言

阅读优秀的开源框架是程序员进步的一种捷径。希望自己能坚持下去。有个好头也有个好的结尾。这部分先讲讲dubbo最核心的设计思想。微内核插件式开发。这种思想贯穿了他的整个项目。在讲微内核之前,可能先要讲讲java的SPI。其实dubbo就是在SPI设计思想基础上进行了升级。

2、什么是SPI

SPI全称Service Provider Interface。字面上理解是面向接口提供服务。他很好的诠释了面向接口编程,以及OCP原则等。大家都知道,我们在模块化编程的时候,往往模块之间是基于接口编程的。例如A模块需要使用一种服务,但具体实现交由第三方实现。那么A会定义出一个接口让B去实现,但是A在实例化这个接口的实现时,往往需要硬编码B的一种实现。这就会导致B如果换了实现方式,又或者A想使用C的实现方式,那么需要修改A的实例化代码去支持实现的修改切换。在OCP原则里,这种修改是需要避免的。所以我们需要一种实现发现机制。在实例化时不应该硬编码一种实现,而是为接口提供一种实现发现机制,然后把实例化这部分功能移到A模块之外。这种思想跟IOC很像。我们在用Spring的时候,就是把对象的装配交给了Spring处理,这样层于层之间才正真达到了面向接口编程。

3、SPI实现思想

假如A模块设计定义了一个接口。当B实现了这个接口之后,需要在B的jar包的META-INF/services/目录里添加一个文件,这个文件名字就是A定义的这个接口的全路径名,内容就是B实现接口的类的全路径名。此时,如果A需要使用B的实现,引入B的jar包。然后A可以使用java.util.ServiceLoader去发现这个实现,并实例化后返回给A。这样A在使用这个实现时,只是用定义好的接口引用了该实现,不用去硬编码这个实现。如果有一天需要替换B的实现,我们只要引入别的第三方jar,根本不用修改A的代码。

4、看例子说话

例如A模块定义了一个接口,如下:

public interface DemoApi {
    String sayHello(String name);
}

然后B实现这个接口,如下:

public class DemoApiImpl1 implements DemoApi {
    public String sayHello(String name) {
        return name + "你好,我是DemoApiImpl1实现";
    }
}

假如我们不用SPI设计思想去设计程序,那么我在A模块中要需要下面这样去使用DemoApi接口,这个问题就来了,我在设计A模块的时候,需要去依赖B模块,而且哪天我想替换B模块的实现,我还得动A模块的代码,重新new一个别的实现。其实A模块只需要使用DemoApi接口的服务,至于这个接口的具体实现根本不需要关心,更不需要事先依赖某种实现。

public static void main(String args[]) {
    DemoApi demoApi = new DemoApiImpl1();
    demoApi.sayHello("xuanner");
}

那么,我们来改进一下,使用SPI来编码使用DemoApi接口的服务,如下:

public static void main(String args[]) {
    ServiceLoader<DemoApi> serviceLoader = ServiceLoader.load(DemoApi.class);
    Iterator<DemoApi> iterator = serviceLoader.iterator();
    System.out.println("可以遍历多种可能存在的实现");
    while (iterator.hasNext()) {
        DemoApi demoApi = iterator.next();
        demoApi.sayHello("xuanner")
    }
}

当然,B模块在实现之后需要在他的JAR包的META-INF/services/目录下放一个配置文件,看如下图:

里面的内容是实现类的全路径,如下:

com.xuan.spidemo.impl1.DemoApiImpl1

当A模块需要B的实现的时候,只要进入B的JAR,ServiceLoader就会在JAR下找到对应接口的实现,然后实例化返回提供给A使用,如果有一天我们需要替换用C去实现,那么只要C实现代码后,同样在他的JAR下放上面的配置文件即可。

5、举例现有的优秀实现

其中java.sql.Driver的实现就就是基于使用了SPI。还有commons-logging日志系统。也是有基于SPI实现的,我只简单的摘录了几个相关的片段,只不过他好像不是使用了ServiceLoader类去寻找实现,而是自己实现加载文件的。

protected static final String SERVICE_ID = 
    "META-INF/services/org.apache.commons.logging.LogFactory";
if (factory == null) {
            if (isDiagnosticsEnabled()) {
                logDiagnostic("[LOOKUP] Looking for a resource file of name [" + SERVICE_ID +
                              "] to define the LogFactory subclass to use...");
            }
            try {
                final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);

                if( is != null ) {
                    // This code is needed by EBCDIC and other strange systems.
                    // It's a fix for bugs reported in xerces
                    BufferedReader rd;
                    try {
                        rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                    } catch (java.io.UnsupportedEncodingException e) {
                        rd = new BufferedReader(new InputStreamReader(is));
                    }

6、简单总结

其实SPI设计思想也很简单,接地气一点的解释就是,我设计了一个接口,然后自己不实现,当需要使用这个接口的实现时,就用ServiceLoader类去各个依赖的第三方包中扫描,只要扫描到有实现的类,就进行实例化提供服务。对我来说,实现是完全透明的,我只根据接口的方法来编程,正真做到了面向接口编程。所以当我们需要换一种实现时,只要替换一下第三方依赖JAR就行了,我的其他代码就不用动了。这种方式用来给第三方自己扩展是不是很赞。对的。

猜你喜欢

转载自my.oschina.net/u/1271235/blog/889412
今日推荐