java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > JDK与Spring Boot应用

深入浅析SPI机制在JDK与Spring Boot中的应用

作者:砖业洋__

SPI是一种使软件框架或库更加模块化、可扩展和可维护的有效方法。通过遵循“开闭原则”, SPI 确保了系统的稳定性和灵活性,从而满足了不断变化的业务需求,这篇文章主要介绍了SPI机制在JDK与Spring Boot中的应用,需要的朋友可以参考下

1. SPI解读:什么是SPI?

   SPI ( Service Provider Interface ) 是一种服务发现机制,它允许第三方提供者为核心库或主框架提供实现或扩展。这种设计允许核心库/框架在不修改自身代码的情况下,通过第三方实现来增强功能。

1.JDK原生的SPI

2.Spring的SPI

举个类比的例子:

  想象我们正在建造一个电视机, SPI 就像电视机上的一个 USB 插口。这个插口可以插入各种设备(例如U盘、游戏手柄、电视棒等),但我们并不关心这些设备的内部工作方式。这样只需要提供一个标准的接口,其他公司(例如U盘制造商)可以为此接口提供实现。这样,电视机可以在不更改自己内部代码的情况下使用各种新设备,而设备制造商也可以为各种电视机制造兼容的设备。

  总之, SPI 是一种将接口定义与实现分离的设计模式,它鼓励第三方为一个核心产品或框架提供插件或实现,从而使核心产品能够轻松地扩展功能。

2. SPI在JDK中的应用示例

  在 Java 的生态系统中, SPI 是一个核心概念,允许开发者提供扩展和替代的实现,而核心库或应用不必更改,下面举出一个例子来说明。

全部代码和步骤如下:

步骤1:定义一个服务接口,文件名: MessageService.java

package com.example.demo.service;
public interface MessageService {
String getMessage();
}

步骤2:为服务接口提供实现,这里会提供两个简单的实现类。

HelloMessageService.java

package com.example.demo.service;
public class HelloMessageService implements MessageService {
@Override
public String getMessage() {
return "Hello from HelloMessageService!";
}
}

HiMessageService.java

package com.example.demo.service;
public class HiMessageService implements MessageService {
@Override
public String getMessage() {
return "Hi from HiMessageService!";
}
}

这些实现就像不同品牌或型号的U盘或其他 USB 设备。每个设备都有自己的功能和特性,但都遵循相同的 USB 标准。

步骤3:注册服务提供者

  在资源目录(通常是 src/main/resources/ )下创建一个名为 META-INF/services/ 的文件夹。在这个文件夹中,创建一个名为 com.example.demo.service.MessageService 的文件(这是我们接口的全限定名),这个文件没有任何文件扩展名,所以不要加上 .txt 这样的后缀。文件的内容应为我们的两个实现类的全限定名,每个名字占一行:

com.example.demo.service.HelloMessageService
com.example.demo.service.HiMessageService

   META-INF/services/ Java SPI ( Service Provider Interface ) 机制中约定俗成的特定目录。它不是随意选择的,而是 SPI 规范中明确定义的。因此,当使用 JDK ServiceLoader 类来加载服务提供者时,它会特意去查找这个路径下的文件。

  请确保文件的每一行只有一个名称,并且没有额外的空格或隐藏的字符,文件使用 UTF-8 编码。

步骤4:使用 ServiceLoader 加载和使用服务

package com.example.demo;
import com.example.demo.service.MessageService;
import java.util.ServiceLoader;
public class DemoApplication {
public static void main(String[] args) {
ServiceLoader<MessageService> loaders = ServiceLoader.load(MessageService.class);
for (MessageService service : loaders) {
System.out.println(service.getMessage());
}
}
}

运行结果如下:

  这说明 ServiceLoader 成功地加载了我们为 MessageService 接口提供的两个实现,并且我们可以在不修改 Main 类的代码的情况下,通过添加更多的实现类和更新 META-INF/services/com.example.MessageService 文件来扩展我们的服务。

想象一下买了一台高端的智能电视,这台电视上有一个或多个 HDMI 端口,这就是它与外部设备连接的接口。

3. SPI在Spring框架中的应用

   Spring 官方在其文档和源代码中多次提到了 SPI Service Provider Interface )的概念。但是,当我们说“ Spring SPI ”时,通常指的是 Spring 框架为开发者提供的一套可扩展的接口和抽象类,开发者可以基于这些接口和抽象类实现自己的版本。

Spring 中, SPI 的概念与 Spring Boot 使用的 spring.factories 文件的机制不完全一样,但是它们都体现了可插拔、可扩展的思想。

1.Spring的SPI

2.Boot的spring.factories机制

3.1 传统Spring框架中的SPI思想

  在传统的 Spring 框架中,虽然没有直接使用名为 "SPI" 的术语,但其核心思想仍然存在。 Spring 提供了多个扩展点,其中最具代表性的就是 BeanPostProcessor 。在本节中,我们将通过一个简单的 MessageService 接口及其实现来探讨如何利用 Spring BeanPostProcessor 扩展点体现 SPI 的思想。

提供两个简单的实现类。

HelloMessageService.java

package com.example.demo.service;
public class HelloMessageService implements MessageService {
@Override
public String getMessage() {
return "Hello from HelloMessageService!";
}
}

HiMessageService.java

package com.example.demo.service;
public class HiMessageService implements MessageService {
@Override
public String getMessage() {
return "Hi from HiMessageService!";
}
}

定义 BeanPostProcessor

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class MessageServicePostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if(bean instanceof MessageService) {
return new MessageService() {
@Override
public String getMessage() {
return ((MessageService) bean).getMessage() + " [Processed by Spring SPI]";
}
};
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}

修改 Spring 配置

MessageServicePostProcessor 添加到 Spring 配置中:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MessageServiceConfig {
@Bean
public MessageService helloMessageService() {
return new HelloMessageService();
}
@Bean
public MessageService hiMessageService() {
return new HiMessageService();
}
@Bean
public MessageServicePostProcessor messageServicePostProcessor() {
return new MessageServicePostProcessor();
}
}

执行程序

使用之前提供的 DemoApplication 示例类:

package com.example.demo;
import com.example.demo.configuration.MessageServiceConfig;
import com.example.demo.service.MessageService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(MessageServiceConfig.class);
MessageService helloMessageService = context.getBean("helloMessageService", MessageService.class);
MessageService hiMessageService = context.getBean("hiMessageService", MessageService.class);
System.out.println(helloMessageService.getMessage());
System.out.println(hiMessageService.getMessage());
}
}

运行结果:

  现在,每一个 MessageService 实现都被 BeanPostProcessor 处理了,添加了额外的消息 “[Processed by Spring SPI]” 。这演示了 Spring SPI 概念,通过 BeanPostProcessor 来扩展或修改 Spring 容器中的 bean

  有人可能留意到这里红色的警告,这个之前在讲 BeanPostProcessor 的时候也提到过,当 BeanPostProcessor 自身被一个或多个 BeanPostProcessor 处理时,就会出现这种情况。简单地说,由于 BeanPostProcessor 需要在其他 bean 之前初始化,所以某些 BeanPostProcessor 无法处理早期初始化的 bean ,包括配置类和其他 BeanPostProcessor 。解决办法就是不要把 MessageServicePostProcessor 放在配置类初始化,在配置类删掉,再把 MessageServicePostProcessor 加上 @Component 注解。

类比文章开头的电视机的例子:

  总的来说,与之前的例子相比,这个新示例提供了一个更加动态的场景,其中 Spring BeanPostProcessor 扩展点允许我们拦截并修改 bean 的行为,就像一个能够干预并改变电视机显示内容的智能设备。

3.2 Spring Boot中的SPI思想

   Spring Boot 有一个与 SPI 相似的机制,但它并不完全等同于 Java 的标准 SPI

   Spring Boot 的自动配置机制主要依赖于 spring.factories 文件。这个文件可以在多个 jar 中存在,并且 Spring Boot 会加载所有可见的 spring.factories 文件。我们可以在这个文件中声明一系列的自动配置类,这样当满足某些条件时,这些配置类会自动被 Spring Boot 应用。

接下来会展示 Spring SPI 思想的好例子,但是它与 Spring Boot 紧密相关。

定义接口

package com.example.demo.service;
public interface MessageService {
String getMessage();
}

这里会提供两个简单的实现类。

HelloMessageService.java

package com.example.demo.service;
public class HelloMessageService implements MessageService {
@Override
public String getMessage() {
return "Hello from HelloMessageService!";
}
}

HiMessageService.java

package com.example.demo.service;
public class HiMessageService implements MessageService {
@Override
public String getMessage() {
return "Hi from HiMessageService!";
}
}

注册服务

resources/META-INF 下创建一个文件名为 spring.factories 。这个文件里,可以注册 MessageService 实现类。

com.example.demo.service.MessageService=com.example.demo.service.HelloMessageService,com.example.demo.service.HiMessageService

  注意这里 com.example.demo.service.MessageService 是接口的全路径,而 com.example.demo.service.HelloMessageService,com.example.demo.service.HiMessageService 是实现类的全路径。如果有多个实现类,它们应当用逗号分隔。

   spring.factories 文件中的条目键和值之间不能有换行,即 key=value 形式的结构必须在同一行开始。但是,如果有多个值需要列出(如多个实现类),并且这些值是逗号分隔的,那么可以使用反斜杠( \ )来换行。 spring.factories 的名称是约定俗成的。如果试图使用一个不同的文件名,那么 Spring Boot 的自动配置机制将不会识别它。

这里 spring.factories 又可以写为

com.example.demo.service.MessageService=com.example.demo.service.HelloMessageService,\
com.example.demo.service.HiMessageService

直接在逗号后面回车 IDEA 会自动补全反斜杠,保证键和值之间不能有换行即可。

使用 SpringFactoriesLoader 来加载服务

package com.example.demo;
import com.example.demo.service.MessageService;
import org.springframework.core.io.support.SpringFactoriesLoader;
import java.util.List;
public class DemoApplication {
public static void main(String[] args) {
List<MessageService> services = SpringFactoriesLoader.loadFactories(MessageService.class, null);
for (MessageService service : services) {
System.out.println(service.getMessage());
}
}
}

SpringFactoriesLoader.loadFactories 的第二个参数是类加载器,此处我们使用默认的类加载器,所以传递 null

运行结果:

  这种方式利用了 Spring SpringFactoriesLoader ,它允许开发者提供接口的多种实现,并通过 spring.factories 文件来注册它们。这与 JDK SPI 思想非常相似,只是在实现细节上有所不同。这也是 Spring Boot 如何自动配置的基础,它会查找各种 spring.factories 文件,根据其中定义的类来初始化和配置 bean

我们继续使用电视机的例子来解释:

简化解释:

  总结:在这个 Spring Boot SPI 例子中,我们展示了核心 Spring 应用如何自动地识别和使用 spring.factories 文件中注册的实现,这与电视机自动地识别和使用所有插入的 USB 设备有相似之处。

4. SPI在JDBC驱动加载中的应用

  数据库驱动的 SPI 主要体现在 JDBC 驱动的自动发现机制中。 JDBC 4.0 引入了一个特性,允许驱动自动注册到 DriverManager 。这是通过使用 Java SPI 来实现的。驱动 jar 包内会有一个 META-INF/services/java.sql.Driver 文件,此文件中包含了该驱动的 Driver 实现类的全类名。这样,当类路径中有 JDBC 驱动的 jar 文件时, Java 应用程序可以自动发现并加载 JDBC 驱动,而无需明确地加载驱动类。

  这意味着任何数据库供应商都可以编写其自己的 JDBC 驱动程序,只要它遵循 JDBC 驱动程序的 SPI ,它就可以被任何使用 JDBC Java 应用程序所使用。

当我们使用 DriverManager.getConnection() 获取数据库连接时,背后正是利用 SPI 机制加载合适的驱动程序。

以下是 SPI 机制的具体工作方式:

1.定义服务接口

在这里,接口已经由 Java 平台定义,即 java.sql.Driver

2.为接口提供实现

各大数据库厂商(如 Oracle , MySQL , PostgreSQL 等)为其数据库提供了 JDBC 驱动程序,它们都实现了 java.sql.Driver 接口。例如, MySQL 的驱动程序中有一个类似于以下的类:

public class com.mysql.cj.jdbc.Driver implements java.sql.Driver {
// 实现接口方法...
}

直接上图:

3.注册服务提供者

对于 MySQL 的驱动程序,可以在其 JAR 文件的 META-INF/services 目录下找到一个名为 java.sql.Driver 的文件,文件内容如下:

com.mysql.cj.jdbc.Driver

直接上图:

看到这里是不是发现和第 2 节举的 JDK SPI 的例子一样?体会一下。

4.使用SPI来加载和使用服务

  当我们调用 DriverManager.getConnection(jdbcUrl, username, password) 时, DriverManager 会使用 ServiceLoader 来查找所有已注册的 java.sql.Driver 实现。然后,它会尝试每一个驱动程序,直到找到一个可以处理给定 jdbcUrl 的驱动程序。

以下是一个简单的示例,展示如何使用 JDBC SPI 获取数据库连接:

import java.sql.Connection;
import java.sql.DriverManager;
public class JdbcExample {
public static void main(String[] args) {
String jdbcUrl = "jdbc:mysql://localhost:3306/mydatabase";
String username = "root";
String password = "password";
try {
Connection connection = DriverManager.getConnection(jdbcUrl, username, password);
System.out.println("Connected to the database!");
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

  在上述代码中,我们没有明确指定使用哪个 JDBC 驱动程序,因为 DriverManager 会自动为我们选择合适的驱动程序。

  这种模块化和插件化的机制使得我们可以轻松地为不同的数据库切换驱动程序,只需要更改 JDBC URL 并确保相应的驱动程序 JAR 在类路径上即可。

  在 Spring Boot 中,开发者通常不会直接与 JDBC SPI 机制交互来获取数据库连接。 Spring Boot 的自动配置机制隐藏了许多底层细节,使得配置和使用数据库变得更加简单。

一般会在 application.properties application.yml 中配置数据库连接信息。

例如:

spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

  在上述步骤中, Spring Boot 的自动配置机制会根据提供的依赖和配置信息来初始化和配置 DataSource 对象,这个对象管理数据库连接。实际上,添加 JDBC 驱动依赖时, Spring Boot 会使用 JDK SPI 机制(在 JDBC 规范中应用)来找到并加载相应的数据库驱动。开发者虽然不直接与 JDK SPI 交互,但在背后 Spring Boot 确实利用了 JDK SPI 机制来获取数据库连接。

5. 如何通过Spring Boot自动配置理解SPI思想

  这种机制有点类似于 Java SPI ,因为它允许第三方库提供一些默认的配置。但它比 Java SPI 更为强大和灵活,因为 Spring Boot 提供了大量的注解(如 @ConditionalOnClass @ConditionalOnProperty @ConditionalOnMissingBean 等)来控制自动配置类是否应该被加载和应用。

  总的来说, Spring Boot spring.factories 机制和 Java SPI 在概念上是相似的,但它们在实现细节和用途上有所不同。

让我们创建一个简化的实际例子,假设我们要为不同的消息服务(如 SMS Email )创建自动配置。

MessageService接口

package com.example.demo.service;
public interface MessageService {
void send(String message);
}

SMS服务实现

package com.example.demo.service.impl;
import com.example.demo.service.MessageService;
public class SmsService implements MessageService {
@Override
public void send(String message) {
System.out.println("Sending SMS: " + message);
}
}

Email服务实现

package com.example.demo.service.impl;
import com.example.demo.service.MessageService;
public class EmailService implements MessageService {
@Override
public void send(String message) {
System.out.println("Sending Email: " + message);
}
}

自动配置类

package com.example.demo.configuration;
import com.example.demo.service.EmailService;
import com.example.demo.service.MessageService;
import com.example.demo.service.SmsService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MessageAutoConfiguration {
@Bean
@ConditionalOnProperty(name = "message.type", havingValue = "sms")
public MessageService smsService() {
return new SmsService();
}
@Bean
@ConditionalOnProperty(name = "message.type", havingValue = "email")
public MessageService emailService() {
return new EmailService();
}
}

  这个类提供两个条件性的 beans (组件),分别是 SmsService EmailService 。这些 beans 的创建取决于 application.properties 文件中特定的属性值。

  当 application.properties application.yml 中定义的属性 message.type 的值为 sms 时,此条件为 true 。此时, smsService() 方法将被调用,从而创建一个 SmsService bean

  当 application.properties application.yml 中定义的属性 message.type 的值为 email 时,此条件为 true 。此时, emailService() 方法将被调用,从而创建一个 EmailService bean

spring.factories文件

src/main/resources/META-INF 目录下创建一个 spring.factories 文件,内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.demo.configuration.MessageAutoConfiguration

application.properties文件

message.type=sms

MessageTester组件

package com.example.demo;
import com.example.demo.service.MessageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class MessageTester {
@Autowired
private MessageService messageService;
@PostConstruct
public void init() {
messageService.send("Hello World");
}
}

DemoApplication主程序

package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

运行结果:

  在上述例子中,我们创建了一个 MessageService 接口和两个实现( SmsService EmailService )。然后,我们创建了一个自动配置类,其中包含两个 bean 定义,这两个 bean 定义分别基于 application.properties 中的属性值条件性地创建。在 spring.factories 文件中,我们声明了这个自动配置类,以便 Spring Boot 在启动时能够自动加载它。

在此,继续用电视机的例子升华理解下

电视机类比

1.总体概念

2.Java的SPI

3.Spring Boot的自动配置

4.扩展性

  通过这种类比,电视机的插槽和自动设置功能为我们提供了一个直观的方式来理解 Java SPI 机制和 Spring Boot 的自动配置如何工作,以及它们如何为应用开发者提供便利。

6. SPI(Service Provider Interface)总结

   SPI ,即服务提供者接口,是一种特定的设计模式。它允许框架或核心库为第三方开发者提供一个预定义的接口,从而使他们能够为框架提供自定义的实现或扩展。

核心目标:

可插拔:第三方提供的服务或实现可以轻松地添加到或从系统中移除,无需更改现有的代码结构。

价值:

SPI与“开闭原则”

  “开闭原则”提倡软件实体应该对扩展开放,但对修改封闭。即在不改变现有代码的前提下,通过扩展来增加新的功能。

SPI如何体现“开闭原则”:

对扩展开放: SPI 提供了一种标准化的方式,使第三方开发者可以为现有系统提供新的实现或功能。

对修改封闭:添加新的功能或特性时,原始框架或库的代码不需要进行修改。

独立发展:框架与其 SPI 实现可以独立地进化和发展,互不影响。

  总之, SPI 是一种使软件框架或库更加模块化、可扩展和可维护的有效方法。通过遵循“开闭原则”, SPI 确保了系统的稳定性和灵活性,从而满足了不断变化的业务需求。

到此这篇关于SPI机制在JDK与Spring Boot中的应用的文章就介绍到这了,更多相关JDK与Spring Boot应用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文