【Java的SPI机制】Java SPI机制:实现灵活的服务扩展

在Java开发中,SPI(Service Provider Interface,服务提供者接口)机制是一种重要的设计模式,它允许在运行时动态地插入或更换组件实现,从而实现框架或库的扩展点。本文将深入浅出地介绍Java SPI机制,帮助读者理解其原理和应用。

一、什么是Java SPI?

SPI,全称Service Provider Interface,是一种服务发现机制。它用于实现框架或库的扩展点,允许在运行时动态地加载实现了某个接口或抽象类的具体实现类。SPI提供了一种框架来发现和加载服务实现,使得软件模块能够灵活地选择和使用不同的服务提供商。

SPI的核心思想是将接口的定义和实现分离,通过配置文件的形式来动态加载实现类,从而实现解耦。这种方式使得服务的消费者不需要直接依赖于具体的服务实现,符合面向对象设计原则中的“对扩展开放,对修改关闭”原则(Open/Closed Principle)。

二、Java SPI的组成

一个标准的Java SPI由三个主要组件构成:

  1. Service:一个公开的接口或抽象类,定义了一个抽象的功能模块。
  2. Service Provider:Service接口的一个实现类,提供了具体的服务实现。
  3. ServiceLoader:SPI机制中的核心组件,负责在运行时发现并加载Service Provider。
三、Java SPI的运行流程

Java SPI的运行流程如下:

  1. 定义服务接口:首先定义一个服务接口,该接口描述了服务的行为和方法。
  2. 提供服务实现:然后,一个或多个服务提供者实现该服务接口。
  3. 创建配置文件:在META-INF/services目录下创建一个以服务接口全限定名命名的文件,文件内容为具体实现类的全限定名。
  4. 加载服务实现:通过ServiceLoader类动态加载并实例化服务提供者的实现类。
四、Java SPI的示例

下面通过一个简单的java自定义序列化器来演示Java SPI的使用:

定义一个接口

/*** 自定义序列化器** @author yuwei* @date 14:00 2024/10/3*/
public interface Serializer {public <T>  byte[] serialize(T obj)  throws IOException;public <T> T deserialize(byte[] bytes,Class<T> classType)  throws IOException ;
}

创建一个服务实现类


import java.io.*;/*** JDK 序列化器** @author yuwei* @date 13:20 2024/10/3*/
public class JdkSerializer implements Serializer {@Overridepublic <T> byte[] serialize(T obj) throws IOException {ByteArrayOutputStream out = new ByteArrayOutputStream();ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);objectOutputStream.writeObject(obj);objectOutputStream.close();return out.toByteArray();};@Overridepublic <T> T deserialize(byte[] bytes,Class<T> type) throws IOException {ByteArrayInputStream in = new ByteArrayInputStream(bytes);ObjectInputStream objectInputStream = new ObjectInputStream(in);try {return (T)objectInputStream.readObject();}catch(ClassNotFoundException e){throw new RuntimeException(e);}finally {objectInputStream.close();}}
}

 创建配置文件

配置文件的路径可以自定义,只要在resources/META-INF目录下就可以

 创建加载服务实现类,程序运行时动态加载配置文件中指定的实现类


/*** SPI 加载器(支持键值对映射)*/
@Slf4j
public class SpiLoader {/*** 存储已加载的类:接口名 =>(key => 实现类)*/private static Map<String, Map<String, Class<?>>> loaderMap = new ConcurrentHashMap<>();/*** 对象实例缓存(避免重复 new),类路径 => 对象实例,单例模式*/private static Map<String, Object> instanceCache = new ConcurrentHashMap<>();/*** 用户自定义 SPI 目录*/private static final String RPC_CUSTOM_SPI_DIR = "META-INF/services/";/*** 扫描路径*/private static final String[] SCAN_DIRS = new String[]{RPC_SYSTEM_SPI_DIR, RPC_CUSTOM_SPI_DIR};/*** 动态加载的类列表*/private static final List<Class<?>> LOAD_CLASS_LIST = Arrays.asList(Serializer.class);/*** 加载所有类型*/public static void loadAll() {log.info("加载所有 SPI");for (Class<?> aClass : LOAD_CLASS_LIST) {load(aClass);}}/*** 获取某个接口的实例** @param tClass* @param key* @param <T>* @return*/public static <T> T getInstance(Class<?> tClass, String key) {String tClassName = tClass.getName();Map<String, Class<?>> keyClassMap = loaderMap.get(tClassName);if (keyClassMap == null) {throw new RuntimeException(String.format("SpiLoader 未加载 %s 类型", tClassName));}if (!keyClassMap.containsKey(key)) {throw new RuntimeException(String.format("SpiLoader 的 %s 不存在 key=%s 的类型", tClassName, key));}// 获取到要加载的实现类型Class<?> implClass = keyClassMap.get(key);// 从实例缓存中加载指定类型的实例String implClassName = implClass.getName();if (!instanceCache.containsKey(implClassName)) {try {instanceCache.put(implClassName, implClass.newInstance());} catch (InstantiationException | IllegalAccessException e) {String errorMsg = String.format("%s 类实例化失败", implClassName);throw new RuntimeException(errorMsg, e);}}return (T) instanceCache.get(implClassName);}/*** 加载某个类型** @param loadClass* @throws IOException*/public static Map<String, Class<?>> load(Class<?> loadClass) {log.info("加载类型为 {} 的 SPI", loadClass.getName());// 扫描路径,用户自定义的 SPI 优先级高于系统 SPIMap<String, Class<?>> keyClassMap = new HashMap<>();for (String scanDir : SCAN_DIRS) {List<URL> resources = ResourceUtil.getResources(scanDir + loadClass.getName());// 读取每个资源文件for (URL resource : resources) {try {InputStreamReader inputStreamReader = new InputStreamReader(resource.openStream());BufferedReader bufferedReader = new BufferedReader(inputStreamReader);String line;while ((line = bufferedReader.readLine()) != null) {String[] strArray = line.split("=");if (strArray.length > 1) {String key = strArray[0];String className = strArray[1];keyClassMap.put(key, Class.forName(className));}}} catch (Exception e) {log.error("spi resource load error", e);}}}loaderMap.put(loadClass.getName(), keyClassMap);return keyClassMap;}
}

通过SPILoader加载实现类

SpiLoader.getInstance(Serializer.class, key);
五、Java SPI的应用场景

Java SPI机制广泛应用于各种Java框架和库中,以下是一些常见的应用场景:

  1. 日志框架:如SLF4J,通过SPI机制加载不同的日志实现(如Logback、Log4j)。
  2. JDBC:Java数据库连接(JDBC)使用SPI机制加载不同的数据库驱动程序。
  3. Servlet容器:如Tomcat、Jetty,通过SPI机制加载和扩展不同的Servlet实现。
六、总结

Java SPI机制是一种灵活的服务扩展方式,它通过将接口的定义和实现分离,实现了服务的动态加载和替换。这种机制不仅提高了代码的灵活性和可扩展性,还降低了系统的耦合度。通过理解和应用Java SPI机制,开发者可以更好地设计和实现模块化、可扩展的Java系统。

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

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

相关文章

JAVA开源项目 旅游管理系统 计算机毕业设计

本文项目编号 T 063 &#xff0c;文末自助获取源码 \color{red}{T063&#xff0c;文末自助获取源码} T063&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析5.4 用例设计 六、核…

TypeScript 封装 Axios 1.7.7

随着Axios版本的不同&#xff0c;类型也在改变&#xff0c;以后怎么写类型&#xff1f; yarn add axios1. 封装Axios 将Axios封装成一个类&#xff0c;同时重新封装request方法 重新封装request有几个好处&#xff1a; 所有的请求将从我们定义的requet请求中发送&#xff…

Vue3实现动态菜单功能

文章目录 0.效果演示1.搭建Vue3项目1.1 vite 脚手架创建 Vue3 项目1.2 设置文件别名1.3 安装配置 element-plus1.4 安装配置路由2.登录页面3.后台管理页面3.1 搭建后台框架3.2 左侧菜单栏3.3 header 用户信息3.4 主要内容3.5 footer4.配置静态路由5.记录激活菜单5.1 el-menu 绑…

卸载apt-get 安装的PostgreSQL版本

文章目录 卸载apt-get 安装的PostgreSQL版本查找已安装的PostgreSQL包卸载PostgreSQL&#xff1a;检查并删除残留文件验证卸载 卸载apt-get 安装的PostgreSQL版本 卸载通过apt-get安装的PostgreSQL 就版本&#xff0c;可以按照以下步骤进行。 查找已安装的PostgreSQL包 在卸…

信号处理快速傅里叶变换(FFT)的学习

FFT是离散傅立叶变换的快速算法&#xff0c;可以将一个信号变换到频域。有些信号在时域上是很难看出什么特征的&#xff0c;但是如果变换到频域之后&#xff0c;就很容易看出特征了。这就是很多信号分析采用FFT变换的原因。另外&#xff0c;FFT可以将一个信号的频谱提取出来&am…

StarRocks 中如何做到查询超时(QueryTimeout)

背景 本文基于 StarRocks 3.1.7 主要是分析以下两种超时设置的方式: SESSION 级别 SET query_timeout 10;SELECT sleep(20);SQL 级别 select /* SET_VAR(query_timeout10) */ sleep(20); 通过本文的分析大致可以了解到在Starrocks的FE端是如何进行Command的交互以及数据流走…

Java Web 之 Cookie 详解

在 JavaWeb 开发中&#xff0c;Cookie 就像网站给浏览器贴的小纸条&#xff0c;用于记录一些用户信息或状态&#xff0c;方便下次访问时识别用户身份或进行个性化服务。 也可以这么理解&#xff1a; 场景一&#xff1a;想象一下&#xff0c;你去一家咖啡店&#xff0c;店员认…

webpack信息泄露

先看看webpack中文网给出的解释 webpack 是一个模块打包器。它的主要目标是将 JavaScript 文件打包在一起,打包后的文件用于在浏览器中使用,但它也能够胜任转换、打包或包裹任何资源。 如果未正确配置&#xff0c;会生成一个.map文件&#xff0c;它包含了原始JavaScript代码的映…

VPN简述

文章目录 VPNVPN基础VPN类型 VPN VPN隧道安全 VPN基础 背景&#xff1a; 在网络传输中&#xff0c;绝大部分数据内容都是明文传输&#xff0c;存在很多安全隐患&#xff08;窃听、篡改、冒充&#xff09; 总部、分公司、办事处、出差人员、合作单位等需要访问总部网络资源 Vi…

富格林:警悟可信经验安全投资

富格林指出&#xff0c;黄金具有不错的投资价值&#xff0c;一直以来备受投资者的喜爱&#xff0c;近年来大家也纷纷加入现货黄金市场为己增值财富。但是要为投资安全护航的前提&#xff0c;是需要投资者使用合适可信的方法以及掌握相对应的投资技巧。下面富格林将总结以下可信…

【数学分析笔记】第4章第4节 复合函数求导法则及其应用(1)

4. 微分 4.4 复合函数求导法则及其应用 4.4.1 复合函数求导法则 【定理4.4.1】 u g ( x ) ug(x) ug(x)在 x x 0 xx_0 xx0​可导&#xff0c; g ( x 0 ) u ( 0 ) g(x_0)u(0) g(x0​)u(0)&#xff0c; y f ( u ) yf(u) yf(u)在 u u 0 uu_0 uu0​可导&#xff0c;则 y f …

SpringBoot+Redis+RabbitMQ完成增删改查

各部分分工职责 RabbitMQ负责添加、修改、删除的异步操作 Redis负责数据的缓存 RabbitMQ里面角色职责简单描述 RabbitMQ里面有几个角色要先分清以及他们的对应关系&#xff1a; 交换机、队列、路由键 交换机和队列是一对多 队列和路由键是多对多 然后就是消息的发送者&…

课设实验-数据结构-线性表-手机销售

题目&#xff1a; 代码&#xff1a; #include<stdio.h> #include<string.h> #define MaxSize 10 //定义顺序表最大长度 //定义手机结构体类型 typedef struct {char PMod[10];//手机型号int PPri;//价格int PNum;//库存量 }PhoType; //手机类型 //记录手机的顺序…

【HTTP(3)】(状态码,https)

【认识状态码】 状态码最重要的目的&#xff0c;就是反馈给浏览器:这次请求是否成功&#xff0c;若失败&#xff0c;则出现失败原因 常见状态码: 200:OK&#xff0c;表示成功 404:Not Found&#xff0c;浏览器访问的资源在服务器上没有找到 403:Forbidden&#xff0c;访问被…

springboot系列--web相关知识探索三

一、前言 web相关知识探索二中研究了请求是如何映射到具体接口&#xff08;方法&#xff09;中的&#xff0c;本次文章主要研究请求中所带的参数是如何映射到接口参数中的&#xff0c;也即请求参数如何与接口参数绑定。主要有四种、分别是注解方式、Servlet API方式、复杂参数、…

【案例】距离限制模型透明

开发平台&#xff1a;Unity 2023 开发工具&#xff1a;Unity ShaderGraph   一、效果展示 二、路线图 三、案例分析 核心思路&#xff1a;计算算式&#xff1a;透明值 实际距离 / 最大距离 &#xff08;实际距离 ≤ 最大距离&#xff09;   3.1 说明 | 改变 Alpha 值 在 …

计算机组成原理之无符号整数的表示和运算

无符号整数的表示 无符号整数的表示&#xff1a;无符号整数直接使用其二进制形式表示&#xff0c;所有位都是数值位&#xff0c;没有符号位。例如&#xff0c;一个8位的无符号整数可以表示的范围是从0&#xff08;00000000&#xff09;到255&#xff08;11111111&#xff09;。…

stm32f103调试,程序与定时器同步设置

在调试定时器相关代码时&#xff0c;注意到定时器的中断位总是置1&#xff0c;怀疑代码有问题&#xff0c;经过增大定时器的中断时间&#xff0c;发现定时器与代码调试并不同步&#xff0c;这一点对于调试涉及定时器的代码是非常不利的&#xff0c;这里给出keil调试stm32使定时…

自用Proteus(8.15)常用元器件图示和功能介绍(持续更新...)

文章目录 一、 前言二、新建工程&#xff08;以51单片机流水灯为例&#xff09;2.1 打开软件2.2 建立新工程2.3 创建原理图2.4 不创建PCB布版设计2.5 创建成功2.6 添加元器件2.7 原理图放置完成2.8 编写程序&#xff0c;进行仿真2.9 仿真 三、常用元器件图示和功能介绍3.1 元件…

【回眸】Tessy 单元测试软件使用指南(四)常见报错及解决方案与批量初始化的经验

前言 分析时Tessy的报错 1.fatal error: Tricore/Compilers/Compilers.h: No such file or directory 2.error: #error "Compiler unsupported" 3.warning: invalid suffix on literal;C11 requires a space between literal and string macro 4.error: unknown…