SPI机制
1、什么是SPI,有什么作用?
SPI ,(service provider interface),翻译过来就是,服务者提供接口,是一种服务发现机制。
他允许在 “运行时动态的加载实现特定接口的类”,而不需要在代码中显示指定该类,从而实现服务的解耦。如图所示,调用方制定接口标准,服务提供者实现对应的接口来提供服务。这样调用方就可以动态的使用不同服务提供方的服务了。
比如最经典的jdbc,提供了统一的接口 java.sql.Driver,mysql或者oracle实现这个接口,提供自己的服务。这样,我们在项目使用的过程中,只要添加对应的依赖,就可以使用对应的数据库了。
2、spi是如何使用的?
1、首先作为调用方,制定一个统一的接口。
package com.example.testspi;
/**
* TestSpiService
*
* @since 2024/12/28
*/
public interface TestSpiService {
void test();
}
2、作为服务方,依赖调用方,然后实现这个接口 (注意了,这里不是调用方依赖服务方了)
package com.example.testspi.provider;
import com.example.testspi.TestSpiService;
/**
* Provider01
*
* @since 2024/12/28
*/
public class Provider01 implements TestSpiService {
@Override
public void test() {
System.out.println("Provider01 is execute...");
}
}
package com.example.testspi.provider;
import com.example.testspi.TestSpiService;
/**
* Provider01
*
* @since 2024/12/28
*/
public class Provider02 implements TestSpiService {
@Override
public void test() {
System.out.println("Provider02 is execute...");
}
}
3、然后在服务方的资源下面,归档一个文件。
注意:
- 文件归档的路径必须是 META-INF/services
- 文件名必须是要实现的接口:com.example.testspi.TestSpiService
- 文件内容是实现这个接口的实现类,如果有多个就换行写
com.example.testspi.provider.Provider01
com.example.testspi.provider.Provider02
4、如何调用?
使用 ServiceLoader.load(Class<S> service)进行调用
@Test
void testSpi() {
ServiceLoader<TestSpiService> load = ServiceLoader.load(TestSpiService.class);
for (TestSpiService next : load) {
next.test();
}
}
-----
Provider01 is execute...
Disconnected from the target VM, address: '127.0.0.1:64558', transport: 'socket'
Provider02 is execute...
3、源码跟踪
1、ServiceLoader.load(Class<S> service)方法会创建一个 ServiceLoader对象,这个对象会生成一个懒加载的迭代器。
# 源码
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
2、当遍历ServiceLoader对象时,会调用迭代器的next方法,然后调用上述懒加载迭代器的hasNextService方法,然后获取对应文件里面的全限定名,然后通过反射的方式,进行调用。
这里解释了为什么对归档的文件路径和名称都有要求。
4、Springboot中的spring.factories文件。
Springboot启动的时候,会去classpath下的META-INF/spring.factories路径加载对应的内容,然后根据其中定义的key来加载对应value的自动配置类。
--- 这个也是用到了SPI机制。
为什么需要这个文件呢?
我们知道springboot启动的时候会自动扫描指定路径下面带@component的bean,但是如果是依赖了第三方组件,我们没有办法穷举这些路径。spring.factories
文件的作用是让 Spring Boot 在启动时自动加载第三方 JAR 包中的配置类,并通过自动配置机制注册 Bean
和其他组件。这大大简化了开发者的工作,特别是在使用第三方库时,减少了大量的手动配置。