Spring Boot不仅是简化Spring应用开发的工具,它还融合了许多先进的机制。本文深入探讨了Spring Boot中与Java的标准SPI相似的机制,揭示了它的工作原理、应用场景及与标准SPI的异同。文章通过实际代码示例为你展示了如何在Spring Boot中使用这一机制,并以形象的比喻帮助你理解其背后的思想。
SPI
(Service Provider Interface
) 是一种服务发现机制,它允许第三方提供者为核心库或主框架提供实现或扩展。这种设计允许核心库/框架在不修改自身代码的情况下,通过第三方实现来增强功能。
JDK原生的SPI:
JDK
的SPI
主要通过在META-INF/services/
目录下放置特定的文件来指定哪些类实现了给定的服务接口。这些文件的名称应为接口的全限定名,内容为实现该接口的全限定类名。ServiceLoader
类使用Java
的类加载器机制从META-INF/services/
目录下加载和实例化服务提供者。例如,ServiceLoader.load(MyServiceInterface.class)
会返回一个实现了MyServiceInterface
的实例迭代器。JDK
原生的SPI
每次通过ServiceLoader
加载时都会初始化一个新的实例,没有实现类的缓存,也没有考虑单例等高级功能。Spring的SPI:
Spring
的SPI
不仅仅是服务发现,它提供了一套完整的插件机制。例如,可以为Spring
定义新的PropertySource
,ApplicationContextInitializer
等。JDK
的SPI
不同,Spring
的SPI
与其IoC
(Inversion of Control
) 容器集成,使得在SPI
实现中可以利用Spring
的全部功能,如依赖注入。Spring
提供了基于条件的匹配机制,这允许在某些条件下只加载特定的SPI
实现,例如,可以基于当前运行环境的不同来选择加载哪个数据库驱动。Spring
允许通过spring.factories
文件在META-INF
目录下进行配置,这与JDK
的SPI
很相似,但它提供了更多的功能和灵活性举个类比的例子:
想象我们正在建造一个电视机,SPI
就像电视机上的一个USB
插口。这个插口可以插入各种设备(例如U盘、游戏手柄、电视棒等),但我们并不关心这些设备的内部工作方式。这样只需要提供一个标准的接口,其他公司(例如U盘制造商)可以为此接口提供实现。这样,电视机可以在不更改自己内部代码的情况下使用各种新设备,而设备制造商也可以为各种电视机制造兼容的设备。
总之,SPI
是一种将接口定义与实现分离的设计模式,它鼓励第三方为一个核心产品或框架提供插件或实现,从而使核心产品能够轻松地扩展功能。
在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
端口,这就是它与外部设备连接的接口。
HDMI
端口的标准。在上面的代码中,MessageService
接口就是这个“HDMI
端口”,定义了如何与外部设备交流。HDMI
接口生产各种设备,如游戏机、蓝光播放器或流媒体棒。在代码中,HelloMessageService
和HiMessageService
就是这些“HDMI
设备”。每个设备/实现都有其独特的输出,但都遵循了统一的HDMI
标准(MessageService
接口)。HDMI
设备,它通常都会在包装盒上明确标明“适用于HDMI
”。这就像一个标识,告诉用户它可以连接到任何带有HDMI
接口的电视。在SPI
的例子中,META-INF/services/
目录和其中的文件就像这个“标签”,告诉JDK
哪些类是MessageService
的实现。HDMI
设备到电视上,并切换到正确的输入频道,电视就会显示该设备的内容。类似地,在代码的这个步骤中,ServiceLoader
就像电视的输入选择功能,能够发现和使用所有已连接的HDMI
设备(即MessageService
的所有实现)。Spring
官方在其文档和源代码中多次提到了SPI
(Service Provider Interface
)的概念。但是,当我们说“Spring
的SPI
”时,通常指的是Spring
框架为开发者提供的一套可扩展的接口和抽象类,开发者可以基于这些接口和抽象类实现自己的版本。
在Spring
中,SPI
的概念与Spring Boot
使用的spring.factories
文件的机制不完全一样,但是它们都体现了可插拔、可扩展的思想
Spring的SPI:
Spring
的核心框架提供了很多接口和抽象类,如BeanPostProcessor
, PropertySource
, ApplicationContextInitializer
等,这些都可以看作是Spring
的SPI
。开发者可以实现这些接口来扩展Spring
的功能。这些接口允许开发者在Spring
容器的生命周期的不同阶段介入,实现自己的逻辑。Spring Boot的spring.factories机制:
spring.factories
是Spring Boot
的一个特性,允许开发者自定义自动配置。通过spring.factories
文件,开发者可以定义自己的自动配置类,这些类在Spring Boot
启动时会被自动加载。SpringFactoriesLoader
的使用,尤其是通过spring.factories
文件来加载和实例化定义的类,可以看作是一种特定的SPI
实现方式,但它特定于Spring Boot
。在传统的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
应用程序,具体来说是DemoApplication
类。这个核心应用程序需要从某个服务(即MessageService
)获取并打印一条消息。MessageService
接口就是这个"USB
插口"。它为电视机提供了一个标准化的接口,即getMessage()
方法,但没有规定具体怎么实现。HelloMessageService
和HiMessageService
。它们为"USB
插口"(即MessageService
接口)提供了不同的设备或实现。一个显示“Hello from HelloMessageService!”
,另一个显示“Hi from HiMessageService!”
。USB
设备(即MessageService
的实现)并尝试从中获取消息时,这个“魔法盒子”会介入,并为每条消息添加“[Processed by Spring SPI]”
。Java
的配置方式,即MessageServiceConfig
类。这个“使用说明书”指导Spring
容器如何创建并管理MessageService
的实例,并且还指导它如何使用“魔法盒子”(即MessageServicePostProcessor
)来处理消息。总的来说,与之前的例子相比,这个新示例提供了一个更加动态的场景,其中Spring
的BeanPostProcessor
扩展点允许我们拦截并修改bean
的行为,就像一个能够干预并改变电视机显示内容的智能设备。
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
应用,就像DemoApplication
。电视机是查看不同信号源或通道的设备,我们的应用程序是为了运行并使用不同的服务实现。MessageService
接口。USB
插口是一个标准的接口,它允许连接各种设备,就像MessageService
接口允许有多种实现方式。HelloMessageService
和HiMessageService
。每个USB
设备在插入电视机后都有特定的内容或功能,这就像我们的每个服务实现返回不同的消息。spring.factories
文件。当我们将USB
设备插入电视机时,电视机会检查设备的信息或内容,spring.factories
文件告诉Spring Boot
哪些服务实现是可用的,就像电视机知道有哪些USB
设备被插入。SpringFactoriesLoader
。当我们要从电视机上查看USB
内容时,电视机会扫描并显示内容。同样,当DemoApplication
运行时,SpringFactoriesLoader
会查找并加载在spring.factories
文件中列出的服务实现。简化解释:
USB
设备到电视机,期望电视机能够识别并显示该设备的内容。USB
设备的内容就是从MessageService
实现类返回的消息。spring.factories
文件就像电视机的内置目录,告诉电视机哪些USB
设备是已知的和可以使用的。DemoApplication
(电视机)运行时,它使用SpringFactoriesLoader
(USB
扫描功能)来检查哪些服务(USB
设备)是可用的,并输出相应的消息(显示USB
内容)。总结:在这个Spring Boot
的SPI
例子中,我们展示了核心Spring
应用如何自动地识别和使用spring.factories
文件中注册的实现,这与电视机自动地识别和使用所有插入的USB
设备有相似之处。
数据库驱动的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
机制的具体工作方式:
定义服务接口:
在这里,接口已经由Java
平台定义,即java.sql.Driver
。
为接口提供实现:
各大数据库厂商(如Oracle
, MySQL
, PostgreSQL
等)为其数据库提供了JDBC
驱动程序,它们都实现了java.sql.Driver
接口。例如,MySQL
的驱动程序中有一个类似于以下的类:
public class com.mysql.cj.jdbc.Driver implements java.sql.Driver {
// 实现接口方法...
}
直接上图:
注册服务提供者:
对于MySQL
的驱动程序,可以在其JAR
文件的META-INF/services
目录下找到一个名为java.sql.Driver
的文件,文件内容如下:
com.mysql.cj.jdbc.Driver
直接上图:
看到这里是不是发现和第2
节举的JDK SPI
的例子一样?体会一下。
使用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
机制来获取数据库连接。
这种机制有点类似于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
。
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。