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

news2024/10/4 19:29:08

在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系统。

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

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

相关文章

【Godot4.3】复合路径类myPath

概述 之前编写过一个基于指令绘图的类交myPoint&#xff0c;但是只涉及折线段生成。这次我基于SVG的<path>标签路径指令的启发&#xff0c;实现了一个能够获得连续绘制的直线段、圆弧和贝塞尔复合路径的类型myPath。 可以使用绘图指令方法或字符串形式的绘图指令解析来…

hbuilderx+uniapp+Android宠物用品商城领养服务系统的设计与实现 微信小程序沙箱支付

目录 项目介绍支持以下技术栈&#xff1a;具体实现截图HBuilderXuniappmysql数据库与主流编程语言java类核心代码部分展示登录的业务流程的顺序是&#xff1a;数据库设计性能分析操作可行性技术可行性系统安全性数据完整性软件测试详细视频演示源码获取方式 项目介绍 顾客 领养…

OpenCAEPoro优化(1)

核心目的&#xff1a;减少运行时的 object time 方法一&#xff1a;改变运行的进程数 进入OpenCAEPoro目录下运行下述代码&#xff08;进程数为4&#xff09; mpirun -np 4 ./testOpenCAEPoro ./data/case1/case1.data verbose1结果如下 可以看到&#xff0c;object time是…

日常工作记录:服务器被攻击导致chattr: command not found

在深夜的寂静中&#xff0c;公司的服务器突然遭遇了一场突如其来的攻击。特别是nginx配置文件无法修改&#xff0c;仿佛预示着不祥的预兆&#xff0c;面对这突如其来的灾难&#xff0c;技术人员迅速响应。 这时候需要chattr&#xff0c;但是执行的chattr -i xxx的时候&#xf…

神经网络激活函数之前的加权求和 | 矩阵相乘运算法则(清晰版)

1. 神经网络中进行加权求和为什么要将w矩阵进行转置&#xff1f; 下面以一个简单的神经网络作为举例&#xff1a; 我们要将输入特征与W进行加权求和&#xff0c;想要的是下面这种结果&#xff1a; 但是根据矩阵相乘的运算法则&#xff1a; 矩阵A的列数&#xff08;column&am…

SysML案例-风力发电

DDD领域驱动设计批评文集>> 《软件方法》强化自测题集>> 《软件方法》各章合集>>

攻防世界---->Newbie_calculations

学习笔记。 前言&#xff1a;试过od动态分析&#xff0c; 然后发现&#xff0c;那些函数不完全是混淆&#xff0c;怎么剥离 - - 不会 现在学会了。 参考&#xff1a; xctf-Newbie_calculations - jane_315 - 博客园 (cnblogs.com)https://www.cnblogs.com/jane315/p/1376964…

111.WEB渗透测试-信息收集-ARL(2)

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a; 易锦网校会员专享课 上一个内容&#xff1a;110.WEB渗透测试-信息收集-ARL&#xff08;1&#xff09; 2、安装ARL 1、最好先“apt-g…

创建django项目时,不同的编译类型有什么区别

这里主要提及的是 project venv 和 Custom environment 两种类型。 简单来说&#xff0c;project venv 是Python 3.3及以上版本中自带的虚拟环境管理工具&#xff0c;使用venv可以为每个项目创建一个独立的环境&#xff1a;python -m venv myprojectenv &#xff08;项目名后e…

电梯,建模的常见话题

以下是最近一则"女婿开电梯门导致岳父惨死"的新闻&#xff0c;可惜生命的同时&#xff0c;也引发了一系列联想。 不少人不了解或者了解但经常下意识忽略&#xff1a;电梯的门和轿厢是分离的部件。部件之间的协作如果有失误&#xff0c;系统就会出问题。电梯可以看作是…

JVM 基础、GC 算法与 JProfiler 监控工具详解

目录 1、引言 1.1 JVM内存与本地内存 1.2 JVM与JDK的关系 2、JVM基础 2.1 JVM&#xff08;Java Virtual Machine&#xff09; 2.2 Java与JVM的关系 2.3 JVM的内存结构 2.3.1 堆内存 2.3.2 栈内存 2.3.3 方法区 2.3.4 本地方法栈 2.3.5 程序计数器&#xff08;PC寄存…

Python+Matplotlib创建y=sinx、y=cosx、y=sinx+cosx可视化

y sin x (奇函数)&#xff1a; 图像关于原点对称。 对于任何 x&#xff0c;sin(-x) -sin(x)&#xff0c;符合奇函数定义。 y cos x (偶函数)&#xff1a; 图像关于 y 轴对称。 对于任何 x&#xff0c;cos(-x) cos(x)&#xff0c;符合偶函数定义。 y sin x cos x (既…

jQuery——对象的查找(查找孩子-父母-兄弟标签)

在已经匹配出的元素集合中根据选择器查找孩子/父母/兄弟标签&#xff0c;并封装为新的 jQuery 对象返回 children&#xff08;&#xff09; 子标签中找 find&#xff08;&#xff09; 后代标签中找 parent&#xff08;&#xff09; 父标签 prevAll&#xff08;&#xff0…

【内存池】——Nginx 内存池结构设计

目录 实现思路——分而治之 Nginx 的内存池结构图 结构体设计 内存池设计&#xff1a; 数据区属性设计&#xff1a; 大块内存区设计&#xff1a; 伪代码解释&#xff1a; 数据结构实现 实现思路——分而治之 算法结构&#xff1a;链表顺序表 1、对于每个请求或者连接都会建…

通信工程学习:什么是IGMP因特网组管理协议

IGMP&#xff1a;因特网组管理协议 IGMP&#xff08;Internet Group Management Protocol&#xff0c;因特网组管理协议&#xff09;是TCP/IP协议簇中负责组播成员管理的协议。它主要用于在用户主机和与其直接相连的组播路由器之间建立和维护组播组成员关系。以下是关于IGMP协议…

浙江工业大学《2019年+2023年828自动控制原理真题》 (完整版)

本文内容&#xff0c;全部选自自动化考研联盟的&#xff1a;《浙江工业大学828自控考研资料》的真题篇。后续会持续更新更多学校&#xff0c;更多年份的真题&#xff0c;记得关注哦~ 目录 2019年真题 2023年真题 Part1&#xff1a;2019年2023年完整版真题 2019年真题 2023年…

NVIDIA H100 GPU 上的机密计算可实现安全且值得信赖的 AI

NVIDIA H100 GPU 上的机密计算,实现安全可信的 AI 文章目录 前言1. 使用硬件虚拟化的 NVIDIA 机密计算2. 跨硬件、固件和软件保护 AI3. NVIDIA H100 GPU 的硬件安全性4. 在机密计算模式下运行 NVIDIA H100 GPU5. NVIDIA Hopper H100 机密计算为可信 AI 带来的优势6. 虚拟机上基…

68.【C语言】动态内存管理(重点)(1)

本文为数据结构打下基础 备注:数据结构需要掌握指针,结构体和动态内存管理 目录 1.内存开辟的方式 2.malloc函数 cplusplus网翻译 提炼要点 操作内存空间 01.开辟内存空间成功 02.开辟内存空间失败 如果是x64debug环境下,可能会成功 1.内存开辟的方式 01.创建变量 i…

从0到1酒店民宿管理系统

最近几天放假没事做&#xff0c;在家里就像把学过的winform技术整合下&#xff0c;一些用的技术点整理整理。想着做个什么软件那&#xff1f;无意中看到的酒店管理系统给了我思路。为啥不自己做一个那&#xff1f;说做就做。首先技术确定了使用winform为啥不用wpf那&#xff1f…

Linux环境基础开发工具使用(2)

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 Linux环境基础开发工具使用(2) 收录于专栏[Linux学习] 本专栏旨在分享学习Linux的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 1. Li…