zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    Java 线程:(一)
    RK:Webview、Chrome
    CocosCreator 加载Bundle里的Prefab
    Canvas 绘画
    CocosCreator 要用3D节点实现skewX
    Unity 动态创建网格
    凹多边形三角剖分
    UnityHub 登录失败
    浮点数
    八皇后问题(回溯算法)
  • 原文地址:https://www.cnblogs.com/nickhan/p/13334197.html
Copyright © 2011-2022 走看看