美团二面:深入理解Java中的SPI机制及其在SpringBoot自动装配中的应用
SPI概述
在面试中,许多人可能听说过一个术语,SPI扩展。面试官常常会问,SpringBoot是如何实现自动装配的? 通常,回答涉及到Spring的SPI扩展机制,并提及spring.factories
文件及EnableAutoConfiguration
。这足以证明你的理解。
回想四五年前,当我第一次在面试中提及SPI动态扩展机制时,面试官惊讶的表情让我意识到,掌握这些术语是多么重要。甚至在我自己对SPI的理解上,也曾显得有些模糊。然而,如果想要在面试中给人留下深刻印象,就必须先掌握这些概念。
今天,我们将专注于Java自带的SPI扩展机制,逐步了解其工作原理。
SPI的定义和工作原理
什么是SPI?
SPI的全称是Service Provider Interface
,翻译为服务提供者接口。它本质上是一种服务发现机制,帮助我们在代码中灵活地发现和使用服务提供者。
为了便于理解,我们可以通过一个例子来说明。在Spring项目中,开发人员通常会在服务层中定义接口,并通过Spring的依赖注入机制(如@Autowired
)来获得该接口的实现。调用服务层时,开发者只需依赖接口,不必关注具体实现。
简而言之,服务提供者提供实现类,而调用者只需调用接口。这种接口的实现方式在一定程度上降低了代码的耦合性,提升了代码的灵活性和可维护性。
SPI与API的区别
虽然SPI与API都涉及接口,但它们的侧重点有所不同。在API中,接口通常是服务提供者向调用者提供的功能列表,而在SPI中,更加强调的是,调用者对服务实现的约束。服务提供者则需依据这些约束来实现服务,以便被调用者发现和使用。
在Java中,SPI的实现方式是:调用者定义接口,并根据该接口规范来实现服务。这样一来,调用者就能通过某种机制发现服务提供者并调用其实现。
接口的定义
接下来,我们将通过一个智能家居系统的例子来说明SPI的应用。
现代的智能家居设备通常支持通过同一款手机应用进行控制。假设我在客厅、卧室和书房分别安装了三款不同品牌的空调,并将它们接入到同一个应用中。无论空调的型号如何,用户对其操控的方式都是相似的,主要功能包括:开关、选择模式和调节温度。
为了避免不同型号空调各自实现独立接口的复杂性,我们可以定义一套标准接口规范。这样,无论未来更新或增添何种空调,只需遵循这一规范,就能够与系统顺利集成。
我们可以创建一个名为aircondition-standard
的新项目,定义如下接口:
public interface IAircondition {
String getType(); // 获取型号
void turnOnOff(); // 开关
void adjustTemperature(int temperature); // 调节温度
void changeModel(int modelId); // 模式变更
}
此接口将被服务实现方使用,并打包成jar文件供后续使用。
服务实现
一旦接口规范设定完毕,首先来实现这一接口的服务提供者将是挂式空调。我们创建一个名为aircondition-hanging-type
的新项目,并引入刚才打好的jar包:
<dependency>
<groupId>com.cn.hydra</groupId>
<artifactId>aircondition-standard</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
然后,我们实现服务类:
public class HangingTypeAircondition implements IAircondition {
public String getType() {
return "HangingType";
}
public void turnOnOff() {
System.out.println("挂式空调开关");
}
public void adjustTemperature(int temperature) {
System.out.println("挂式空调调节温度");
}
public void changeModel(int modelId) {
System.out.println("挂式空调更换模式");
}
}
在resources
目录下创建META-INF/services
文件夹,并以com.cn.hydra.IAircondition
命名创建文件,写入实现类的全限定名。
接下来,我们可以创建另一个立式空调的项目aircondition-vertical-type
,实现相同的接口。
服务发现
至此,我们已经完成了两个服务提供者的实现。接下来,关键的一步是服务发现。Java中的SPI机制可以帮助我们完成这个过程。
我们创建一个新项目aircondition-app
,并引入之前打好的两个jar包:
<dependencies>
<dependency>
<groupId>com.cn.hydra</groupId>
<artifactId>aircondition-hanging-type</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.cn.hydra</groupId>
<artifactId>aircondition-vertical-type</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
作为调用者,无需关注具体的实现类,而是通过接口调用服务提供者的方法。我们将实现一个方法,根据类型调用相应空调的开关方法:
public class AirconditionApp {
public static void main(String[] args) {
new AirconditionApp().turnOn("VerticalType");
}
public void turnOn(String type) {
ServiceLoader<IAircondition> load = ServiceLoader.load(IAircondition.class);
for (IAircondition aircondition : load) {
System.out.println("检测到:" + aircondition.getClass().getSimpleName());
if (type.equals(aircondition.getType())) {
aircondition.turnOnOff();
}
}
}
}
测试时,我们通过定义的接口IAircondition
发现了两个实现类,并根据参数调用特定实现类的方法,而没有直接涉及到服务实现类。
SPI的实现原理
了解了SPI的工作流程后,我们接下来分析其实现。核心在于ServiceLoader
类。它实现了Iterable
接口,服务发现的核心在于其iterator()
方法中。
ServiceLoader
的load()
方法返回的结果可以通过for
循环访问,迭代器将服务提供者的实现类依次返回。其过程主要依赖Java反射机制。
SPI的实际应用
SPI的常见应用之一是日志框架slf4j
。它通过SPI实现了对其他具体日志框架的插拔式接入。slf4j
本身作为日志门面,不提供具体实现,而是需要绑定其他日志框架才能实现记录功能。
例如,我们可以使用log4j2
作为具体的日志实现,只需在pom.xml中引入slf4j-log4j12
即可。
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>2.0.3</version>
</dependency>
通过查看jar包的结构,我们会发现,尽管我们在pom中引入的是slf4j-log4j12
,实际使用的是slf4j-reload4j
。这是因为随着log4j1.x
的EOL,slf4j
在构建阶段会自动重定向到slf4j-reload4j
。
结论
Java中的SPI为服务发现与调用提供了一种灵活的机制,使开发者能够通过接口将服务调用与服务提供者分离。这一机制在扩展和集成第三方服务时尤为方便。尽管SPI也存在加载所有实现类的潜在缺陷,但总的来看,它为我们提供了一个有效的框架扩展思路。