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堆转储文件之1.6G大文件处理完整指南

《Java堆转储文件之1.6G大文件处理完整指南》堆转储文件是优化、分析内存消耗的重要工具,:本文主要介绍Java堆转储文件之1.6G大文件处理的相关资料,文中通过代码介绍的非常详细,需要的朋友可... 目录前言文件为什么这么大?如何处理这个文件?分析文件内容(推荐)删除文件(如果不需要)查看错误来源如何避

SpringBoot整合Dubbo+ZK注册失败的坑及解决

《SpringBoot整合Dubbo+ZK注册失败的坑及解决》使用Dubbo框架时,需在公共pom添加依赖,启动类加@EnableDubbo,实现类用@DubboService替代@Service,配... 目录1.先看下公共的pom(maven创建的pom工程)2.启动类上加@EnableDubbo3.实

SpringBoot整合(ES)ElasticSearch7.8实践

《SpringBoot整合(ES)ElasticSearch7.8实践》本文详细介绍了SpringBoot整合ElasticSearch7.8的教程,涵盖依赖添加、客户端初始化、索引创建与获取、批量插... 目录SpringBoot整合ElasticSearch7.8添加依赖初始化创建SpringBoot项

JAVA覆盖和重写的区别及说明

《JAVA覆盖和重写的区别及说明》非静态方法的覆盖即重写,具有多态性;静态方法无法被覆盖,但可被重写(仅通过类名调用),二者区别在于绑定时机与引用类型关联性... 目录Java覆盖和重写的区别经常听到两种话认真读完上面两份代码JAVA覆盖和重写的区别经常听到两种话1.覆盖=重写。2.静态方法可andro

SpringBoot中六种批量更新Mysql的方式效率对比分析

《SpringBoot中六种批量更新Mysql的方式效率对比分析》文章比较了MySQL大数据量批量更新的多种方法,指出REPLACEINTO和ONDUPLICATEKEY效率最高但存在数据风险,MyB... 目录效率比较测试结构数据库初始化测试数据批量修改方案第一种 for第二种 case when第三种

python生成随机唯一id的几种实现方法

《python生成随机唯一id的几种实现方法》在Python中生成随机唯一ID有多种方法,根据不同的需求场景可以选择最适合的方案,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习... 目录方法 1:使用 UUID 模块(推荐)方法 2:使用 Secrets 模块(安全敏感场景)方法

Java docx4j高效处理Word文档的实战指南

《Javadocx4j高效处理Word文档的实战指南》对于需要在Java应用程序中生成、修改或处理Word文档的开发者来说,docx4j是一个强大而专业的选择,下面我们就来看看docx4j的具体使用... 目录引言一、环境准备与基础配置1.1 Maven依赖配置1.2 初始化测试类二、增强版文档操作示例2.

一文详解如何使用Java获取PDF页面信息

《一文详解如何使用Java获取PDF页面信息》了解PDF页面属性是我们在处理文档、内容提取、打印设置或页面重组等任务时不可或缺的一环,下面我们就来看看如何使用Java语言获取这些信息吧... 目录引言一、安装和引入PDF处理库引入依赖二、获取 PDF 页数三、获取页面尺寸(宽高)四、获取页面旋转角度五、判断

Spring Boot中的路径变量示例详解

《SpringBoot中的路径变量示例详解》SpringBoot中PathVariable通过@PathVariable注解实现URL参数与方法参数绑定,支持多参数接收、类型转换、可选参数、默认值及... 目录一. 基本用法与参数映射1.路径定义2.参数绑定&nhttp://www.chinasem.cnbs

JAVA中安装多个JDK的方法

《JAVA中安装多个JDK的方法》文章介绍了在Windows系统上安装多个JDK版本的方法,包括下载、安装路径修改、环境变量配置(JAVA_HOME和Path),并说明如何通过调整JAVA_HOME在... 首先去oracle官网下载好两个版本不同的jdk(需要登录Oracle账号,没有可以免费注册)下载完