详解 SPI 机制

news2024/11/23 14:38:48

SPI(Service Provider Interface) 是 JDK 内置的一种服务提供发现机制:可以用来启用框架扩展和替换组件,主要用于框架中开发。例如:Dubbo、Spring、Common-Logging,JDBC 等都是采用 SPI 机制,针对同一接口采用不同的实现提供给不同的用户,从而提高了框架的扩展性

1. Java SPI 实现

Java 内置的 SPI 通过 java.util.ServiceLoader 类解析 classPath 和 jar 包的 META-INF/services/ 目录下的以接口全限定名命名的文件,并加载该文件中指定的接口实现类,以此完成调用

1.1 案例

对于智能家居系统,只要是相同品牌下的产品,连上 wifi 就能够通过手机 app 控制了,非常方便。虽然产品不断更新换代,型号更新层出不穷,但是同种家电在 app 上操作起来,功能一般都是一样的。就拿空调来说,我们在 app 上操作起来一般也就三个主要功能:开关,选模式,调节温度

假设:我现在在客厅、卧室、书房安装了 3 款不同型号的空调,并把它们都接入到了我 app 中,那么之后的操作都是相同的几个按键,简单粗暴。

问题:无论是开关还是调温,都是通过 app 去调用设备的接口罢了,那么如果不同型号的空调各写各的接口,后端 app 在开发的时候光对接接口都麻烦的要死

解决方法:我先定义一套接口规范,不管你以后什么型号的空调,都按我的规范来实现接口。以后只要我能发现你的设备,那么都可以按相同的方法来调用接口

①:定义接口

新建一个 maven 项目 aircondition-standard,定义一个接口:

public interface AirConditionService {

    // 获取型号
    String getType();

    // 开关
    void turnOnOff();

    // 调节温度
    void adjustTemperature(int temperature);

    // 模式变更
    void changeModel(int modelId);

}

用 maven 把它打成 jar 包,供后续的服务实现使用(服务提供者在项目中就可以引入这个 jar 包)

mvn clean install

有了这套规范,就保证了产品后期不管怎么更新换代,都能接入到系统来

②:服务实现

现有两个类型的空调:挂式空调(HangingType)、立式空调(VerticalType)

挂式空调: 新建一个项目 aircondition-hanging-type,并引入上述 jar:

<dependency>
    <groupId>com.zzc</groupId>
    <artifactId>aircondition-standard</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

创建服务类,并实现前面定义的接口:

public class HangingTypeAirConditionService implements AirConditionService {

    @Override
    public String getType() {
        return "HangingType";
    }

    @Override
    public void turnOnOff() {
        // TODO
        System.out.println("挂式空调开关");
    }

    @Override
    public void adjustTemperature(int temperature) {
        // TODO
        System.out.println("挂式空调调节温度");
    }

    @Override
    public void changeModel(int modelId) {
        // TODO
        System.out.println("挂式空调更换模式");
    }

}

在项目的 resources 的目录下,创建 META-INF/services目录,然后以前面定义的接口名 com.zzc.airconditionstandard.service.AirConditionService 创建文件,并在文件中写入实现类的全限定名:

com.zzc.airconditionhangingtype.service.HangingTypeAirConditionService

如下图:

在这里插入图片描述
这样,一个服务方的简单实现就搞定了,用 maven 打成 jar 包,之后就可以提供给调用方使用了

同理,我们可以再创建一个立式空调的项目 aircondition-vertical-type

public class VerticalTypeAirConditionService implements AirConditionService {

    @Override
    public String getType() {
        // TODO
        return "VerticalType";
    }

    @Override
    public void turnOnOff() {
        // TODO
        System.out.println("立式空调开关");
    }

    @Override
    public void adjustTemperature(int i) {
        // TODO
        System.out.println("立式空调调节温度");
    }

    @Override
    public void changeModel(int i) {
        // TODO
        System.out.println("立式空调更换模式");
    }

}

在项目的 resources 的目录下,创建 META-INF/services目录,然后以前面定义的接口名 com.zzc.airconditionstandard.service.AirConditionService 创建文件,并在文件中写入实现类的全限定名:

com.zzc.service.VerticalTypeAirConditionService
③:服务发现

现在两个服务提供方都实现了接口,下面关键的一步就是服务发现,这一步 Java 中的 spi 发现机制已经帮我们实现好了

创建一个新项目 aircondition-app,引入上面打好的两个 jar

<dependency>
    <groupId>com.zzc</groupId>
    <artifactId>aircondition-hanging-type</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>com.zzc</groupId>
    <artifactId>aircondition-vertical-type</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

按照上面的说法,虽然每个服务提供者对于接口都有不同的实现,但是作为调用者来说,它并不需要关心具体的实现类,我们要做的是通过接口来调用服务提供者实现的方法

下面,就是关键的服务发现环节,我们写一个方法,根据型号去调用对应空调的开关方法:

public class AirConditionApp {

    public static void main(String[] args) {
        new AirConditionApp().turnOn("VerticalType");
    }

    public void turnOn(String type){
        ServiceLoader<AirConditionService> load = ServiceLoader.load(AirConditionService.class);

        for (AirConditionService iAircondition : load) {
            System.out.println("检测到:"+iAircondition.getClass().getSimpleName());
            if (type.equals(iAircondition.getType())){
                iAircondition.turnOnOff();
            }
        }
    }
}

测试结果:

在这里插入图片描述

可以看到,测试过程中,通过定义的接口 AirConditionService 发现了两个实现类,并通过参数,调用了特定实现类的某个方法。整段代码中没有出现过具体的服务实现类,操作都是通过接口调用

1.3 JDBC 的 SPI 机制

深入理解 Java 中的 SPI 机制

2. Spring 的 SPI 机制

2.1 简介

Spring SPI 配置文件是一个固定的文件 - META-INF/spring.factories,功能上和 JDK 的类似,每个接口可以有多个扩展实现,使用起来非常简单:

//获取所有factories文件中配置的LoggingSystemFactory
List<LoggingSystemFactory>> factories = SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);

Spring 也是支持 ClassPath 中存在多个 spring.factories 文件的,加载时会按照 classpath 的顺序依次加载这些 spring.factories 文件,添加到一个 ArrayList 中。由于没有别名,所以也没有去重的概念,有多少就添加多少

但由于 Spring 的 SPI 主要用在 Spring Boot 中,而 Spring Boot 中的 ClassLoader 会优先加载项目中的文件,而不是依赖包中的文件。所以如果在你的项目中定义个 spring.factories 文件,那么你项目中的文件会被第一个加载,得到的 Factories中,项目中 spring.factories 里配置的那个实现类也会排在第一个

如果我们要扩展某个接口的话,只需要在你的项目(spring boot)里新建一个 META-INF/spring.factories 文件,只添加你要的那个配置,不要完整的复制一遍 Spring Boot 的 spring.factories 文件然后修改

2.2 案例

①:定义接口:HelloService

public interface HelloService {
    void sayHello();
}

②:其实现类:HelloServiceImpl1

public class HelloServiceImpl1 implements HelloService {

    @Override
    public void sayHello() {
        System.out.println("Hello World from HelloServiceImpl1!");
    }
}

HelloServiceImpl2

public class HelloServiceImpl2 implements HelloService {

    @Override
    public void sayHello() {
        System.out.println("Hello World from HelloServiceImpl2!");
    }
}

③:spring.factories 文件:resources/META-INF 路径下

com.zzc.service.HelloService=\
com.zzc.service.impl.HelloServiceImpl1,\
com.zzc.service.impl.HelloServiceImpl2

idea当中如何创建*.factories文件

④:HelloServiceLoader

public class HelloServiceLoader {

    public static void main(String[] args) {
        // 使用Spring SPI机制动态加载HelloService实现类
        List<HelloService> loader = SpringFactoriesLoader.loadFactories(HelloService.class, Thread.currentThread().getContextClassLoader());
        // 遍历所有实现类并调用sayHello方法
        for (HelloService helloService : loader) {
            helloService.sayHello();
        }
    }
}

2.3 实现原理

查看 SpringFactoriesLoader 类源码:

public final class SpringFactoriesLoader {
    public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
    private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);
    static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap();

    private SpringFactoriesLoader() {
    }
	
	// 加载并实例化给定类型的工厂实现
    public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
        List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
        List<T> result = new ArrayList(factoryImplementationNames.size());
        Iterator var5 = factoryImplementationNames.iterator();
        while(var5.hasNext()) {
            String factoryImplementationName = (String)var5.next();
            result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
        }
        return result;
    }
	
    public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
        String factoryTypeName = factoryType.getName();
        return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
    }
	
	// 获取所有 jar 包中 META-INF/spring.factories 文件路径,整合 factoryClass 类型的实现类名称,获取到实现类的全类名称后进行类的实例化操作
    private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
        Map<String, List<String>> result = (Map)cache.get(classLoader);
        if (result != null) {
            return result;
        } else {
            HashMap result = new HashMap();

            try {
                Enumeration urls = classLoader.getResources("META-INF/spring.factories");
                while(urls.hasMoreElements()) {
                    URL url = (URL)urls.nextElement();
                    UrlResource resource = new UrlResource(url);
                    Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                    Iterator var6 = properties.entrySet().iterator();

                    while(var6.hasNext()) {
                        Entry<?, ?> entry = (Entry)var6.next();
                        String factoryTypeName = ((String)entry.getKey()).trim();
                        String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                        String[] var10 = factoryImplementationNames;
                        int var11 = factoryImplementationNames.length;

                        for(int var12 = 0; var12 < var11; ++var12) {
                            String factoryImplementationName = var10[var12];
                            ((List)result.computeIfAbsent(factoryTypeName, (key) -> {
                                return new ArrayList();
                            })).add(factoryImplementationName.trim());
                        }
                    }
                }

                result.replaceAll((factoryType, implementations) -> {
                    return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
                });
                cache.put(classLoader, result);
                return result;
            } catch (IOException var14) {
                throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
            }
        }
    }
	
	// 实例化 Bean:通过反射来实现对应的初始化
    private static <T> T instantiateFactory(String factoryImplementationName, Class<T> factoryType, ClassLoader classLoader) {
        Class<?> factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader);
        return ReflectionUtils.accessibleConstructor(factoryImplementationClass, new Class[0]).newInstance();
    }
}

3. Common-Logging 的 SPI

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

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

相关文章

RTOS系统移植

一、完成系统移植 系统移植上官网寻找合适的系统包&#xff0c;下载后将文件移植入工程文件 二、创建任务句柄、内核对象句柄&#xff08;信号量&#xff0c;消息队列&#xff0c;事件标志组&#xff0c;软件定时器&#xff09;、声明全局变量、声明函数 三、创建主函数&#…

Stable Diffusion绘画 |,IP角色多视图生成技巧(附插件模型)

在游戏设计、小说推文、角色设计里面&#xff0c;很多场景都运用到IP角色的多视图。 人物角色多视图 第1步&#xff0c;输入提示词&#xff1a; 第2步&#xff0c;由于要在同一张图片中生成多角度的并排展示&#xff0c;需要修改图片的分辨率&#xff08;尤其是宽度&#xff…

开源问答类知识付费网站源码系统 带完整的安装代码包以及搭建部署教程

系统概述 近年来&#xff0c;随着互联网的飞速发展&#xff0c;知识付费市场呈现出爆炸式增长。各大知识付费平台如雨后春笋般涌现&#xff0c;涵盖了从教育、科技到生活娱乐等各个领域。用户通过付费获取高质量的知识内容&#xff0c;而内容创作者则通过分享知识获得经济回报…

大模型应用探讨,免费AI写作、一键PPT、免费PDF百种应用、与AI对话

大模型应用平台知识普及, 应用可见评论区 我们生活在一个充满无限可能的数字时代&#xff0c;人工智能技术正在推动着各种创新的边界。大模型应用平台一般包含以下功能。 ## 1. 一键生成论文 写作是学生、研究人员和职场人士都无法避免的任务。大模型应用平台拥有强大的文本生…

如何让算法拥有“记忆”?一文读懂记忆化搜索

✨✨✨学习的道路很枯燥&#xff0c;希望我们能并肩走下来! 文章目录 目录 文章目录 前言 一 什么是记忆化搜索 二 相关题目练习 2.1 斐波那契数&#xff08;详解记忆化搜索&#xff09; ​编辑 解法一&#xff08;递归&#xff09;&#xff1a; 解法二&#xff08;记…

全面整理人工智能(AI)学习路线图及资源推荐,非常详细收藏我这一篇就够了

在人工智能&#xff08;AI&#xff09;飞速发展的今天&#xff0c;掌握AI技术已经成为了许多高校研究者和职场人士的必备技能。从深度学习到强化学习&#xff0c;从大模型训练到实际应用&#xff0c;AI技术的广度和深度不断拓展。作为一名AI学习者&#xff0c;面对浩瀚的知识海…

kafka的成神秘籍(java)

kafka的成神秘籍 kafka的简介 ​ Kafka 最初是由Linkedin 即领英公司基于Scala和 Java语言开发的分布式消息发布-订阅系统&#xff0c;现已捐献给Apache软件基金会。Kafka 最被广为人知的是作为一个 消息队列(mq)系统存在&#xff0c;而事实上kafka已然成为一个流行的分布式流…

【吊打面试官系列-MySQL面试题】试述视图的优点?

大家好&#xff0c;我是锋哥。今天分享关于【试述视图的优点&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; 试述视图的优点&#xff1f; (1) 视图能够简化用户的操作 (2) 视图使用户能以多种角度看待同一数据&#xff1b; (3) 视图为数据库提供了一定程度的…

8年JAVA逆袭转AI之路!成功拿下offer

前段时间有一个粉丝投稿&#xff0c;他是8年老Java程序员了&#xff0c;每天两小时的碎片化学习时间&#xff0c;不仅没有陷入程序员的年龄恐慌&#xff0c;还拿到了目前薪资翻倍的offer 问到他是什么让他坚持学了6个月&#xff0c;他用了华为总裁任正非说的“今后职场上只有…

Nginx03-使用

零、文章目录 Nginx03-使用 1、Nginx服务器启停命令 对于 Nginx 的启停在 Linux 系统中也有很多种方式&#xff0c;我们介绍两种方式&#xff1a; Nginx信号控制Nginx命令行控制 &#xff08;1&#xff09;Nginx信号控制 查看Nginx 中的 master 和 worker 进程 [rootloc…

计算机进制之间的关系

计算机中常见的进制 十进制、二进制、十六进制、八进制之间对照表 进制之间的转换 通过上面的十进制对应二进制进位的表示&#xff1a; 当二进制产生增加位数时&#xff0c;相对应十进制数为2、4、8、16、32、64、128&#xff0c;也被称为二进制的位权&#xff0c;根据规律可知…

linux中缓存,在kafka上应用总结

linux中的缓存 页缓存 pagecatch&#xff08;读缓存用于提供快速读&#xff09;块缓存&#xff08;用于提供其他设备快速写&#xff09;当对读缓存读的时候&#xff0c;修改了读的数据&#xff0c;页缓存就会被标记为脏数据&#xff0c;等到写的时候它会向块缓存同步数据&…

关于7zip解压缩的下载和使用

我们有的时候下载软件&#xff0c;后缀是 ".exe" 或者 “.zip”&#xff0c;".7z"等&#xff0c;".exe"文件还好&#xff0c;打开就能进行下载&#xff0c;但是“.zip”&#xff0c;".7z“等就需要用解压缩软件进行解压了。 今天介绍的解…

No.11 笔记 | PHP学习指南:从函数到面向对象概览

一、PHP函数&#xff1a;代码复用的艺术 1. 函数的本质与魅力 函数是PHP的核心力量&#xff0c;分为内置函数和自定义函数函数名应当简洁明了&#xff0c;以字母或下划线开头 2. 函数的构成要素 function 关键字&#xff1a;函数的开始标志函数名&#xff1a;您的函数的独特…

【Git原理与使用】远程操作标签管理

远程操作&&标签管理 1.理解分布式版本控制系统2.新建远程仓库3.克隆远程仓库4.向远程仓库推送5.拉取远程仓库6.配置 Git7.配置命令别名8.标签管理8.1创建标签8.2操作标签 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496;…

把当抠门程序员,遇到了免费AI大模型

这篇想和大家分享一下&#xff0c;一个抠门的程序员和一个免费的AI大模型的故事。 “抠门程序员<–>免费大模型”&#xff0c;让我看看&#xff0c;能不能擦出马内的火花。 故事的开始 不知道有没有程序员和我一样&#xff0c;付费的东西&#xff0c;都会省着点开。什…

远程访问服务是什么?如何通过节点小宝远程访问办公室电脑?

在家办公若能各安其位、高效完成任务&#xff0c;实为美事。然而&#xff0c;现实往往不尽如人意&#xff0c;偶尔需用到办公室电脑上的资料&#xff0c;这时便需依赖远程访问服务的助力。那么&#xff0c;远程访问服务究竟是何方神圣&#xff1f;又该如何借助节点小宝实现对办…

解锁空间距离计算的多种方式-含前端、空间数据库、后端

目录 前言 一、空间数据库求解 1、PostGIS实现 二、GIS前端组件求解 1、Leaflet.js距离测算 2、Turf.js前端计算 三、后台距离计算生成 1、欧式距离 2、Haversice球面距离 3、GeoTools距离计算 4、Gdal距离生成 5、geodesy距离计算 四、成果与生成对比 1、Java不…

CSRF | POST 型 CSRF 漏洞攻击

关注这个漏洞的其他相关笔记&#xff1a;CSRF 漏洞 - 学习手册-CSDN博客 0x01&#xff1a;POST 型 CSRF 漏洞攻击 —— 理论篇 POST 型 CSRF 漏洞是指攻击者通过构造恶意的 HTTP POST 请求&#xff0c;利用用户的登录状态&#xff0c;在用户不知情的情况下&#xff0c;诱使浏览…

Mythical Beings:Web3游戏如何平衡创造内容、关注度与实现盈利的不可能三角

Web3游戏自其诞生以来&#xff0c;以去中心化和独特的代币经济体系迅速引起关注。然而&#xff0c;如何在创造内容、吸引用户和实现盈利之间达到平衡&#xff0c;始终是Web3游戏面临的核心挑战。Mythical Beings作为一款Web3卡牌游戏&#xff0c;通过创新设计和独特机制&#x…