TL;DR
前一篇详细介绍了JDK
中的SPI
机制,今天来看看Spring
框架中是如何使用SPI
来实现可扩展性的,然后与JDK
的实现进行一下对比,再看看Spring
是如何应用SPI
的,最后,我们基于SPI
来动手实现一个Spring
的扩展。
本文大纲如下:
Spring
的SPI
实现- 与
JDK
的SPI
的差异 Spring
中SPI
的应用- 基于
SPI
实现Spring
扩展
其实,明白了JDK
中的SPI
,Spring
的SPI
实现原理是一样的,所以,本篇的内容会比较简单。
0x01 Spring的SPI实现
SPI
分为多个角色:Service
、Service Provider
、ServiceLoader
和资源文件(META-INF
目录下)。
Spring
框架的SPI
实现也离不开这几个角色。
首先,我们在Spring
框架的源代码(或Spring Boot
源代码)的资源文件目录下,可以找到META-INF
文件夹,里面可以找到spring.factories
文件,这个文件就是Spring
实现SPI
机制所需要的资源文件,里面会定义好一些扩展接口及实现类的信息,文件格式稍后再看。
有了这个spring.factories
文件,那服务加载器肯定会读取这个路径,在源码中搜索字符串spring.factories
,即可定位到一个类:SpringFactoriesLoader
,这是一个用final
修饰的类,是不可以被继承的。它的注释中第一句就明确的说明了这个类是用于Spring
框架内部的一般用途的工厂加载机制的。主要有loadFactories()
和loadFactoryNames()
这两个公有方法,这两个方法的用途注释已经解释得很清楚了,第一个是加载和实例化资源文件(META-INF/spring.factories
)中的对应的工厂的实现类列表,另一个是加载资源文件中对应的工厂的实现类的全路径名称列表。
按上面的描述,剩下的Service
和Service Provider
已经定义在资源文件中了,那我们现在看一下资源文件spring.factories
的具体内容。
上图是spring-boot
项目resources
目录下的META-INF/spring.factories
文件的内容。
SpringFactoriesLoader
类的注释中有对这个文件的格式做出相关的说明:
即,spring.factories
文件的格式必须是Properties
的格式,也就是键值对的格式,其中键(也就是key
)是接口或抽象类的全路径名称,值(也就是value
)是逗号分隔的实现类名的列表,也必须是全路径名称。
以spring-boot
下的spring.factories
文件的第一个定义为例:
# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
这里的org.springframework.boot.env.PropertySourceLoader
就是服务(即SPI
中的Service
角色)的定义,org.springframework.boot.env.PropertiesPropertySourceLoader
和org.springframework.boot.env.YamlPropertySourceLoader
就是服务提供者(即SPI
中的Service Provider
角色)的定义了。
到这里,SPI
中的四种角色就齐了。
那Spring
的SPI
如何使用的呢?
以Spring Boot
为例,我们启动程序的时候入口是SpringApplication#run()
方法,我们打开这个方法:
上图就是SpringApplication#run()
方法的具体实现,其中有一行是调用了getSpringFactoriesInstances()
方法,我们再看一下该方法的实现:
该方法里有一行调用SpringFactoriesLoader.loadFactoryNames(type, classLoader)
,它的结果直接作为了LinkedHashSet
的初始化参数。这一行实是在使用服务加载器SpringFactoriesLoader
来加载所有对应类型的实现了。
现在我们清楚了Spring
中SPI
是怎么工作的了。
0x02 与JDK的SPI的差异
其实明白了JDK
的SPI
实现,Spring
的实现几乎一样,唯一的差别就是资源文件的名称和内容了。
JDK
的SPI
对应的资源文件的名称必须是服务的全路径名称,而内容就是具体实现的类的全路径名称,可以有多个实现,但一行只能放一个实现;另外就是一个资源文件对于一个服务的定义;
再看Spring
的SPI
对应的资源文件,该文件的名称是固定的,为spring.factories
,内容必须是Properties
格式,也可以有多组,但一个键值对应于一个服务的定义,如果一个服务有多个实现,可以在value
中用逗号将多个实现的全路径类名分隔开。如果需要换行的号,就像前面的截图那样,用\
结尾,以此表示下一行是当前行的续行。
另外,就是根据使用场景的不同,两种实现的服务加载器中定义的方法不同,Spring
的SPI
实现支持更多的使用场景。
0x03 Spring中SPI的应用
我们还是以上面的那个示例来看SPI
在Spring
中的应用。
服务的定义:
/**
* Strategy interface located via {@link SpringFactoriesLoader} and used to load a
* {@link PropertySource}.
*
* @author Dave Syer
* @author Phillip Webb
* @since 1.0.0
*/
public interface PropertySourceLoader {
/**
* Returns the file extensions that the loader supports (excluding the '.').
* @return the file extensions
*/
String[] getFileExtensions();
/**
* Load the resource into one or more property sources. Implementations may either
* return a list containing a single source, or in the case of a multi-document format
* such as yaml a source for each document in the resource.
* @param name the root name of the property source. If multiple documents are loaded
* an additional suffix should be added to the name for each source loaded.
* @param resource the resource to load
* @return a list property sources
* @throws IOException if the source cannot be loaded
*/
List<PropertySource<?>> load(String name, Resource resource) throws IOException;
}
PropertiesPropertySourceLoader
实现:
/**
* Strategy to load '.properties' files into a {@link PropertySource}.
*
* @author Dave Syer
* @author Phillip Webb
* @author Madhura Bhave
* @since 1.0.0
*/
public class PropertiesPropertySourceLoader implements PropertySourceLoader {
private static final String XML_FILE_EXTENSION = ".xml";
@Override
public String[] getFileExtensions() {
return new String[] { "properties", "xml" };
}
@Override
public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
Map<String, ?> properties = loadProperties(resource);
if (properties.isEmpty()) {
return Collections.emptyList();
}
return Collections
.singletonList(new OriginTrackedMapPropertySource(name, Collections.unmodifiableMap(properties), true));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private Map<String, ?> loadProperties(Resource resource) throws IOException {
String filename = resource.getFilename();
if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) {
return (Map) PropertiesLoaderUtils.loadProperties(resource);
}
return new OriginTrackedPropertiesLoader(resource).load();
}
}
YamlPropertySourceLoader
实现:
/**
* Strategy to load '.yml' (or '.yaml') files into a {@link PropertySource}.
*
* @author Dave Syer
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.0.0
*/
public class YamlPropertySourceLoader implements PropertySourceLoader {
@Override
public String[] getFileExtensions() {
return new String[] { "yml", "yaml" };
}
@Override
public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
if (!ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", null)) {
throw new IllegalStateException(
"Attempted to load " + name + " but snakeyaml was not found on the classpath");
}
List<Map<String, Object>> loaded = new OriginTrackedYamlLoader(resource).load();
if (loaded.isEmpty()) {
return Collections.emptyList();
}
List<PropertySource<?>> propertySources = new ArrayList<>(loaded.size());
for (int i = 0; i < loaded.size(); i++) {
String documentNumber = (loaded.size() != 1) ? " (document #" + i + ")" : "";
propertySources.add(new OriginTrackedMapPropertySource(name + documentNumber,
Collections.unmodifiableMap(loaded.get(i)), true));
}
return propertySources;
}
}
这个服务是对Spring
的配置文件的格式定义的扩展,两个实现分别是基于Properties
格式的实现和yaml
格式的实现。
上面的代码是服务的定义,以及两种服务的实现。具体来说就是Spring Boot
对默认配置文件的解析处理的定义。
到这里,我们还不知道Spring
是什么时候,在哪里加载该服务的定义,然后去哪里读取properties
文件或yml
文件的。也就是说,整个流程还没有串联起来,流程不清晰。
于是,我们继续看SpringAppliction#run()
方法,里面有一行调用ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
,再进去看方法prepareEnvironment
的具体实现:
这里首先会获取或创建环境对象,然后配置环境对象,我们看一下配置环境方法configureEnvironment
的具体实现:
这个方法是委托实现configurePropertySources()
和configureProfiles()
方法的模版方法,我们看一下这一行调用configurePropertySources(environment, args);
,这里面通过环境去获取了PropertySource
列表,并判断了defaultProperties
和addCommandLineProperties
,这里并没有使用服务加载器的代码,所以继续看源码。
回到prepareEnvironment
方法,接下来有一行调用listeners.environmentPrepared(environment);
,再后面就绑定环境对象了,即这一行的调用bindToSpringApplication()
,再后面就是配置与环境绑定的调用ConfigurationPropertySources.attach(environment);
了。
按道理,对配置文件的定位工作应该是在绑定到环境之前,因为就Spring
的使用经验来说,配置文件本身是可以分环境来进行命名和配置的,所以,对应到这里的代码,应该就是在绑定环境之前的那一行调用了,即:listeners.environmentPrepared(environment);
。
我们进一步看这个方法的实现:
这里是通知所有关注ConfigurableEnvironment
的监听器,我们看一下listener.enviromentPrepared()
方法的具体实现:
原来是调用了EventPublishingRunListener
的事件发布方法,这里使用的是事件驱动的方式进行事件的派发。
而且,这个EventPublishingRunListener
本身也是基于SPI
机制实现的:
我们接着看前面的事件派发,这里派发出去的事件类是ApplicationEnvironmentPreparedEvent
,如果你清楚事件驱动模式的调用的话,这里就简单了,因为所有的关注事件ApplicationEnvironmentPreparedEvent
的监听器都需要实现一个处理该事件的方法,所以,我们只需要去到该事件类,然后查看哪里监听器监听了该事件就可以定位到最终的目标了。
我们继续打开类ApplicationEnvironmentPreparedEvent
,在IDEA
中,按住ctrl
再单击类名即可出现所有使用了该类的位置,如下图所示:
我们可以看到,有一个ConfigFileApplicationListener
监听器,定义了方法:
onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event)
我们看一下该方法的实现,第一行调用了loadPostProcessors()
方法,我们进去看一下具体的实现:
这里加载了所有的EnvironmentPostProcessor.class
服务的实现,然后方法的第二行把当前对象也加到了EnvironmentPostProcessor.class
服务提供者的列表中,然后对找到的这些服务提供者循环调用进行后续环境处理操作。
对这些实现一个个查找发现,当前类的处理方法的实现为:
进一步跟进,发现它调用了内部类Loader#load()
方法:
我们定位到这个Loader
内部类,发现它的构造方法中就使用服务加载器对PropertySourceLoader.class
服务进行了查找:
前面的调用了Loader#load()
方法,我们再看这个方法:
进一步定位到:
再继续查看就是根据条件进行判断并使用不同的实现去解析不同的文件了。
现在,我们回到PropertySourceLoader.class
服务的两个实现看一下(代码已经贴在前面了)。
两个实现分别定义了各自所支持的不同的文件后缀,和上面的load()
方法里的判断是匹配的。
那Spring
默认查找的是application.properties
(或application.yaml
),这个文件名application
是哪里定义的呢?
其实这个文件名就定义在ConfigFileApplicationListener
类中,同时还定义了配置文件默认的路径,它还支持通过不同的参数来修改这个配置文件的路径和名称等,有兴趣可以自行查看该类的代码。
现在我们再来回答前面的问题:Spring
是什么时候,在哪里加载该服务的定义,然后去哪里读取properties
文件或yml
文件的?
答案就是ConfigFileApplicationListener
类了,SpringApplication#run()
方法在启动时,创建了环境对象之后,会通过事件派发的机制广播一系列的事件给一系列的SpringApplicationRunListener
实现者,它们会关注各自所关心的事件,并在接收到相应的事件后,做一些相应的处理。
0x04 基于SPI实现Spring扩展
现在我们以PropertySourceLoader.class
服务为例,结合SpringApplication
的启动过程分析了Spring
如何使用它的SPI
机制的。
下面又是动手的环境,我们还是以这个服务为例,为Spring
实现一种新的配置文件格式的支持。
toml
是在golang
生态中广泛使用的一种配置文件格式,我们现在就动手为Spring
实现对toml
文件格式的支持吧。
最新的toml
标准为v1.0.0-rc
,官方推荐的该版本的Java
实现为https://github.com/tomlj/tomlj
,我们就使用这个库来实现上面的扩展。
首先定义服务提供者的类名TomlPropertySourceLoader
,并实现PropertySourceLoader
接口,然后实现对应的方法:
/**
* Strategy to load '.tml' (or '.toml') files into a {@link PropertySource}.
*
* @author zhaoyang.
*/
public class TomlPropertySourceLoader implements PropertySourceLoader {
@Override
public String[] getFileExtensions() {
return new String[] { "tml", "toml" };
}
@Override
public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
if (!ClassUtils.isPresent("org.tomlj.Toml", null)) {
throw new IllegalStateException(
"Attempted to load " + name + " but tomlj was not found on the classpath");
}
TomlParseResult tomlParseResult = Toml.parse(resource.getInputStream());
Map<String, Object> resultMap = tomlParseResult.toMap();
if (resultMap.isEmpty()) {
return Collections.emptyList();
}
return Collections.singletonList(new OriginTrackedMapPropertySource(name, resultMap));
}
}
除此之外,还要在resources
目录新建META-INF
目录,并在该目录下新建文件spring.factories
,文件内容如下:
org.springframework.boot.env.PropertySourceLoader=\
me.zy.std.spi.spring.extension.TomlPropertySourceLoader
然后,在resources
目录新建application.toml
文件,内容如下:
# 这是一个 TOML 文档。
title = "TOML 示例"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00 # 第一类日期时刻
[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# 允许缩进(制表符和/或空格),不过不是必要的
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data = [ ["gamma", "delta"], [1, 2] ]
# 数组中是可以换行的
hosts = [
"alpha",
"omega"
]
最后,新建一个Spring Boot
程序的启动类:
/**
* @author zhaoyang.
*/
@SpringBootApplication
public class SpringTomlTextApplication {
public static void main(String[] args) {
SpringApplication.run(SpringTomlTextApplication.class, args);
}
@Value("${title}")
private String title;
@Bean
public CommandLineRunner test() {
return (args -> {
System.out.println("Server started...");
System.out.println("\n\n\n");
System.out.println("title = " + title);
});
}
}
我们从配置文件中,注入一个title
属性,并在应用启动后,将配置文件中配置的title
属性的值输出到配置文件。
启动程序后,可以看到控制台的输出:
成功的从application.toml
配置文件中读取出了我们所配置的属性值。
至此,基于Spring
的SPI
实现就分析完了。
0x05 总结
这一篇我们分析了Spring
框架的SPI
扩展,我们仅以PropertySourceLoader.class
服务为例进行了分析,并动手为此服务实现了toml
的扩展。
本质上,Spring
的SPI
与Java
的SPI
实现大同小异,但Spring
在资源文件方面做了优化,将多个服务所需要的多个资源配置文件优化为统一的文件spring.factories
,同时,文件的格式也是标准的Properties
格式,这样对开发者要友好很多。
在Spring
框架的源代码中,大量的都使用了SPI
机制进行扩展,有的扩展甚至有几十个之多,例如:EnableAutoConfiguration.class
。
SPI
机制带来的可扩展性是得到了业界的认可的。
接下来会分析Dubbo
框架中的SPI
的实现,看看Dubbo
的实现又有什么区别。
好了,这一篇的内容到此就结束了,欢迎各种反馈和交流!