1.SPI是什么?
SPI 全称为服务提供者接口 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。可以轻松实现面向服务的注册与发现,完成服务提供与使用的解耦,并且可以实现动态加载。
上述概念,在各类博客都能搜到。但是如何理解呢?
(1)java中的服务
服务就是提供某种特殊功能的模块,例如某一模块提供数据库增删改查的服务。
直白理解:服务就是为他人提供某种需要。例如:你为公司提供劳动力,医生为你提供医疗服务。
(2)服务注册与发现
服务注册与发现是站在不同的角度表达相同的意思。例如,存在明星A和其粉丝B。对于粉丝B来说,粉丝B向明星A注册了自己。对于明星A来说,明星A发现了粉丝B。
(3)服务提供者与使用者解偶
在java具体实现中,解偶可以通过接口、实现类来体现。
依旧拿明星A和粉丝来举例。明星A提供一个卡帐号(java中接口),粉丝群体1向明星提供应援服务。对于明星A来说,他只需向粉丝群体1提供了一个卡帐号1,向粉丝群体2提供一个帐号2,这样就和多个粉丝群体解偶,不费吹灰之力(低耦合)就得到各个粉丝群体的应援。此外,如果扩展了粉丝群体3,只需要提供帐号3即可(可扩展性)。
(4)动态加载
java中动态加载简单理解就是,在程序需要的时候加载类。
拿明星A和粉丝群体来说,明星如果在需要的时候,可以告知粉丝群体n让其提供应援服务。
SPI其实就是java中利用接口作为桥梁,接口的实现类提供服务。
2. SPI在java和各种框架实现
2.1 SPI JAVA实现
当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/
目录里同时创建一个以服务接口命名的文件,文件内容是实现该服务接口的具体类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/
里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。 基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。JDK提供服务实现查找的一个工具类:java.util.ServiceLoader。
代码示例:
自定义接口和实现类:
public interface Subscribe {
void follow();
}
//接口实现1
public class MySubscribe implements Subscribe {
@Override
public void follow() {
System.out.println("测试自己的订阅");
}
}
//接口实现2
public class OtherSubscribe implements Subscribe{
@Override
public void follow() {
System.out.println("测试别人的订阅");
}
}
添加接口配置文件并配置实现类:
在resources目录下创建META-INF/services目录,并在目录下创建以接口全限定名为文件名的文件com.service.javaspi.Subscribe。文件内容为接口两个实现类的全限定名:
com.service.javaspi.OtherSubscribe
com.service.javaspi.MySubscribe
加载并启动:
注意:我这里启动方式是在SpringBoot应用程序的自定义监听器,普通的java程序可以直接在main()方法中加载。
@Component
public class ContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent>{
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("容器初始化结束,可以执行自定义初始化");
ServiceLoader<Subscribe> services = ServiceLoader.load(Subscribe.class);
for (Subscribe sub : services) {
sub.follow();
}
}
}
输出:
容器初始化结束,可以执行自定义初始化
测试别人的订阅
测试自己的订阅
2.2 SPI SpringBoot实现
总述:SpringBoot的SPI机制和Java SPI思想类似。SpringBoot在自动装配过程中,会扫描所有路径下的jar包,根据META-INF/spring.factories
文件中的类全限定名,由SpringFactoriesLoader加载各种类。
在SpringBoot启动过程第一步,创建SpringApplication对象时,会加载多个初始化器和监听器,加载的方法就是loadFactoryNames。该方法源码如下:
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// spring.factories文件的格式为:key=value1,value2,value3
// 从所有的jar包中找到META-INF/spring.factories文件
// 然后从文件中解析出key=factoryClass类名称的所有value值
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
// 取得资源文件的URL
Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
List<String> result = new ArrayList<String>();
// 遍历所有的URL
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
// 根据资源文件URL解析properties文件,得到对应的一组@Configuration类
Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
String factoryClassNames = properties.getProperty(factoryClassName);
// 组装数据,并返回
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
return result;
}
可以看到,它并没有采用Java SPI机制来加载这些类,不过原理差不多。都是通过一个配置文件,加载并解析文件内容,然后通过反射创建实例。
2.3 SpringBoot SPI自定义实现:
和Java SPI实现类似,首先在resources目录下创建META-INF/spring.factories文件,在文件中配置自定义初始化器。
org.springframework.context.ApplicationContextInitializer=com.service.javaspi.MyContextInitializer
然后定义一个MyContextInitializer类
@Component
public class MyContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>{
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
// TODO Auto-generated method stub
System.out.println("测试SpringBoot自定义SPI");
}
}
测试输出:
测试SpringBoot自定义SPI
注意,这里自定义的初始化器日志打印在SpringBoot启动记录中间部分。
参考链接: