JAVA的SPI机制-介绍与感受

简单介绍

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。

在许多第三方框架中,SPI机制都得以运用。比如JDBC,Slf4j,Dubbo,spring等。

在使用后jdbc的时候,我们都是通过DriverManager.getConnection获取数据库的连接,连接MySQL时,引入mysql的驱动;连接sqlserver时,引入sqlserver的驱动。。。获取连接的代码始终没变,这就用到了SPI的机制,更多原理参考。这样就使得驱动更像是一个可插拔,可替换换的组件,需要那个,引入那个便可,JDBC只是提供了一个java连接数据库的规范,每个厂商只需要实现规范提供对应的驱动,然后通过SPI机制加载驱动进行使用。

Slf4j也是一样,提供了一套输出日志的规范,具体实现可以有logback,log4j,java-logging,slf4j-nop,slf4j-simple等等。当时用的时候,只需要引入一个对应的实现即可。

SPI机制

JAVA SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口类全路径的文件。该文件里的内容就是实现该服务接口的具体实现类的全路径。当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader,通过load方法就可以对META-INF/services里面的实现类进行加载和实例化。

写个demo感受一下

场景,现在有一个短信发送的需求,根据不同的业务场景,项目需要选择不同的运营商,要求:

  • 在切换运营商的时候不要对代码进行改动
  • 保证扩展性
  • 使用方便

当然,这个需求的解决方案肯定不止一个,但是通过这个例子可以直观的感受SPI是个啥。

项目结构

project structure

sms-api: 定义了一个短信发送提供者ISMSProvider应该具备的功能,发送短信

sms-provider-telecom: 电信,实现了ISMSProvider

sms-provider-unicom: 联通,实现了ISMSProvider

user: 模拟用户调用

sms-api

短信提供商接口定义

public interface ISMSProvider {
    void sendSMS(String msg);
}

同时,提供了一个工厂类,获取短信提供商,方便用户调用

public class SMSProviderFactory {
    private SMSProviderFactory() {
        throw new IllegalStateException("Utinity Class");
    }

    public static ISMSProvider getProvider() {
        ServiceLoader<ISMSProvider> smsProviders = ServiceLoader.load(ISMSProvider.class);
        Iterator<ISMSProvider> smsIterator = smsProviders.iterator();
        if (!smsIterator.hasNext()) {
            throw new IllegalStateException("No valid SMS provider is found!");
        }
        ISMSProvider provider = smsIterator.next();
        System.out.println("Actual SMS provider is: " + provider.getClass());
        return provider;
    }
}

为了方便,如果同时引入了多个提供商的情况下,默认用第一个。

sms-provider-telecom

对ISMSProvider进行实现

public class TelecomSMSProvider implements ISMSProvider {
    public void sendSMS(String msg) {
        System.out.println(String.format("Send SMS [%s] by Telecom...", msg));
    }
}

最重要的是要在classpath下面准备Java SPI需要的文件,这里是META-INF/services/top.njlife.sms.ISMSProvider, 内容为

top.njlife.sms.TelecomSMSProvider

sms-provider-unicom

与上面一样,进行接口实现

public class UnicomSMSProvider implements ISMSProvider {
    public void sendSMS(String msg) {
        System.out.println(String.format("Send SMS [%s] by Unicom...", msg));
    }
}

准备SPI需要的文件,文件内容为META-INF/services/top.njlife.sms.ISMSProvider。

注意: 文件名都是实现的接口的全路径名。

top.njlife.sms.UnicomSMSProvider

到此两个短信提供商就开发好了。

user

用户在使用的时候,只需要在pom里面引入

<dependency>
  <groupId>top.njlife</groupId>
  <artifactId>sms-api</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

首先试试telecom,继续引入

<dependency>
  <groupId>top.njlife</groupId>
  <artifactId>sms-provider-telecom</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

模拟调用代码

public class SMSSender {
    public static void main(String[] args) {
        ISMSProvider provider = SMSProviderFactory.getProvider();
        provider.sendSMS("test msg");
    }
}

运行,结果如下

Actual SMS provider is: class top.njlife.sms.TelecomSMSProvider
Send SMS [test msg] by Telecom...

此时我们需要切换到unicom,在pom里面telecom的依赖改成

<dependency>
  <groupId>top.njlife</groupId>
  <artifactId>sms-provider-unicom</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

再次运行代码,得到

Actual SMS provider is: class top.njlife.sms.UnicomSMSProvider
Send SMS [test msg] by Unicom...

可以看到,短信提供商成功切换了,项目代码不需要做任何改动。

如果这时候需要新的集成新的短信提供商,只需要再开发一个项目,然后引入依赖即可。

这就是SPI的方便之处,基于这个机制,我们可以方便地做到在一个系统/框架中实现一个插件的功能,或者扩展点,可以参考Dubbo的SPI机制。

至于JAVA的SPI内部机制是如何做到的,后续继续探讨。

简单总结

SPI机制可以帮助我们轻松实现解耦,使得第三方服务提供者模块独立于业务代码之外,实现模块的插拔。

但是JAVA原生的SPI也有一些不足的地方

  • 无法按需加载。ServiceLoader每次都会加载所有的实现,如果有的没有用到也进行加载和实例化,会造成一定系统资源的浪费。
  • 线程安全问题。ServerLoader可以看做是一个工具类,提供了很多static方法,但是其内部用到了一些成员变量,这样就会导致在多线程调用的时候有线程安全问题,需要注意。
  • 异常吞噬。ServerLoader在加载类的过程中如果出现异常无法加载没有相关的异常抛出,导致一旦出现问题需要花时间进行定位。

鉴于这些缺点,很多开源框架都实现了一套自己的SPI机制,比如Dubbo对SPI进行了增强,参考:https://dubbo.apache.org/zh-cn/docs/source_code_guide/dubbo-spi.html

Demo源码

最后附上文中demo的源代码:https://gitee.com/nickhan/java-spi

原文地址:https://www.cnblogs.com/nickhan/p/13334197.html