Java SPI 机制

news2024/10/5 18:26:59

文章首发于个人博客,欢迎访问关注: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 方式获取,不够灵活。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/954877.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ESB(Enterprise Service Bus,即企业服务总线)

以前用过部分功能&#xff0c;但是没有很好地去理解过。 ESB&#xff08;Enterprise Service Bus&#xff0c;即企业服务总线&#xff09;是传统中间件技术与XML、Web服务等技术结合的产物。ESB提供了网络中最基本的连接中枢&#xff0c;是构筑企业神经系统的必要元素。 企业服…

【Bug】Ubuntu 有线设置打不开无反应

前言&#xff1a; 突然有线设置就没法启用了&#xff0c;但是能联网&#xff0c;能查看ip 解决&#xff1a; 最后安装了一个新的依赖包&#xff1a; sudo apt install gnome-control-center 然后就可以了 还有一个方法&#xff0c;没试过&#xff0c;但感觉有点道理的&#…

《向量数据库》——为何向量数据库对大模型LLM很重要?

当您浏览Twitter、LinkedIn或新闻源上的时间轴时,可能会看到一些关于聊天机器人、LLM和GPT的内容。因为每周都有新的LLM发布,很多人都在谈论LLM。 我们目前置身于一场人工智能革命,许多新应用都依赖于向量嵌入。不妨让我们更多地了解向量数据库以及为什么它们对LLM很重要。…

【UIPickerView-UIDatePicker-应用程序对象 Objective-C语言】

一、今天我们来学习三个东西 1.UIPickerView-UIDatePicker-应用程序对象 1.首先,来看数据选择控件 数据选择控件, 大家对这个数据选择控件,是怎么理解的, 1)数据选择控件,首先,是不是得有数据, 2)然后呢,你还得让用户能够选择, 3)最后,你还得是一个控件儿 那…

MySQL 数据库常用命令大全(详细)

文章目录 1. MySQL命令2. MySQL基础命令3. MySQL命令简介4. MySQL常用命令4.1 MySQL准备篇4.1.1 启动和停止MySQL服务4.1.2 修改MySQL账户密码4.1.3 MySQL的登陆和退出4.1.4 查看MySQL版本 4.2 DDL篇&#xff08;数据定义&#xff09;4.2.1 查询数据库4.2.2 创建数据库4.2.3 使…

Python UDP编程

前面我们讲了 TCP 编程&#xff0c;我们知道 TCP 可以建立可靠连接&#xff0c;并且通信双方都可以以流的形式发送数据。本文我们再来介绍另一个常用的协议--UDP。相对TCP&#xff0c;UDP则是面向无连接的协议。 UDP 协议 我们来看 UDP 的定义&#xff1a; UDP 协议&#xff…

ABB PCD231B101励磁控制模块

电磁励磁控制&#xff1a; PCD231B101 模块专门设计用于电磁励磁设备的控制&#xff0c;以确保发电机的励磁电流和电压维持在合适的水平。 多通道控制&#xff1a; 这种模块通常具有多个控制通道&#xff0c;可用于同时监测和控制多台电力发电机。 通讯接口&#xff1a; PCD2…

AI机器视觉赋能电池缺陷检测,深眸科技助力新能源行业规模化发展

新产业周期下&#xff0c;新能源行业风口已至&#xff0c;现代社会对于新能源电池产品需求量加大&#xff0c;对产品的质量安全也更加重视。当前&#xff0c;传统的检测方法已经不能满足新能源电池行业的发展&#xff0c;越来越多的厂商开始应用创新机器视觉技术与产品于生产环…

受老板器重的项目经理都是这样工作的

大家好&#xff0c;我是老原。 当了领导才明白&#xff0c;那些优秀的人都一个样。 “平庸的人各有各的平庸&#xff0c;优秀的人基本都一样” 作为普通员工&#xff0c;身边的内卷的、单纯的、摸鱼的、斤斤计较的、慷慨无私的……各种各样的都有&#xff0c;有时候聚在一起…

安防监控视频平台EasyCVR视频汇聚平台定制项目增加AI智能算法详细介绍

安防视频集中存储EasyCVR视频汇聚平台&#xff0c;可支持海量视频的轻量化接入与汇聚管理。平台能提供视频存储磁盘阵列、视频监控直播、视频轮播、视频录像、云存储、回放与检索、智能告警、服务器集群、语音对讲、云台控制、电子地图、平台级联、H.265自动转码等功能。为了便…

无涯教程-Android Online Test函数

Android在线测试模拟了真正的在线认证考试。您将看到基于 Android概念的多项选择题(MCQ),将为您提供四个options。您将为该问题选择最合适的答案,然后继续进行下一个问题,而不会浪费时间。完成完整的考试后,您将获得在线考试分数。 总问题数-20 最长时间-20分钟 Start Test …

《信息系统项目管理师教程(第4版)》第15章 项目风险管理 知识点汇总

文章只对常见考点进行整理&#xff0c;有关项目风险管理的完整知识还请参照教程。 风险基础知识 1、风险的属性 随机性相对性可变性 2.风险的分类 按后果分&#xff1a;纯粹风险、投机风险&#xff0c;纯粹风险和 投机风险在一定条件下可以互相转化 按可预测性分&#xff…

23 Linux高级篇-Linux内核介绍内核升级

23 Linux高级篇-Linux内核介绍&内核升级 文章目录 23 Linux高级篇-Linux内核介绍&内核升级23.1 linux-0.01内核介绍23.1.1 为什么要阅读Linux内核&#xff1f;23.1.2 下载linux-0.01内核源码23.1.3 linux-0.01内核介绍 23.3 Linux内核升级23.3.1 最新版内核介绍23.3.2 …

AMEYA360:ROHM开发出适用于条码标签打印应用、超快打印速度的热敏打印头

AMEYA360&#xff1a;ROHM开发出适用于条码标签打印应用、超快打印速度的热敏打印头 全球知名半导体制造商ROHM(总部位于日本京都市)新推出两款高可靠性高速热敏打印头 “TE2004-QP1W00A(203dpi)”和“TE3004-TP1W00A(300dpi)”&#xff0c;新产品非常适用于物流和库存管理等领…

基础论文学习(6)——BeiT

BEiT 是把 BERT 模型成功用在 image 领域的首创&#xff0c;也是一种自监督训练的形式&#xff0c;所以取名为视觉Transformer的BERT预训练模型。这个工作用一种巧妙的办法把 BERT 的训练思想成功用在了 image 任务中。 BERT&#xff1a;Bidiractional(双向) Encoder Represen…

助力森林火情烟雾检测预警,基于YOLOv5全系列模型[n/s/m/l/x]开发构建无人机航拍场景下的森林火情检测识别系统

森林防火一直是非常重要的事情&#xff0c;火情的早发现早预警就能及早扑灭&#xff0c;对社会安全有着重要的意义&#xff0c;近些年来随着AI技术的快速发展&#xff0c;AI与各行各业有了很多成功的合作案例&#xff0c;这里主要的思想就是在无人机航拍视角的场景构想下开发构…

Vue2项目练手——通用后台管理项目第三节

Vue2项目练手——通用后台管理项目 首页组件布局个人信息展示使用的组件App.vueHome.vue 列表信息展示使用的组件Home.vue 订单统计Home.vue 数据的请求axios的基本使用二次封装文件目录src/api/index.jssrc/utils/request.jsHome.vue 首页组件布局 个人信息展示 使用的组件 …

Doris(六)--通过 Canal 同步数据到 Doris 中

pre 开启 mysql Binlog 网上有众多方法&#xff0c;自行百度。 查询是否成功&#xff0c;在 mysql 客户端输入 show BINARY LOGS; 出现如下提示&#xff0c;即表示 big log 正常开启。 1&#xff0c;下载 canal 服务端 传送门 注意&#xff1a;下载 canal.deployer-xxx …

惹人喜爱的朋友圈背景图

分享一波可爱喜庆的朋友圈背景图&#xff0c;快来看看有没有你喜欢的吧~ ​

Flutter 安装教程 + 运行教程

1.下载依赖 https://flutter.cn/docs/get-started/install/windows 解压完后根据自己的位置放置&#xff0c;如&#xff08;D:\flutter&#xff09; 注意 请勿将 Flutter 有特殊字符或空格的路径下。 请勿将 Flutter 安装在需要高权限的文件夹内&#xff0c;例如 C:\Program …