【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 {
    @Override
    public <T> byte[] serialize(T obj) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);
        objectOutputStream.writeObject(obj);
        objectOutputStream.close();
        return out.toByteArray();
    };
    @Override
    public <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 优先级高于系统 SPI
        Map<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系统。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

08_OpenCV文字图片绘制

import cv2 import numpy as npimg cv2.imread(image0.jpg,1) font cv2.FONT_HERSHEY_SIMPLEXcv2.rectangle(img,(500,400),(200,100),(0,255,0),20) # 1 dst 2 文字内容 3 坐标 4 5 字体大小 6 color 7 粗细 8 line type cv2.putText(img,flower,(200,50),font,1,(0,0,250)…

【AI学习】Mamba学习(二):线性注意力

上一篇《Mamba学习&#xff08;一&#xff09;&#xff1a;总体架构》提到&#xff0c;Transformer 模型的主要缺点是&#xff1a;自注意力机制的计算量会随着上下文长度的增加呈平方级增长。所以&#xff0c;许多次二次时间架构&#xff08;指一个函数或算法的增长速度小于二次…

国外电商系统开发-运维系统批量添加服务器

您可以把您准备的txt文件&#xff0c;安装要求的格式&#xff0c;复制粘贴到里面就可以了。注意格式&#xff01; 如果是“#” 开头的&#xff0c;则表示注释&#xff01;

【Qt】控件概述 (1)—— Widget属性

控件概述 1. QWidget核心属性1.1核心属性概述1.2 enable1.3 geometry——窗口坐标1.4 window frame的影响1.4 windowTitle——窗口标题1.5 windowIcon——窗口图标1.6 windowOpacity——透明度设置1.7 cursor——光标设置1.8 font——字体设置1.9 toolTip——鼠标悬停提示设置1…

《安富莱嵌入式周报》第343期:雷电USB4开源示波器正式发布,卓越的模拟前端低噪便携示波器,自带100W电源的便携智能烙铁,NASA航空航天锂电池设计

周报汇总地址&#xff1a;嵌入式周报 - uCOS & uCGUI & emWin & embOS & TouchGFX & ThreadX - 硬汉嵌入式论坛 - Powered by Discuz! 更新一期视频教程 【授人以渔】CMSIS-RTOS V2封装层专题视频&#xff0c;一期视频将常用配置和用法梳理清楚&#xff0…

鸿蒙harmonyos next flutter通信之MethodChannel获取设备信息

本文将通过MethodChannel获取设备信息&#xff0c;以此来演练MethodChannel用法。 建立channel flutter代码&#xff1a; MethodChannel methodChannel MethodChannel("com.xmg.test"); ohos代码&#xff1a; private channel: MethodChannel | null nullthis.c…

openpnp - 图像传送方向要在高级校正之前设置好

文章目录 openpnp - 图像传送方向要在高级校正之前设置好笔记END openpnp - 图像传送方向要在高级校正之前设置好 笔记 图像传送方向和JOG面板的移动控制和实际设备的顶部摄像头/底部摄像头要一致&#xff0c;这样才能和贴板子时的实际操作方向对应起来。 设备标定完&#xf…

加油站智能视频监控预警系统(AI识别烟火打电话抽烟) Python 和 OpenCV 库

加油站作为存储和销售易燃易爆油品的场所&#xff0c;是重大危险源之一&#xff0c;随着科技的不断发展&#xff0c;智能视频监控预警系统在加油站的安全保障方面发挥着日益关键的作用&#xff0c;尤其是其中基于AI的烟火识别、抽烟识别和打电话识别功能&#xff0c;以及其独特…

V2M2引擎源码BlueCodePXL源码完整版

V2M2引擎源码BlueCodePXL源码完整版 链接: https://pan.baidu.com/s/1ifcTHAxcbD2CyY7gDWRVzQ?pwdmt4g 提取码: mt4g 参考资料&#xff1a;BlueCodePXL源码完整版_1234FCOM专注游戏工具及源码例子分享

[Cocoa]_[初级]_[绘制文本如何设置断行效果]

场景 在开发Cocoa程序时&#xff0c;表格NSTableView是经常使用的控件。其基于View Base的视图单元格模式就是使用NSCell或其子类来控制每个单元格的呈现。当一个单元格里的文字过多时&#xff0c;需要截断超出宽度的文字&#xff0c;怎么实现&#xff1f; 说明 Cocoa下的文本…

[Go语言快速上手]函数和包

目录 一、Go中的函数 函数声明 多个返回值 可变参数 匿名函数 值传递和地址传递 函数执行顺序&#xff08;init函数&#xff09; 二、Go中的包 基本语法 主要包&#xff08;main package&#xff09; 导入其他包 包的作用域 包的使用 包名别名 小结 一、Go中的函…

[C++]使用纯opencv部署yolov11目标检测onnx模型

yolov11官方框架&#xff1a;https://github.com/ultralytics/ultralytics 【算法介绍】 在C中使用纯OpenCV部署YOLOv11进行目标检测是一项具有挑战性的任务&#xff0c;因为YOLOv11通常是用PyTorch等深度学习框架实现的&#xff0c;而OpenCV本身并不直接支持加载和运行PyTor…

使用python基于DeepLabv3实现对图片进行语义分割

DeepLabv3 介绍 DeepLabv3 是一种先进的语义分割模型&#xff0c;由 Google Research 团队提出。它在 DeepLab 系列模型的基础上进行了改进&#xff0c;旨在提高图像中像素级分类的准确性。以下是 DeepLabv3 的详细介绍&#xff1a; 概述DeepLabv3 是 DeepLab 系列中的第三代…

设计模式-策略模式-200

优点&#xff1a;用来消除 if-else、switch 等多重判断的代码&#xff0c;消除 if-else、switch 多重判断 可以有效应对代码的复杂性。 缺点&#xff1a;会增加类的数量&#xff0c;有的时候没必要为了消除几个if-else而增加很多类&#xff0c;尤其是那些类型又长又臭的 原始代…

基于vue框架的大学生四六级学习网站设计与实现i8o8z(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;学生,训练听力,学习单词,单词分类,阅读文章,文章类型,学习课程 开题报告内容 基于Vue框架的大学生四六级学习网站设计与实现开题报告 一、研究背景与意义 随着全球化进程的加速和国际交流的日益频繁&#xff0c;英语作为国际通用语言…

【前端开发入门】html快速入门

目录 引言一、html基础模板内容二、html文档流三、html 标签1.块级元素2.行内元素3.功能性元素4.标签嵌套 四、html编码习惯五、总结 引言 本系列教程旨在帮助一些零基础的玩家快速上手前端开发。基于我自学的经验会删减部分使用频率不高的内容&#xff0c;并不代表这部分内容不…

vue.js 原生js app端实现图片旋转、放大、缩小、拖拽

效果图&#xff1a; 旋转 放大&#xff1a;手机上可以双指放大缩小 拖拽 代码实现&#xff1a; html <div id"home" class"" v-cloak><!-- 上面三个按钮 图片自己解决 --><div class"headImage" v-if"showBtn">&l…

数据订阅与消费中间件Canal 服务搭建(docker)

MySQL Bin-log开启 进入mysql容器 docker exec -it mysql5.7 bash开启mysql的binlog cd /etc/mysql/mysql.conf.dvi mysqld.cnf #在文件末尾处添加如下配置&#xff08;如果没有这个文件就创建一个&#xff09; [mysqld] # 开启 binlog log-binmysql-bin #log-bin/var/lib/mys…

Linux集群部署RabbitMQ

目录 一、准备三台虚拟机&#xff0c;配置相同 1、所有主机都需要hosts文件解析 2、所有主机安装erLang和rabbitmq 3、修改配置文件 4、导入rabbitmq 的管理界面 5、查看节点状态 6、设置erlang运行节点 7、rabitmq2和rabbitmq3重启服务 8、查看各个节点状态 二、添加…

LC刷题专题:二叉树;迭代;递归(897、1372、208)

文章目录 897.递增顺序搜索树1372. 二叉树中的最长交错路径208. 实现 Trie (前缀树) 897.递增顺序搜索树 https://leetcode.cn/problems/increasing-order-search-tree/description/ 这道题目本身就是一个简单题&#xff1a;非常容易实现&#xff1a;只需要在递归或者迭代中序…