Code前端首页关于Code前端联系我们

Java SPI机制使用场景、原理分析、示例代码

terry 2年前 (2023-09-25) 阅读数 47 #后端开发

1 什么是SPI

SPI的全称是Service Provider Interface。它是一组由第三方使用或扩展的 Java API。它可用于启用框架扩展和替换组件。

大致机制图如下: Java SPI机制使用场景、原理解析、示例代码

Java SPI实际上是“基于接口编程+策略模式+配置文件”组合实现的动态加载机制。

在系统设计中,针对不同的抽象往往有多种不同的实现方案。在面向对象设计中,一般建议基于接口来编写模块,模块之间不要编写实现类。当代码中包含特定的实现类时,就违反了连通性原则。如果需要更改实现,则需要更改代码。为了组装模块而不在程序中动态定义它,需要服务发现机制。 Java SPI 提供了这样一种机制:一种查找给定接口的服务实现的机制。有点类似于IOC的思想,旨在将汇编控制移到程序之外。这种机制在模块化设计中尤为重要。所以SPI的核心思想就是解耦。?驱动下载接口实现类下载 JDBC下载不同类型数据库的驱动

  • 日志门面接口实现类下载 SLF4J下载不同厂商的日志实现类
  • Spring Spring使用了很多SPI,如:servletation3.0定义Implementation3 .0 ServletContainerInitializer、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等。
  • Dubbo Dubbo也大量使用SPI来扩展框架,但它封装了Java提供的原生SPI,允许用户扩展Filter接口
  • 3 Java SPI 使用简介

    使用 Java SPI 需要以下实践:

    • 1.当服务提供者提供接口的具体实现时,jar包的META会在INF/services目录下创建一个名为“接口全限定名”的文件,内容为实现类的全名;
    • 2。用户界面实现类所在的jar包放在主程序的classpath下;
    • 3。主程序通过java.util.ServiceLoader动态加载实现模块。它通过扫描META-INF/services目录下的配置文件找到完整的实现类名,并将该类加载到JVM中;
    • 4。 SPI 实现类必须包含一个不带参数的构造函数;

    Java SPI 示例代码

    步骤 1,定义一组接口(假设为 org.foo.demo.IShout)并编写其中一个接口或多个实现(假设为 org.foo)。 demo.animal.Dog,org.foo.demo.animal.Cat)。

    public interface IShout {
        void shout();
    }
    public class Cat implements IShout {
        @Override
        public void shout() {
            System.out.println("miao miao");
        }
    }
    public class Dog implements IShout {
        @Override
        public void shout() {
            System.out.println("wang wang");
        }
    }
    复制代码

    步骤2,在src/main/resources/目录下创建/META-INF/services目录,并添加一个以用户界面命名的文件(org.foo.demo.IShout文件),其内容为应用的实现类(这里是 org.foo.demo.animal.Dog 和 org.foo.demo.animal.Cat,每行一个类)。

    文件位置

    - src
        -main
            -resources
                - META-INF
                    - services
                        - org.foo.demo.IShout
    复制代码

    文件内容

    org.foo.demo.animal.Dog
    org.foo.demo.animal.Cat
    复制代码

    第3步,使用ServiceLoader加载配置文件中指定的实现。

    public class SPIMain {
        public static void main(String[] args) {
            ServiceLoader<IShout> shouts = ServiceLoader.load(IShout.class);
            for (IShout s : shouts) {
                s.shout();
            }
        }
    }
    复制代码

    代码输出:

    wang wang
    miao miao
    复制代码

    4 Java SPI原理分析

    首先看ServiceLoader类签名类的成员变量:

    public final class ServiceLoader<S> implements Iterable<S>{
    private static final String PREFIX = "META-INF/services/";
    
        // 代表被加载的类或者接口
        private final Class<S> service;
    
        // 用于定位,加载和实例化providers的类加载器
        private final ClassLoader loader;
    
        // 创建ServiceLoader时采用的访问控制上下文
        private final AccessControlContext acc;
    
        // 缓存providers,按实例化的顺序排列
        private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    
        // 懒查找迭代器
        private LazyIterator lookupIterator;
      
        ......
    }
    复制代码

    看某几条ServiceLoader代码源码。 ,总共有587行注释,实现过程如下:

    • 1 应用程序调用ServiceLoader.load方法。 ServiceLoader.load方法首先创建一个新的ServiceLoader,并实例化该类的成员变量,包括:
      • loader(ClassLoader type, Classloader)
      • acc (AccessControlContext type, Access Controller)providers(LinkedHashMap type ,用于缓存成功加载的类)
      • lookupIterator(实现迭代器函数)
    • 2 应用程序通过迭代器接口获取对象实例。 ServiceLoader首先判断成员变量providers对象是否有缓存实例对象(类型为LinkedHashMap)。如果缓存存在则直接返回。如果没有缓存,则按如下方式进行类加载:
    • (1)读取META-INF/services/中的配置文件,获取所有可以实例化的类的名称。值得注意的是,ServiceLoaderMETA-INF配置文件可以通过jar包获得。加载配置的具体实现代码如下:
            try {
                String fullName = PREFIX + service.getName();
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else
                    configs = loader.getResources(fullName);
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }
    复制代码
    • (2)通过反射方法Class.forName()加载类对象,并使用instantiate方法和instance()方法。
    • (3) 将实例化的类存储在供应商对象中(类型为 LinkedHashMap)并返回实例化的对象。

    5Java SPI总结

    优点:使用Java SPI机制的优点是摆脱第三方服务模块配置控制逻辑与调用者业务代码分离而不是耦合的情况一起。应用程序可以根据实际业务情况进行框架扩展或更换框架组件。

    缺点

    • 虽然ServiceLoader可以被认为是延迟加载,但它基本上只能通过遍历来访问,即所有用户界面实现类都被加载并实例化一次。如果不想使用某个实现类,它也会被加载并实例化,造成浪费。实现类的采购方式不够灵活。只能以Iterator的形式获取,无法根据给定的参数获取对应的实现类。
    • 将 ServiceLoader 类的实例与多个并发线程一起使用是不安全的。

    作者:北辽科技
    链接:https://juejin.im/post/5b9b1c115188255c5e66d18c
    来源:掘金♺版权归作者所有。商业转载请联系作者获得许可。非商业转载请注明出处。

    版权声明

    本文仅代表作者观点,不代表Code前端网立场。
    本文系作者Code前端网发表,如需转载,请注明页面地址。

    发表评论:

    ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

    热门