JAVA的SPI机制-介绍与感受

摘要:
由于这一特性,我们可以通过SPI机制轻松地为程序提供扩展功能。SPI机制已经应用于许多第三方框架中。获取连接的代码从未改变,它使用SPI机制。有关更多原则,请参阅。SPI机制JAVASPI的具体协议是在服务提供商提供服务接口实现后,在jar包的META-INF/services/目录中创建一个包含服务接口类完整路径的文件。

简单介绍

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

免责声明:文章转载自《JAVA的SPI机制-介绍与感受》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Linux centosVMware 命令 lvm、磁盘故障小案例Android源码分析(二)-----如何编译修改后的framework资源文件下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Java SPI

一、什么是Java SPI?   SPI的全名为Service Provider Interface.大多数开发人员可能不熟悉,因为这个是针对厂商或者插件的。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java spi机制的思想。我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块...

STM32学习笔记(八) SPI总线(操作外部flash)

1. SPI总线简介 SPI全称串行外设接口,是一种高速,全双工,同步的外设总线;它工作在主从方式,常规需要至少4根线才能够正常工作。SPI作为基本的外设接口,在FLASH,EPPROM和一些数字通讯中,具有广泛的应用。SPI总线由四个接口构成: CS :片选端,由主设备控制 MISO:主设备输入,从设备输出 MOSI:主设备输出,从设备输入 SCK :时...

[SPI].SPI协议详解

转自:http://www.sohu.com/a/211324861_468626 1、 SPI简介 SPI,是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器...

Spring SPI 机制总结

1、概念: SPI(Service Provider Interface)服务提供接口,简单来说就是用来解耦,实现插件的自由插拔,具体实现方案可参考JDK里的ServiceLoader(加载classpath下所有META-INF/services/目录下的对应给定接口包路径的文件,然后通过反射实例化配置的所有实现类,以此将接口定义和逻辑实现分离)Spri...

STM32(13)——SPI

简介:   SPI,Serial Peripheral interface串行外围设备接口。   接口应用在:EEPROM, FLASH,实时时钟,AD 转换器,还有数字信号处理器和数字信号解码器之间。   特点:高速的、全双工、同步的通信总线、占用4根线;可以同时发生和接收串行数据;可以当做主机或从机工作;提供频率可编程时钟;发送结束中断标志;写冲突保护...

几个串口协议学习整理

一、UART UART是一个大家族,其包括了RS232、RS499、RS423、RS422和RS485等接口标准规范和总线标准规范。它们的主要区别在于其各自的电平范围不相同。 嵌入式设备中常常使用到的是TTL、TTL转RS232的这种方式。常用的就三根引线:发送线TX、接收线RX、电平参考地线GND。  1.1    电路示意图   1.2    通信协议...