Java进阶:利用SPI机制不侵入源码而实现定制功能【附带源码】

2024-08-29 08:44

本文主要是介绍Java进阶:利用SPI机制不侵入源码而实现定制功能【附带源码】,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 0. 引言
  • 1. 什么是SPI
  • 2. SPI的优缺点
    • 2.1 优点
    • 2.2 缺点
  • 3. 应用场景
  • 4. 使用步骤
  • 5. 演示源码
  • 6. 总结

0. 引言

最近遇到一个场景,需要针对之前的文件上传工具包进行拓展,增加其他类型的文件服务器的操作代码,但之前的工具包因为是通用包在其他项目中也有使用,暂时不想去更改之前的包内容,于是想在不影响原代码的情况下,去实现拓展实现类,针对这个的需求,显然很适合java的SPI机制来实现,下面我们来看看具体如何操作。

1. 什么是SPI

首先我们要了解什么是SPI, (服务提供者接口 Service Provider Interface)是一种动态加载实现扩展点的机制,它允许服务提供者在运行时动态地为某个接口提供实现,而不需要在程序编译时进行硬编码。这种机制的核心思想是将装配的控制权移到程序之外,通过在模块化设计中实现接口与服务实现的解耦,从而提供一种插件化的扩展机制。SPI机制主要应用于框架和库的开发中,以支持服务的动态加载和替换

在Java中,SPI机制的实现依赖于java.util.ServiceLoader类,它负责查找和加载实现了特定接口的服务提供者。服务提供者需要在其JAR包的META-INF/services目录下创建一个以服务接口全限定名为名称的文件,并在该文件中列出实现该接口的具体类的名称。当应用程序需要使用某个服务时,可以通过ServiceLoader类查找并加载这些实现类,进而使用它们提供的服务

2. SPI的优缺点

2.1 优点

  • 松耦合

SPI 机制允许应用程序在运行时动态加载实现,客户端代码与具体实现解耦,提高了模块化程度和灵活性。

  • 动态扩展

可以在不修改现有代码的情况下,添加新的服务提供者。只需在配置文件中指定新的实现类即可,这使得应用程序可以方便地扩展功能,这也是使用spi的主要原因

  • 标准化

SPI 是 Java 平台的一部分,遵循标准规范,开发者可以利用它实现跨库的插件机制,增强代码的兼容性和可维护性。

  • 方便的服务加载

SPI 使用 ServiceLoader 类来加载服务提供者,简化了服务的加载过程,自动处理了实现类的查找和实例化。但加载虽然方便,使用起来仍然需要遍历所有的实现类,使用上也有不便之处。

2.2 缺点

  • 性能开销

SPI 在运行时动态加载实现类,这可能会引入一定的性能开销,特别是当服务提供者数量较多时,加载和实例化的过程可能较慢。另外java spi会在项目启动时就加载所有的实现类,而某些实现类如果暂时用不到但初始化耗时又很长时,就会增加初始化的整体耗时

  • 调试困难

由于 SPI 机制涉及动态加载和配置文件,调试可能会比较困难,错误信息不够直观,可能导致问题定位困难。

  • 管理复杂性

随着服务提供者数量的增加,管理和维护 SPI 配置文件可能会变得复杂,特别是在多个模块和库之间存在大量实现时。所以当我们的实现类很多时,要慎重考虑使用spi

  • 不支持泛型

SPI 机制不直接支持泛型,这意味着在定义服务接口时,不能使用泛型参数,可能需要采用其他设计模式或策略来解决。

  • 依赖配置文件

SPI 机制依赖于配置文件(通常是 META-INF/services/ 目录下的文件),如果配置文件不正确或缺失,会导致服务无法加载,增加了配置和部署的复杂度。这点在新增实现类时会经常遗漏

3. 应用场景

  • 插件架构

SPI 机制常用于实现插件架构,允许用户在运行时动态加载和卸载插件。应用程序可以通过 SPI 机制发现和加载插件,而不需要在编译时确定插件的具体实现。例如,很多大型应用程序和框架(如 Spring Boot、dubbo)都使用 SPI 来支持插件和扩展功能。

  • 数据库驱动加载

在 Java 数据库连接(JDBC)中,数据库驱动的加载就是一个典型的 SPI 应用场景。JDBC 驱动程序通过 SPI 机制注册自己,以便 Java 程序可以通过 DriverManager 动态加载并使用不同的数据库驱动,而无需在编译时指定具体的数据库实现。

  • 自定义功能拓展

针对一些工具包或者第三方包,我们希望拓展其功能,又不能改动其源码的情况,就可以借助SPI机制来进行扩展。

总之,Java SPI 机制在需要动态加载、解耦和扩展的场景中表现尤为出色,可以帮助构建灵活、可扩展的应用程序。

4. 使用步骤

1、而java SPI的使用实际上也很简单,首先我们先创建一个maven项目spi_demo_import,用来模拟我们要扩展的工具包,其项目下有接口类IFileService,以及原有的实现类ObsService

package com.example.file;import java.io.InputStream;public interface IFileService {String makeBucket(String bucketName);boolean existBucket(String bucketName);boolean removeBucket(String bucketName);boolean setBucketExpires(String bucketName, int days);void upload(String bucketName, String fileName, InputStream stream);
}public class ObsService implements IFileService{@Overridepublic String makeBucket(String bucketName) {return  "obs create " + bucketName + " bucket success";}@Overridepublic boolean existBucket(String bucketName) {// 具体的代码实现省略,仅演示return false;}@Overridepublic boolean removeBucket(String bucketName) {return false;}@Overridepublic boolean setBucketExpires(String bucketName, int days) {return false;}@Overridepublic void upload(String bucketName, String fileName, InputStream stream) {}
}

2、然后我们创建一个springboot项目spi_demo,我们在该项目中实现对工具包中IFileService接口的扩展,然后在spi_demo项目pom中引入刚刚创建的工具包项目spi_demo_import

3、我们计划扩展IFileService接口,实现对minio和oss对象存储的实现,创建MinioServiceOssService,这里省略具体的实现代码,仅做一个演示需要

public class OssService implements IFileService {@Overridepublic String makeBucket(String bucketName) {return "oss create " + bucketName + " bucket success";}@Overridepublic boolean existBucket(String bucketName) {return false;}@Overridepublic boolean removeBucket(String bucketName) {return false;}@Overridepublic boolean setBucketExpires(String bucketName, int days) {return false;}@Overridepublic void upload(String bucketName, String fileName, InputStream stream) {}
}public class MinioService implements IFileService {@Overridepublic String makeBucket(String bucketName) {return "minio create " + bucketName + " bucket success";}@Overridepublic boolean existBucket(String bucketName) {return false;}@Overridepublic boolean removeBucket(String bucketName) {return false;}@Overridepublic boolean setBucketExpires(String bucketName, int days) {return false;}@Overridepublic void upload(String bucketName, String fileName, InputStream stream) {}
}

4、其次在spi_demo项目中的resources资源目录下创建META-INF/services目录(注意这里是两个目录,先创建META-INF后再在其下创建services,否则可能会识别为一个目录)

5、再根据之前接口类IFileService的全包名来创建一个同名文本文件,如我这里是com.example.file.IFileService

6、然后在该文本中添加所有实现类的全包名,注意包括原工具包中的实现类

com.example.spi_demo.service.MinioService
com.example.spi_demo.service.OssService
com.example.file.ObsService

7、到这里我们的服务扩展就完成了,那么如何使用呢,我们创建一个controller,来模拟调用接口

如下可以看到其使用只需要通过ServiceLoader.load(IFileService.class)来获取所有实现类,然后遍历循环,通过instanceof来找到自己需要的具体实现类

@RestController
public class DemoController {@GetMapping("createBucket")public String createBucket(String name, Integer type){ServiceLoader<IFileService> serviceLoader = ServiceLoader.load(IFileService.class);IFileService fileService = null;for (IFileService service: serviceLoader){if(type == 0){if(service instanceof MinioService){fileService = service;}}else if(type == 1){if(service instanceof OssService){fileService = service;}}else {if(service instanceof ObsService){fileService = service;}}}return fileService.makeBucket(name);}
}

8、调用测试
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以看到不同的type参数成功切换到不同的实现类了,那么我们的扩展就成功了。

5. 演示源码

本文演示源码可在如下地址下载:

https://gitee.com/wuhanxue/wu_study/tree/master/demo/spi_demo

6. 总结

SPI的使用很简单,但是其性能上少有欠缺,实际使用时我们需要结合具体的情况来选择,千万不要涉及到扩展就无脑使用SPI, 选择最有效、最简单的方案。

这篇关于Java进阶:利用SPI机制不侵入源码而实现定制功能【附带源码】的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1117399

相关文章

MySQL索引的优化之LIKE模糊查询功能实现

《MySQL索引的优化之LIKE模糊查询功能实现》:本文主要介绍MySQL索引的优化之LIKE模糊查询功能实现,本文通过示例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧... 目录一、前缀匹配优化二、后缀匹配优化三、中间匹配优化四、覆盖索引优化五、减少查询范围六、避免通配符开头七、使用外部搜索引擎八、分

Python实现特殊字符判断并去掉非字母和数字的特殊字符

《Python实现特殊字符判断并去掉非字母和数字的特殊字符》在Python中,可以通过多种方法来判断字符串中是否包含非字母、数字的特殊字符,并将这些特殊字符去掉,本文为大家整理了一些常用的,希望对大家... 目录1. 使用正则表达式判断字符串中是否包含特殊字符去掉字符串中的特殊字符2. 使用 str.isa

Java程序进程起来了但是不打印日志的原因分析

《Java程序进程起来了但是不打印日志的原因分析》:本文主要介绍Java程序进程起来了但是不打印日志的原因分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java程序进程起来了但是不打印日志的原因1、日志配置问题2、日志文件权限问题3、日志文件路径问题4、程序

Spring 基于XML配置 bean管理 Bean-IOC的方法

《Spring基于XML配置bean管理Bean-IOC的方法》:本文主要介绍Spring基于XML配置bean管理Bean-IOC的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一... 目录一. spring学习的核心内容二. 基于 XML 配置 bean1. 通过类型来获取 bean2. 通过

Spring Boot 集成 Quartz并使用Cron 表达式实现定时任务

《SpringBoot集成Quartz并使用Cron表达式实现定时任务》本篇文章介绍了如何在SpringBoot中集成Quartz进行定时任务调度,并通过Cron表达式控制任务... 目录前言1. 添加 Quartz 依赖2. 创建 Quartz 任务3. 配置 Quartz 任务调度4. 启动 Sprin

springboot上传zip包并解压至服务器nginx目录方式

《springboot上传zip包并解压至服务器nginx目录方式》:本文主要介绍springboot上传zip包并解压至服务器nginx目录方式,具有很好的参考价值,希望对大家有所帮助,如有错误... 目录springboot上传zip包并解压至服务器nginx目录1.首先需要引入zip相关jar包2.然

Android实现悬浮按钮功能

《Android实现悬浮按钮功能》在很多场景中,我们希望在应用或系统任意界面上都能看到一个小的“悬浮按钮”(FloatingButton),用来快速启动工具、展示未读信息或快捷操作,所以本文给大家介绍... 目录一、项目概述二、相关技术知识三、实现思路四、整合代码4.1 Java 代码(MainActivi

Java数组初始化的五种方式

《Java数组初始化的五种方式》数组是Java中最基础且常用的数据结构之一,其初始化方式多样且各具特点,本文详细讲解Java数组初始化的五种方式,分析其适用场景、优劣势对比及注意事项,帮助避免常见陷阱... 目录1. 静态初始化:简洁但固定代码示例核心特点适用场景注意事项2. 动态初始化:灵活但需手动管理代

Java使用SLF4J记录不同级别日志的示例详解

《Java使用SLF4J记录不同级别日志的示例详解》SLF4J是一个简单的日志门面,它允许在运行时选择不同的日志实现,这篇文章主要为大家详细介绍了如何使用SLF4J记录不同级别日志,感兴趣的可以了解下... 目录一、SLF4J简介二、添加依赖三、配置Logback四、记录不同级别的日志五、总结一、SLF4J

将Java项目提交到云服务器的流程步骤

《将Java项目提交到云服务器的流程步骤》所谓将项目提交到云服务器即将你的项目打成一个jar包然后提交到云服务器即可,因此我们需要准备服务器环境为:Linux+JDK+MariDB(MySQL)+Gi... 目录1. 安装 jdk1.1 查看 jdk 版本1.2 下载 jdk2. 安装 mariadb(my