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

相关文章

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

Spring Cloud LoadBalancer 负载均衡详解

《SpringCloudLoadBalancer负载均衡详解》本文介绍了如何在SpringCloud中使用SpringCloudLoadBalancer实现客户端负载均衡,并详细讲解了轮询策略和... 目录1. 在 idea 上运行多个服务2. 问题引入3. 负载均衡4. Spring Cloud Load

Springboot中分析SQL性能的两种方式详解

《Springboot中分析SQL性能的两种方式详解》文章介绍了SQL性能分析的两种方式:MyBatis-Plus性能分析插件和p6spy框架,MyBatis-Plus插件配置简单,适用于开发和测试环... 目录SQL性能分析的两种方式:功能介绍实现方式:实现步骤:SQL性能分析的两种方式:功能介绍记录

在 Spring Boot 中使用 @Autowired和 @Bean注解的示例详解

《在SpringBoot中使用@Autowired和@Bean注解的示例详解》本文通过一个示例演示了如何在SpringBoot中使用@Autowired和@Bean注解进行依赖注入和Bean... 目录在 Spring Boot 中使用 @Autowired 和 @Bean 注解示例背景1. 定义 Stud

如何通过Python实现一个消息队列

《如何通过Python实现一个消息队列》这篇文章主要为大家详细介绍了如何通过Python实现一个简单的消息队列,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录如何通过 python 实现消息队列如何把 http 请求放在队列中执行1. 使用 queue.Queue 和 reque