文章目录有道无术,术尚可求,有术无道,止于术。
本系列Spring Boot版本2.7.0
- 什么是SPI机制?
- JDK中的SPI机制
- 实现案例
- 1. 定义接口及功能
- 2. 实现接口
- 3. 创建元数据文件
- 4. 测试
- SPI对双亲委派机制的破坏
- Spring Boot 中的SPI
- SpringFactoriesLoader
- Spring Boot 中SPI机制的应用
- 自动配置
- 2.7.0 版本的改动
SPI全称为Service Provider Interface,是一种服务提供发现机制。SPI是非常重要的一个机制,像Spring、Dubbo、JDBC驱动、Nacos等等都用到了该机制。
SPI 机制的思想:软件系统里抽象的各个模块,往往有很多不同的实现方案,比如JDBC 模块,由各个数据库厂商去实现驱动。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务提供发现机制。
SPI就提供为某个接口寻找服务实现的机制。有点类似 IOC 的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
JDK中的SPI机制JDK提供了SPI机制实现工具类java.util.ServiceLoader,可以通过该类来实现动态加载服务实例。
实现案例按照JDK中实现SPI机制的约定,需要以下几个步骤:
- 定义接口及功能
- 提供各种模块实现类
- 各实现模块在resources/META-INF/services目录下创建一个名称为接口全限定接口名文件,内容为实现类的全限定类名
- 引入模块JAR包直接使用
接下来我们仿照JDBC 驱动来实现一个案例。
1. 定义接口及功能创建一个组件工程,用于定义驱动接口标准和加载驱动的功能,定义一个加载驱动接口:
public interface JdbcDriver {
void load();
}
使用ServiceLoader动态加载JdbcDriver的实现类实现加载驱动:
public class Db {
public static void loadDriver(){
ServiceLoader providers = ServiceLoader.load(JdbcDriver.class);
for (JdbcDriver provider : providers) {
provider.load();
}
}
}
2. 实现接口
创建Mysql 驱动实现类,对应也创建一个Oracle 驱动实现类:
在spi-mysql模块中的resources/META-INF/services目录下创建一个名称为org.pearl.app.study.JdbcDriver的文件,内容为实现类的全限定类名MysqlJdbcDriver。
spi-oracle模块中也一样创建对应的文件。
创建一个测试模块,引入Mysql 驱动和加载驱动功能的模块。
调用功能测试,发现加载到了Mysql 驱动。
将依赖换为spi-oracle,就动态的加载到了Oracle 驱动。
SPI和类加载机制有关,所以先简单回顾下。
类加载机制:Java代码经过编译后形成的.class文件,需要加载到虚拟机后才能运行,该过程称之为类加载机制。
类加载器ClassLoader:通过一个类的全限定名来获取描述该类的二进制字节流的的功能代码。
JDK1.8中JVM默认提供了以下加载器:
-
BootstrapClassLoader:(引导类加载器),这个类加载器使用C++语言实现,是虚拟机自身的一部分,用来加载java核心类库。
-
ExtensionClassLoader(扩展类加载器):加载 Java 的扩展库。
-
SystemClassLoader(系统类加载器):负责将环境变量CLASSPATH目录下的类库加载到内存中,自己编写的类和使用的第三方jar都是由它加载的。
-
CustomerClassLoader(自定义加载器): 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
我们知道JVM类加载时都遵循双亲委派机制,当一个类加载器收到了类加载的请求,将其委托给父加载器去完成,以此类推,这样所有的加载请求最终都会传到最顶层的启动类加载器,只有当父加载器无法完成类加载的请求,子加载器才会自己去加载。
双亲委派机制的作用:
-
保证安全性:防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
-
保证唯一性:保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI 接口中的代码经常需要加载具体的实现类。那么问题来了,SPI 的接口是 Java 核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI 的实现类是由系统类加载器(System ClassLoader)来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader 无法委派 AppClassLoader 来加载类。
为了解以上问题,我们可以看到ServiceLoader中获取当前线程的ContextClassLoader ,ContextClassLoader 默认存放了 AppClassLoader 的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader 或是 ExtClassLoader 等),在任何需要的时候都可以用 Thread.currentThread().getContextClassLoader() 取出应用程序类加载器来完成需要的操作。
Spring里也有类似的SP机制,从classpath下所有jar包的META-INF/spring.factories 配置文件中加载类,然后将其中定义的bean注入到Spring容器中。
实现这一机制的是spring-core中的SpringFactoriesLoader类,可以看到该类提供了加载并反射实例化对象的方法:
public final class SpringFactoriesLoader {
// 需要类的位置,可以存在于多个JAR文件中。
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);
// 缓存,第一次加载所有并缓存
static final Map>> cache = new ConcurrentReferenceHashMap();
private SpringFactoriesLoader() {
}
// 将给定的类型实例化
public static List loadFactories(Class factoryType, @Nullable ClassLoader classLoader) {
Assert.notNull(factoryType, "'factoryType' must not be null");
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
List factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
if (logger.isTraceEnabled()) {
logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames);
}
List result = new ArrayList(factoryImplementationNames.size());
Iterator var5 = factoryImplementationNames.iterator();
while(var5.hasNext()) {
String factoryImplementationName = (String)var5.next();
result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
}
AnnotationAwareOrderComparator.sort(result);
return result;
}
public static List 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());
}
// 使用classLoader 类加载器,将资源中的所有类型都加载出来
private static Map> loadSpringFactories(ClassLoader classLoader) {
// 1. 缓存中获取
Map> result = (Map)cache.get(classLoader);
if (result != null) {
return result; // 缓存有直接返回
} else {
HashMap result = new HashMap();
try {
// 2. 获取META-INF/spring.factories 文件资源
Enumeration urls = classLoader.getResources("META-INF/spring.factories");
// 缓存
while(urls.hasMoreElements()) {
// 3. 通过文件URL 创建资源对象
// url:jar:file:/D:/javaDevelop/maven_repository/org/springframework/boot/spring-boot/2.5.2/spring-boot-2.5.2.jar!/META-INF/spring.factories
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
// 4. 将文件中的资源都加载出来
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);
}
}
}
private static T instantiateFactory(String factoryImplementationName, Class factoryType, ClassLoader classLoader) {
try {
Class> factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader);
if (!factoryType.isAssignableFrom(factoryImplementationClass)) {
throw new IllegalArgumentException("Class [" + factoryImplementationName + "] is not assignable to factory type [" + factoryType.getName() + "]");
} else {
// 反射实例化
return ReflectionUtils.accessibleConstructor(factoryImplementationClass, new Class[0]).newInstance();
}
} catch (Throwable var4) {
throw new IllegalArgumentException("Unable to instantiate factory class [" + factoryImplementationName + "] for factory type [" + factoryType.getName() + "]", var4);
}
}
}
Spring Boot 中SPI机制的应用
自动配置
Spring Boot 最重要的核心功能是自动配置,字面上理解就是配置是自动化,不需要我们额外的编配置文件,全部都有默认的配置。
SpringBoot采用约定大于配置设计思想,将所有可能遇到的配置信息提前配置好,写在自动配置的jar包中。每个Starter基本都会有对应的自动配置。
在spring-boot-autoconfigure包中提供了自动配置的功能,基于约定大于配置设计思想,将我们常用的框架或组件都实现配置好,然后在starter 包中,引入autoconfigure 及对应第三方框架包,就可以实现自动配置。
然后可以在Spring Boot 包中看到很多AutoConfiguration结尾的自动配置类,这些配置类主要根据引入的包和配置,完成第三方框架的配置,比如RedisAutoConfiguration 中,就帮我们配置了客户端、连接池等。
这些AutoConfiguration配置类都是@AutoConfiguration或者@Configuration注解标识的,那么就需要包扫描才能生效,但是如果直接大面积的扫描或者需要找到这些配置类的路径,就效率很低、容易出错,也不方便管理。
在spring-boot-autoconfigure 中的META-INFspring.factories 文件中,可以看到定义了很多自动配置类。
所以Spring Boot 基于Spring 的SPI 机制,来动态的加载这些自动配置类,当我引入starter 包时,会去查询resources/META-INF/spring.factories目录下配置的类,将其注入到IOC中,这样也就实现了自动配置。
具体是怎么加载的,我们会在后续启动流程中分析。
2.7.0 版本的改动在Spring Boot 2.7.0 版本spring.factories 已被标记为过时,并将在3.0版本移除,使用新的规则,如下图:



