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

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�