面试官问烂的 Dubbo中 SPI机制的运行原理是什么?

SPI的全称是(Service Provider Interface)是服务提供接口的意思。如果我们不写框架性代码或者开发插件的话,对于SPI机制可能不会那么熟悉,但如果我们阅读诸如DubboJDBC数据库驱动包、Spring以及最近比较流行的Spring Boot相关starter组件源码的话,就会发现SPI机制及其思想在这些框架中有大量的应用。

我们知道系统中抽象的各个模块,往往会有很多不同的实现方案,例如我们常见的日志模块方案xml解析模块JDBC驱动模块等。在面向对象的设计思想中,我们一般推荐模块之间的对接基于面向接口的编程方式,而不是直接面向实现类硬编码。因为,一旦代码里涉及具体的实现类的耦合,就违反了可插拔、闭开等原则,如果需要替换一种实现方式,就需要修改代码。如果我们希望实现在模块装配的时候能够不在程序硬编码指定,那就需要一种服务发现的机制(PS:不要和现在微服务的服务发现机制搞混淆了)。

JAVA中的SPI技术就是提供了这样一个为某个接口寻找服务实现类的机制,这一点也类似于Spring框架中的IOC思想,就是将程序加载装配的控制权移到程序之外,这个机制在组件模块化设计中非常重要!那么在JAVA中SPI机制具体是如何约定的呢?

在JAVA SPI机制中约定,当服务的提供者(例如某个新的日志组件),提供了服务接口的某种实现之后,在jar包的META-INF/services/目录中同时创建一个以该服务接口命名的文件,文件中填写了实现该服务接口具体实现类的全限定类名。这样,在当外部程序装配这个模块的时候,就可以通过该jar包中META-INF/services/目录里的配置文件找到具体的实现类路径,从而可以通过反射机制装载实例化,从而完成模块的注入。例如,我们Dubbo框架时,除了引入核心依赖jar外,还会有很多扩展组件如dubbo-monitor,如果我们需要引入此组件只需要简单引入就可以,而不需要做额外的集成,主要原因就是因为该组件时以SPI机制进行的集成

综上所述,SPI机制实际上就是“基于接口的编程+策略模式+配置文件”组合实现的一种动态加载机制,在JDK中提供了工具类:“java.util.ServiceLoader”来实现服务查找。

JDK SPI机制实现示例

JDK中自带对SPI机制的支持,主要是涉及java.util.ServiceLoader类的使用,接下来,我们通过一个简单的代码示例来理解下JAVA中SPI机制的实现方式吧!我们先通过一张图来看看使用JAVA SPI机制需要遵循什么规范吧:

└── src/main/java
├── cn
│   └── wudimanong
│       └── spi
│           ├── SPIService.java
│           ├── impl
│           │   ├── SpiImpl1.java
│           │   └── SpiImpl2.java
│           └── SPIMain.java
└── META-INF
    └── services
        └── com.wudimanong.spi.SPIService

1、我们需要在项目中创建目录META-INF\services

2、定义一个接口服务提供,如SpiService

public interface SpiService {
    void execute();
}

3、分别定义两个服务接口实现类,如:SpiImpl1,SpiImpl2

public class SpiImpl1 implements SPIService {
    @Override
    public void execute() {
        System.out.println("SpiImpl1 Hello.");
    }
}

// ------------------------------------------

public class SpiImpl2 implements SPIService {
    @Override
    public void execute() {
        System.out.println("SpiImpl2 Hello.");
    }
}

4、我们在ClassPath路径下添加一个配置文件,文件的名称是接口的全限定类名,内容则是实现类的全限定类名,如果是多个实现类则用换行符分割,文件路径如下

文件内容如下:

cn.wudimanong.spi.impl.SpiImpl1
cn.wudimanong.spi.impl.SpiImpl2

这样,我们基本上就遵循JAVA SPI的机制定义了组件基本结构,最后我们通过编写测试类,看看如果使用SPI机制,客户端代码应该如何写:

public class SPIMain {
    public static void main(String[] args) {

        ServiceLoader<SPIService> loaders =
                ServiceLoader.load(SPIService.class);

        Iterator<SPIService> spiServiceIterator = loaders.iterator();
        System.out.println("classPath:" + System.getProperty("java.class.path"));
        while (spiServiceIterator.hasNext()) {
            SPIService spiService = spiServiceIterator.next();
            System.out.println(spiService.execute());
        }
    }
}

可以看到在引入方加载组件模块时是通过ServiceLoader这个类来操作的,如果我们打开该类的源码,就可以看到,其主要实现逻辑其实就是在META-INF/services/目录中查找实现类,并进行相关实例化操作。关于该类的部分关键源代码如下:

public final class ServiceLoader<S> implements Iterable<S>{
    //配置文件的路径
    private static final String PREFIX = "META-INF/services/";
    //加载的服务类或接口
    private final Class<S> service;
    //已加载的服务类集合
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    //类加载器
    private final ClassLoader loader;
    //内部类,真正加载服务类
    private LazyIterator lookupIterator;

    public void reload() {
        //先清空
        providers.clear();
        //实例化内部类
        lookupIterator = new LazyIterator(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方法如上
        reload();
    }
}

而后面查找类和创建实现类的过程就都是在LazyIterator类完成的了,由于篇幅的关系,感兴趣的朋友可以点开源码自行阅读一番!

JDBC数据库驱动包中SPI机制分析

通过上面的描述,相信大家对Java SPI机制的实现应该是有了一个基本的认识,接下来我们以JDBC数据库驱动设计来看下Java SPI机制的真实应用场景。我们知道通常各大数据库厂商(如Mysql、Oracle)都会根据一个统一的规范,如:java.sql.Driver去开发各自的驱动实现逻辑。

而我们在使用的jdbc的时候客户端却是不需要改变代码的,直接引入不同的SPI接口服务即可。例如以Mysql的JDBC驱动jar来说:

这样在引入mysql驱动包后jdbc连接代码java.sql.DriverManager,就会使用SPI机制来加载具体的jdbc实现,关键源码如下:

public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
}

可以看到粉红色代码部分,JDBC驱动管理代码就是通过原生的Java SPI机制来实现加载具体的Mysql JDBC驱动的。

Dubbo框架中SPI机制分析

需要说明的是虽然Java 提供了对SPI机制的默认实现支持,但是并不表示所有的框架都会默认使用这种Java自带的逻辑,SPI机制更多的是一种实现思想,而具体的实现逻辑,则是可以自己定义的。例如我们说Dubbo框架中大量使用了SPI技术,但是Dubbo并没有使用JDK原生的ServiceLoader,而是自己实现了ExtensionLoader来加载扩展点,所以我们看Dubbo框架源码的时候,千万不要被配置目录是/META-INF/dubbo/internal,而不是META-INF/services/所迷惑了。

相应地如果其他框架中也使用了自定义的SPI机制实现,也不要疑惑,它们也只是重新定义了类似于ServiceLoader类的加载逻辑而已,其背后的设计思想和原理则都是一样的!例如,以Dubbo中卸载协议的代码举例:

 private void destroyProtocols() {
        ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
        for (String protocolName : loader.getLoadedExtensions()) {
            try {
                Protocol protocol = loader.getLoadedExtension(protocolName);
                if (protocol != null) {
                    protocol.destroy();
                }
            } catch (Throwable t) {
                logger.warn(t.getMessage(), t);
            }
        }
    }

而ExtensionLoader中扫描的配置路径如下:

public class ExtensionLoader<T> {

    private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class);

    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/";

    private static final Pattern NAME_SEPARATOR = Pattern.compile("\\s*[,]+\\s*");

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

    private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();
}

以上通过对Java自带SPI机制的示例以及对Dubbo和JDBC驱动框架中对SPI机制应用的分析,相信大家应该是有了一个总体的原理性的认识了。如果需要更加深入的了解一些细节的实现逻辑就需要大家好好去看看ServiceLoader的源码了,如果其他框架单独实现了SPI机制,其相应的实现加载工具类也需要具体看看它们的源码是怎么实现的了!

原文地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容