Java SPI 机制
SPI(Service Provider Interface) 服务提供发现接口。
不同于微服务中的服务发现,其本质是典型的面向接口编程,使用了策略模式,实现解耦。
同时SPI 使用的是一种 ”插件思维“,即服务提供者负责服务所有的维护,当替换服务提供方时不需要调用方修改代码及配置文件。
理解:
1、即我们只声明接口,具体的实现并不在程序中确定,而是由程序之外的配置掌控,用于具体实现的装配。
2、SPI机制是Java的一种服务发现机制,为了方便应用扩展。那什么是服务发现机制?简单来说,就是你定义了一个接口,但是不提供实现,接口实现由其他系统应用实现。你只需要提供一种可以找到其他系统提供的接口实现类的能力或者说机制。这就是SPI机制( Service Provider Interface)
组成:
服务:指接口和抽象类的集合。由服务调用方提供
服务提供者:是服务的特定实现
服务提供者可以以扩展的形式安装在Java平台的实现中,也就是将 jar 文件放入任意常用的扩展目录中。也可通过将提供者加入应用程序类路径路径,或者通过其他某些特定于平台的方式使其可用。
SPI规范:
1、在资源目录 resource/META-INF/services 中放置提供者配置文件 来标识服务提供者。文件名称是服务类型(接口)的全限定名(最终会通过这里的全限定名来反射创建对象)。
2、该文件包含一个具体提供者类的全限定名,每行一个。忽略各名称周围的空格、制表符和空行。可以使用 ‘#’ 标注注释。
3、该文件必须使用UTF-8编码
服务加载器:
java.util.ServiceLoader<S>
ServiceLoader是JDK官方提供的SPI机制下的服务加载器,S:要被此加载器加载的服务类型
返回 | 方法名 | 描述 |
Iterator<S> | iterator() | 以延迟方式加载此加载器服务的可用提供者。 |
static <S> ServiceLoader<S> | load(Class<S> service) | 针对给定服务类型创建新的服务加载器,使用当前线程的上下文类加载器。 |
static <S> ServiceLoader<S> | load(Class<S> service, ClassLoader loader) | 针对给定服务类型和类加载器创建新的服务加载器。 |
static <S> ServiceLoader<S> | loadInstalled(Class<S> service) | 针对给定服务类型创建新的服务加载器,使用扩展类加载器。 |
void | reload() | 清除此加载器的服务者缓存,以重载所有服务者。 |
String | toString() | 返回一个描述此服务的字符串。 |
JDK通过ServiceLoader类去ClassPath下的 “META-INF/services/”(此路径约定成俗) 路径里查找相应的接口实现类
SPI具体步骤如下:
1、定义一个接口及对应的方法
2、编写该接口的一个实现类
3、在META-INF/services/ 目录下,创建一个以接口全路径命名的文件
4、文件内容为具体实现类的全路径名,每行一个
5、在代码中通过java.util.ServiceLoader 来加载具体的实现类
示例:
mysql-connector-java-5.1.47.jar为例
1、接口服务Driver,全限定名为:
java.sql.Driver
2、服务配置
在其jar包的META-INF/services/ 目录下有个 java.sql.Driver 文件,内容为:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
3、调用方使用ServiceLoader加载所有提供的服务 (java.sql.DriverManager中的static{loadInitialDrivers();})
Dubbo SPI:
http://dubbo.apache.org/docs/v2.7/dev/spi/
SPI是一种服务发现机制,本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类,这样可以在运行时,动态为接口替换实现类。
Dubbo正式通过SPI机制实现了众多的扩展功能,而且Dubbo没有使用Java原生的SPI机制,而是对齐进行了增强和改进。
SPI在Dubbo中应用很多,包括协议扩展、路由扩展、序列化扩展等。
使用方式可以在META-INF/dubbo目录下配置:
key=com.xxx.value
然后通过dubbo的ExtensionLoader按照指定的key加载对应的实现类,这样做的好处就是可以按需加载,性能上得到优化。
Spring SPI机制应用:
在Spring boot 中也有一种类似的加载机制,它在META-INFO/spring.factories文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化。
spring.factories机制的实现实际上是仿照Java中SPI扩展机制实现的,这种自定义的SPI 机制就是Spring Boot Starter 实现的基础
spring.factories文件作用:
1、里面写了自动配置的相关的类型,帮助我们加载项目以外的bean到Spring容器中
由于@ComponentScan注解只能扫描spring-boot项目包内的bean并注册到spring容器中,因此需要@EnableAutoConfiguration注解来注册项目包外的bean。而spring.factories文件,可用来记录项目包外需要注册的bean类名
2、提供SDK或者Spring Boot Starter给被人使用时,让使用者只需要很少或者不需要进行配置,然后在服务中引入我们的jar包即可
spring.factories的实现原理:
spring-core 包里定义了SpringFactoriesLoader 类,这个类实现了检索META-INF/spring.factories文件,并获取指定接口的配置的功能。在这个类中定义了两个对外的方法:
①:loadFactories 根据接口类获取其实现类的实例,这个方法返回的是对象列表
②:loadFactoryNames 根据接口获取其接口类的名称,这个方法返回的是类名的列表
上面两个方法的关键都是从指定的ClassLoader中获取spring.factories文件,并解析得到类名列表,具体代码如下:
loadFactoryNames:
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
loadSpringFactories:
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
HashMap result = new HashMap();
try {
Enumeration urls = classLoader.getResources("META-INF/spring.factories");
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}
在这个方法中会遍历整个ClassLoader 中所有Jar包下的spring.factories文件,也就是我们可以在自己jar中配置spring.factories文件,不会影响到其他地方的配置,也不回被别人的配置覆盖
spring.factories的是通过Properties解析得到的,如果一个接口希望配置多个实现类,可以用","分割,如:
# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
spring.factories的使用示例:
如,监听器bean的加载:
SpringApplication.run(UvcpsTranscodeApplication.class, args);
run()方法中的new SpringApplication(primarySources).run(args);
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.bootstrappers = new ArrayList<>(getSpringFactoriesInstances(Bootstrapper.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 从spring.factories文件中获取监听器(ApplicationListener类型),并创建其实例,添加到Spring容器中
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
在项目启动时,从spring.factories文件中获取监听器(ApplicationListener类型),并创建其实例,添加到Spring容器中
END.