一、什么是SPI机制
SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制,可以轻松实现面向服务的注册与发现,完成服务提供与使用的解耦,并且可以实现动态加载。
引入服务提供者就是引入了SPI接口的实现者,通过本地的注册发现获取到具体的实现类,轻松可插拔,SPI实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载,为某个接口寻找服务实现的机制。
我的理解就是上层提供接口,我们需要去实现,并且上层只需要根据我们的配置文件即可拿到我们的实现类(反射获取)。
二、SPI具体约定
2.1 Java SPI的具体约定为
当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/
目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/
里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。 基于这样一个约定就能很好的找到服务接口的实现类,而不需要在代码里制定。JDK提供服务实现查找的一个工具类:java.util.ServiceLoader
2.2 JDK提供服务实现查找的一个工具类
主程序通过java.util.ServiceLoder
动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到 实现类的全限定名,把类加载到JVM。
2.3 SPI的理解举例
假设JDK中支持音乐播放,且只支持mp3的播放,有些厂商想在这个基础之上支持mp4播放,有些想支持mp5播放,而这些厂商都是第三方厂商,如果没有提供SPI这种实现标准,那就只有修改JAVA的源代码了,那这个弊端也是显而易见的,而有了SPI标准,SUN公司只需要提供一个播放接口,在实现播放的功能上通过ServiceLoad的方式加载服务,那么第三方只需要实现这个播放接口,再按SPI标准的约定进行打包,再放到classpath下面就OK了,没有一点代码的侵入性。
三、应用场景
3.1 JDBC驱动
典型应用,JDBC的实现机制。通常各大厂商(如MySQL、Oracle)会根据一个统一的规范(java.sql.Driver)开发各自的驱动实现逻辑。客户端使用JDBC时不需要去改变代码,直接引入不同的SPI接口服务即可。MySQL的则是com.mysql.jdbc.Drive
,Oracle则是oracle.jdbc.driver.OracleDriver
3.2 日志框架SLF4J
SLF4J,即Java的简单日志门面( Simple Logging Facade for Java SLF4J),作为一个简单的门面或抽象,用来服务于各种各样的日志框架,比如java.util.logging、logback和log4j。SLF4J允许最终用户在部署时集成自己想要的日志框架(SPI机制)。简单来说,SLF4J是Java日志的一个标准或规范,logging、logback和log4j是对该规范的具体实现。可参考从SLF4J谈到依赖倒置原则、面向抽象编程、开闭原则
logback的jar包 LogbackServletContainerInitializer ServletContainerInitializer3.3 SpringBoot中的类SPI扩展机制
在SpringBoot的自动装配过程中,最终会加载META-INF/spring.factories
文件,而加载的过程是由SpringFactoriesLoader加载的。从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories
配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。(例如:数据库的自动配置功能)
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;
}
可以看到,它并没有采用JDK中的SPI机制来加载这些类,不过原理差不多。都是通过一个配置文件,加载并解析文件内容,然后通过反射创建实例。
假如你希望参与到SpringBoot初始化的过程中,现在我们又多了一种方式。我们也创建一个spring.factories文件,自定义一个初始化器。
org.springframework.context.ApplicationContextInitializer=com.youyouxunyin.config.context.MyContextInitializer
image.png
然后定义一个MyContextInitializer类
public class MyContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
System.out.println(configurableApplicationContext);
}
}
3.4 Dubbo中的应用
我们熟悉的Dubbo也不例外,它也是通过 SPI 机制加载所有的组件。同样的,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。
参考文章
SPI机制的原理和应用
Dubbo源码分析(三)Dubbo中的SPI和自适应扩展机制
java之spi机制简介
Java SPI 实战
网友评论