Java SPI 机制

2023-11-07

文章首发于个人博客,欢迎访问关注:https://www.lin2j.tech

什么是 SPI 机制

SPI (Service Provider Interface)是 Java 内置的一种服务提供发现机制,将功能的实现交给第三方,用来拓展和替换组件。

SPI 的核心思想是解耦,将接口的定义和实现分开两部分处理。接口的调用方负责定义接口,而实现则由第三方去实现。

SPI 机制允许将功能的实现抽离出原本的模块,在模块化设计中颇为受用。

当服务的提供者实现了一种接口之后,需要在自己的 classpath 下的 META-INF/services 目录新建一个文件,文件名是接口的名称,内容是接口的实现类的全限定名称,每个实现类占一行。

SPI机制

SPI 机制的简单示例

假设当前有一个 DataSearch 接口,搜索的实现可以基于数据库或者 Elastic Search 实现。

这里采用 Maven 多模块的方式来模拟调用方和服务提供方可能不在同一个包内。

目录结构:

spi-demo
  1. 先定义好接口

    package com.jia.spidemo;
    
    public interface DataSearch {
        /**
         * 数据查询
         */
        void search();
    }
    
  2. MySQL 数据库实现,并在 resource 下新建 META-INF/services/com.jia.spidemo.DataSearch 文件,内容为 com.jia.spidemo.MySqlSearch

    package com.jia.spidemo;
    
    public class MySqlSearch implements DataSearch {
        @Override
        public void search() {
            System.out.println("MySQL Search");
        }
    }
    
  3. Elastic Search 全文搜索,并在 resource 下新建 META-INF/services/com.jia.spidemo.DataSearch 文件,内容为 com.jia.spidemo.ElasticSearch

    package com.jia.spidemo;
    
    public class ElasticSearch implements DataSearch{
        @Override
        public void search() {
            System.out.println("Elastic Search");
        }
    }
    
  4. 测试

    package com.jia.spidemo;
    
    public class Application {
    
        public static void main(String[] args) {
            ServiceLoader<DataSearch> serviceLoader = ServiceLoader.load(DataSearch.class);
            Iterator<DataSearch> iterator = serviceLoader.iterator();
            while (iterator.hasNext()) {
              	// 只有在调用 next 方法时,才会创建对应的实例
                DataSearch ds = iterator.next();
                ds.search();
            }
        }
    }
    

可以看到输出结果:

MySQL Search
Elastic Search

这就是 SPI 的使用方式,示例代码下载

SPI 机制的应用

数据库驱动程序加载

Java数据库连接(JDBC)规范使用了SPI机制,通过在classpath下提供特定命名的配置文件,让开发者可以注册和加载数据库驱动程序的实现类。这样可以实现在运行时根据配置文件加载不同的数据库驱动程序。

SLF4J 日志门面

许多Java日志系统(如SLF4J)使用SPI机制,通过提供不同的实现类来支持不同的日志框架。开发者可以根据需要选择合适的日志实现,并在classpath下提供相应的配置文件,实现日志系统的动态切换和扩展。

插件系统

许多应用程序(比如 Eclipse)或框架都提供了插件机制,允许开发者通过SPI机制来注册和加载插件。这样可以让应用程序在不修改源代码的情况下,通过提供新的插件实现类来扩展功能,实现动态的插件加载和卸载。

Spring 中的 SPI 机制

与 Java 内置的 SPI 有异曲同工之妙的是 Spring 的工厂加载机制,即 Spring Factories Loader,用户先在 META-INF/spring.factories 路径下配置好接口和实现类的关系,然后通过 SpringFactoriesLoader 加载实现类,该机制可以为框架上下文动态的增加扩展。

spring.factories 文件示例:

com.jia.spidemo.spring_factory_test.IUserService=\
  com.jia.spidemo.spring_factory_test.UserService,\
  com.jia.spidemo.spring_factory_test.UserService2

SPI 的实现原理

package java.util;

// ServiceLoader 实现了 Iterable 接口,
// 通过自己的内部迭代器可以遍历所有的服务实现类
public final class ServiceLoader<S>
    implements Iterable<S>
{
		// 规定的配置文件路径
    private static final String PREFIX = "META-INF/services/";
  
    // 服务接口的 Class
    private final Class<S> service;

    // 用来加载服务实现实例的类加载器,用来定位、加载和实例化提供者
    private final ClassLoader loader;
  
    // 访问控制上下文,用来限制对敏感操作的访问权限
    private final AccessControlContext acc;

    // 缓存服务实现类,并保证加载的顺序缓存
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // ServiceLoader 实现的内部迭代器
    private LazyIterator lookupIterator;

  	// 重新加载所有的实现类,相当重新创建了 ServiceLoader
    // 这个方法用来在 JVM 运行时,加载新的服务提供者
    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();
    }

    // 解析失败处理
    private static void fail(Class<?> service, String msg, Throwable cause)
        throws ServiceConfigurationError
    {
        throw new ServiceConfigurationError(service.getName() + ": " + msg,
                                            cause);
    }

    private static void fail(Class<?> service, String msg)
        throws ServiceConfigurationError
    {
        throw new ServiceConfigurationError(service.getName() + ": " + msg);
    }

    private static void fail(Class<?> service, URL u, int line, String msg)
        throws ServiceConfigurationError
    {
        fail(service, u + ":" + line + ": " + msg);
    }

    // 解析配置文件的某一行,先去掉注释,然后判断该行是否含有非法字符
    // 将成功解析的结果放入到 names 列表中,重复的配置项和已经被加载的服务项不会被放入列表
    // 返回下一行的行号
    private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                          List<String> names)
        throws IOException, ServiceConfigurationError
    {
        String ln = r.readLine();
        if (ln == null) {
            return -1;
        }
        int ci = ln.indexOf('#');
        if (ci >= 0) ln = ln.substring(0, ci);
        ln = ln.trim();
        int n = ln.length();
        if (n != 0) {
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = ln.codePointAt(0);
            // 通过调用isJavaIdentifierStart方法,
            // 可以方便地检查一个字符是否符合Java标识符的开始条件(就是 Java 定义的合法标识符开头,'a', '_', '$')
            // 从而进行合法性验证或进行相应的处理。
            if (!Character.isJavaIdentifierStart(cp))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
            for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                cp = ln.codePointAt(i);
                if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                    fail(service, u, lc, "Illegal provider-class name: " + ln);
            }
            if (!providers.containsKey(ln) && !names.contains(ln))
                names.add(ln);
        }
        return lc + 1;
    }

    // 加载给定的 URL 作为配置文件
    private Iterator<String> parse(Class<?> service, URL u)
        throws ServiceConfigurationError
    {
        InputStream in = null;
        BufferedReader r = null;
        ArrayList<String> names = new ArrayList<>();
        try {
            in = u.openStream();
            r = new BufferedReader(new InputStreamReader(in, "utf-8"));
            int lc = 1;
            while ((lc = parseLine(service, u, r, lc, names)) >= 0);
        } catch (IOException x) {
            fail(service, "Error reading configuration file", x);
        } finally {
            try {
                if (r != null) r.close();
                if (in != null) in.close();
            } catch (IOException y) {
                fail(service, "Error closing configuration file", y);
            }
        }
        return names.iterator();
    }

    // 私有的内部懒加载迭代器
    private class LazyIterator
        implements Iterator<S>
    {

        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;

        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }

        // 加载
        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                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);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

        // 调用 nextService 方法时才会去实例化服务类
        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

        public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { return nextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        // 不支持删除元素
        public void remove() {
            throw new UnsupportedOperationException();
        }

    }

    // 以懒加载的方式返回一个获取服务提供者的迭代器
    // 懒加载的意思是:在迭代的过程中去加载配置文件和实例化服务提供者
    public Iterator<S> iterator() {
        return new Iterator<S>() {

            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();

            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }

            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }

        };
    }

    // 使用给定的类型和类加载器创建一个 ServiceLoader
    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }

    // 使用给定的类型和线程上下文类加载器创建 ServiceLoader
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

    // 使用给定类型和拓展类加载器创建 ServiceLoader
    // 只会加载 JVM 已经安装的服务提供者,用户的应用程序类路径下的服务提供者将被忽略
    public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        ClassLoader prev = null;
        while (cl != null) {
            prev = cl;
            cl = cl.getParent();
        }
        return ServiceLoader.load(service, prev);
    }

    public String toString() {
        return "java.util.ServiceLoader[" + service.getName() + "]";
    }

}
  1. ServiceLoader 实现了 Iterable 接口,加载配置文件和实例化服务提供者的工作都交给了迭代器来做,这个加载器是懒加载器,只有在遍历时才会去加载,hasNext() 方法加载配置文件,next() 方法实例化服务提供者。
  2. 静态变量 PREFIX = "META-INF/services/" 规定了配置文件必须放在 META-INF/services/ 路径下。
  3. 服务提供者实例化是调用 Class.forName() 方法进行的,实例化之后,服务提供者会被缓存在 providers 有序列表中。

SPI 机制的缺陷

  • ServiceLoader 不是线程安全的,多个线程同时使用会有并发问题;
  • 不能按需加载,每次都需要通过遍历来获取,对于不想要和实例化很耗时的类,也会被实例化;
  • 获取的方式只能通过 Iterator 方式获取,不够灵活。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java SPI 机制 的相关文章

随机推荐

  • leetcode 376 摆动序列

    动态规划 class Solution public int wiggleMaxLength int nums if nums length 1 return nums length 动态规划 dp i 0 表示当前数字的子序列长度 dp
  • rabbitmq Attempting to connect to: [localhost:5672] SocketExceptio:Socket Closed

    今天使用spring cloud stream for rabbitmq启动项目报错 2019 05 03 13 22 27 350 INFO file 18160 main o s a r c CachingConnectionFacto
  • FAPI专题-8:5G FAPI接口 - 中文规范-4- P7消息格式

    作者主页 文火冰糖的硅基工坊 文火冰糖 王文兵 的博客 文火冰糖的硅基工坊 CSDN博客 本文网址 https blog csdn net HiWangWenBing article details 120645757 目录 第3章 主要的
  • 若依管理系统RuoYi-Vue(二):权限系统设计详解

    本篇文章试图讲解若依Vue系统中的权限设计原理以及实战 为什么是 试图 因为这也是摸索着理解的 不一定准 若依Vue系统中的权限管理部分的功能都集中在了系统管理菜单模块中 如下图所示 其中权限部分主要涉及到了用户管理 角色管理 菜单管理 部
  • nginx中root与alias关键字的区别

    前言 近段时间秋招上岸了 于是每天疯狂补各种分布式基础 每天都在痛苦与快乐中度过 在学习 nginx 的时候 遇到配置上的问题 root 与 alias 的区别 卡了大概三个小时 记录下来警醒自己不要再犯了 正文 在使用 进行配置时 两者没
  • CentOS yum的详细使用方法

    yum 是什么yum Yellow dog Updater Modified主要功能是更方便的添加 删除 更新RPM包 它能自动解决包的倚赖性问题 它能便于管理大量系统的更新问题 yum特点可以同时配置多个资源库 Repository 简洁
  • DirectShow播放视频步骤

    DirectShow是MicrosoftWindows平台上的流媒体架构 可以用它来方便的进行视频捕获和回放 DirectShow是基于组件对象模型 COM 下面是DirectShow播放AVI视频的代码 include
  • java项目-谷粒商城(持续更新ing)

    使用docker安装nacos 我是直接在网上找的教程 17条消息 Docker下载安装Nacos并完成持久化配置 docker nacos 下载 虫链Java Library的博客 CSDN博客 在项目中导入依赖 在 common 项目中
  • [0x7FFE1E17E050] ANOMALY: meaningless REX prefix used

    今天要记录一下糟糕的事情 遇到一个很是 cao dan 的问题 再用git时 报错了 0x7FFE1E17E050 ANOMALY meaningless REX prefix used 在cmd窗口输入 也报这个错 idea中也报错 gi
  • 云原生——云平台操作

    作者介绍 奇妙的大歪 个人名言 但行前路 不负韶华 个人简介 云计算网络运维专业人员 前言 云 云是网络 互联网的一种比喻说法 平台 即操作系统 数据库和一些中间件都可称为软件平台 云计算 使用互联网接入存储或者运行在远程服务器端的应用 数
  • 3dsMax2016卡死的一种解决办法

    可能是升级win10版本的时候 win10自带的输入法也升级了 然后3dsMax2016就卡死了 设置一下输入法的兼容性
  • Vue基础知识总结 7:插槽slot与vue导入导出

    作者简介 哪吒 CSDN2021博客之星亚军 新星计划导师 博客专家 哪吒多年工作总结 Java学习路线总结 搬砖工逆袭Java架构师 关注公众号 哪吒编程 回复1024 获取Java学习路线思维导图 大厂面试真题 加入万粉计划交流群 一起
  • 基于matlab实现信号的线性卷积与循环卷积

    系列文章目录 数字信号处理 DSP Digital Signal Process 是电子通信领域非常重要的研究方向 博主汇总了数字信号处理 DSP 中常用的经典案例分析 主要基于算法分析 MATLAB程序实现 信号图像显示 对数字信号处理的
  • python flask框架下登录注册界面_基于Flask框架如何实现用户登录功能

    form hidden tag form username class form control input lg placeholder 用户名 form password class form control input lg plac
  • dubbo解析-@EnableDubbo注解详解

    本文基于dubbo 2 7 5版本代码 文章目录 一 EnableDubbo注解功能 二 详细介绍注解引入的三个类的作用 dubbo必须配置注解 EnableDubbo 一 EnableDubbo注解功能 EnableDubbo整合了三个注
  • MetaFormer/PoolFormer学习笔记及代码

    MetaFormer PoolFormer学习笔记及代码 MetaFormer Is Actually What Y ou Need for Vision code https github com sail sg poolformer A
  • RN仿微信朋友圈图片拖拽删除

    目录 前言 初始时 渲染时 开始拖拽 拖拽中 拖拽结束 拖拽删除 参考链接 前言 之前负责的一个需求 让在RN端做仿微信朋友圈的图片删除和排序 由于经验和时间限制 就跟PM协商改为点击删除 由此欠下一个技术栈 今天是来还债的 本文基于tra
  • STM32 电机教程 16 - PMSM电机磁场定向控制原理

    前言 磁场定向控制又称矢量控制 FOC 本质上为控制定子电流的幅度和相位 使之产生的磁场和转子的磁场正交 以产生最大的扭矩 1 PMSM 的磁场定向控制 磁场定向控制 Field Oriented Control FOC 表示这样一种方法
  • 计算机网络应用层之HTTP协议

    一 什么是HTTP协议 HTTP是HyperText Transfer Protocol即超文本传输协议的缩写 是Web应用层协议之一 HTTP协议由两部分程序实现 一个客户机程序和一个服务器程序 它们运行在不同的端系统中 通过交换HTTP
  • Java SPI 机制

    文章首发于个人博客 欢迎访问关注 https www lin2j tech 什么是 SPI 机制 SPI Service Provider Interface 是 Java 内置的一种服务提供发现机制 将功能的实现交给第三方 用来拓展和替换