Spring中的SPI机制

一、SPI机制概述

SPI概念

通常情况下,我们设计一个接口,然后自己写实现类,这叫 API (Application Programming Interface),API 是调用方 依赖实现方

但如果场景反过来呢?

比如你是 Java 官方,你定义了 java.sql.Driver(数据库驱动接口),但你并不知道用户是用 MySQL、Oracle 还是 PostgreSQL。这时候,你需要一种机制:由接口定义方制定标准,由第三方(服务提供商)去实现,并且程序能够在运行时动态发现并加载这些实现 ,这就是 SPI

SPI (Service Provider Interface)直译"服务提供接口",它是一种 解耦扩展机制。核心思想:面向接口编程 + 运行时动态加载具体实现类。SPI机制体现了以下几个设计原则:

  1. 开闭原则:对扩展开放,对修改关闭
  2. 依赖倒置原则:高层模块不依赖低层模块,二者都依赖抽象
  3. 控制反转:由框架控制组件的加载和初始化

二、JDK原生的SPI

Java 自身早在 JDK 1.6 时期就提供了 SPI 支持,核心类是 java.util.ServiceLoader

2.1 约定与规范

JDK SPI 对目录结构有严格的约定:

在 classpath 下的 META-INF/services/ 目录中创建一个文件。

文件名必须是接口的全限定名(例如 org.example.spi.SearchService)。

文件内容是实现类的全限定名,每行一个。

2.2 使用方式

(1)定义接口,假设我们定义了一个搜索接口:

java 复制代码
package org.example.spi;

public interface SearchService {
    void search(String keyword);
}

(2)编写实现类

java 复制代码
public class DbSearchServiceImpl implements SearchService {
    @Override
    public void search(String keyword) {
        System.out.println("数据库搜索:" + keyword);
    }
}
public class FileSearchServiceImpl implements SearchService {
    @Override
    public void search(String keyword) {
        System.out.println("文件搜索:" + keyword);
    }
}

(3)在 META-INF/services/ 目录下创建文件:org.example.spi.SearchService

内容写实现类完整类名:

java 复制代码
org.example.spi.DbSearchServiceImpl
org.example.spi.FileSearchServiceImpl

(4)调用,使用 ServiceLoader 加载

java 复制代码
public class SpiTest {
    public static void main(String[] args) {
        ServiceLoader<SearchService> loader = ServiceLoader.load(SearchService.class);
        for (SearchService service : loader) {
            System.out.println("  " + service.getClass().getSimpleName() + " - " + service);
            service.search("hello world");
        }
    }
}

项目结构:

测试结果如下:

bash 复制代码
  DbSearchServiceImpl - org.example.spi.DbSearchServiceImpl@4554617c
数据库搜索:hello world
  FileSearchServiceImpl - org.example.spi.FileSearchServiceImpl@74a14482
文件搜索:hello world

三、Spring中的SPI

JDK SPI 的痛点

虽然 JDK SPI 属于标准,但在实际的大型框架开发中,它有几个明显的缺陷:

  • 全量加载:ServiceLoader 在遍历时,会把配置文件中所有的实现类都实例化一遍。如果你只想用其中某一个,或者其中某个实现类初始化非常耗时,这会造成极大的资源浪费。
  • 获取不便:只能通过 Iterator 遍历,无法根据某个参数(比如 key)直接获取对应的实现类。
  • 异常处理不友好:加载失败时异常信息不明确

Spring SPI 的目标

Spring 框架的许多关键点都依赖于其 SPI 机制来实现 可插拔:

  • Web 容器嵌入: 允许你轻松切换 Tomcat、Jetty、Undertow 等 Web 服务器。
  • 数据访问抽象: 允许你使用不同的 ORM 框架(MyBatis、Hibernate、JPA)。
  • AOP 代理机制: 可以选择 JDK 动态代理或 CGLIB 代理。
  • 自定义配置解析: 允许扩展 XML 或注解的解析逻辑。

SPI文件 核心约定

Spring Boot 2.6.x 及以下(常见)

  • 文件:META-INF/spring.factories
  • 格式:Java properties,key 为"扩展点"(例如:org.springframework.boot.autoconfigure.EnableAutoConfiguration),value 为逗号分隔的实现类列表。

示例:

复制代码
# META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.autoconfig.MyAutoConfiguration,\
com.example.autoconfig.MyOtherAutoConfiguration
  • 加载方式:SpringFactoriesLoader 读取 properties,按 key 找出值(逗号分割),再将类名导入/实例化。这意味着,当 Spring Boot 启动时,它会通过 SPI 机制扫描到 EnableAutoConfiguration 对应的所有配置类,并将它们加载到容器中

Spring Boot 2.7.x+

  • 文件:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  • 格式:每行一个类的全限定名(支持注释 #),没有 key → 文件专门用于 AutoConfiguration 列表。

示例:

txt 复制代码
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.autoconfig.MyAutoConfiguration
com.example.autoconfig.MyOtherAutoConfiguration
  • 加载方式:Spring Boot 在定位自动配置候选时会读取此 imports 文件,并配合 @AutoConfiguration 注解与条件注解来决定是否导入。

四、Spring SPI机制的实现原理

Spring SPI 的核心实现类是 org.springframework.core.io.support.SpringFactoriesLoader

SpringFactoriesLoader核心方法:loadSpringFactories

Spring 是如何找到所有 jar 包里的配置文件的,通过SpringFactoriesLoader源码可以看到

java 复制代码
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

// 存储SPI机制加载的类及其映射
static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap<>();

// 核心加载方法
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
	ClassLoader classLoaderToUse = classLoader;
	// 如果外部没有传 classloader,就用当前类(SpringFactoriesLoader)的 classloader
	if (classLoaderToUse == null) {
		classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
	}
	String factoryTypeName = factoryType.getName();
	// 从缓存中加载所有 spring.factories 再返回 key 对应的值
	return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}


// 返回结构 Map<扩展点接口完整类名, List<实现类完整类名>>
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {		
		// 优先返回缓存
		Map<String, List<String>> result = cache.get(classLoader);
		if (result != null) {
			return result;
		}

		result = new HashMap<>();
		try {
		// 扫描所有 META-INF/spring.factories 文件
			Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
			while (urls.hasMoreElements()) {
				// 每个 URL 都代表一个 jar 包中的 META-INF/spring.factories 文件
				URL url = urls.nextElement();
				UrlResource resource = new UrlResource(url);
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
				for (Map.Entry<?, ?> entry : properties.entrySet()) {
					String factoryTypeName = ((String) entry.getKey()).trim();
					// 解析所有 key 和 value,如果一个key配置了value,则用英文逗号隔开,此处会做分割
					String[] factoryImplementationNames =
							StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
					// 将 key → list 加入结果 Map
					for (String factoryImplementationName : factoryImplementationNames) {
						result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
								.add(factoryImplementationName.trim());
					}
				}
			}

			// 替换实现类名称列表为不可修改的集合
			result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
					.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
			// 加缓存,下次直接复用
			cache.put(classLoader, result);
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load factories from location [" +
					FACTORIES_RESOURCE_LOCATION + "]", ex);
		}
		return result;
	}

// 加载并实例化工厂对象
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
	Assert.notNull(factoryType, "'factoryType' must not be null");
	ClassLoader classLoaderToUse = classLoader;
	// 确定要使用的 ClassLoader:如果传入为空,则使用 SpringFactoriesLoader 自身的 ClassLoader
	if (classLoaderToUse == null) {
		classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
	}
	// 调用 loadFactoryNames 获取所有实现类的全限定名列表
	List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
	if (logger.isTraceEnabled()) {
		logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames);
	}
	// 实例化所有实现类
	List<T> result = new ArrayList<>(factoryImplementationNames.size());
	for (String factoryImplementationName : factoryImplementationNames) {
		// 逐个通过反射实例化
		result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
	}
	// 对结果进行排序,Spring 使用 AnnotationAwareOrderComparator,支持 @Order 或 Ordered 接口
	AnnotationAwareOrderComparator.sort(result);
	return result;
}

/**
    * 辅助方法:通过反射实例化工厂实现类。
    * @param factoryImplementationName 实现类的全限定名
    * @param factoryType 工厂接口/抽象类类型
    * @param classLoader 用于加载类的 ClassLoader
    * @return 实例化后的对象
    * @throws IllegalArgumentException 如果类加载失败、无法赋值或实例化失败
    */
private static <T> T instantiateFactory(String factoryImplementationName, Class<T> factoryType, ClassLoader classLoader) {
		try {
			// 1. 加载实现类 Class 对象
			Class<?> factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader);
			// 2. 校验:检查实现类是否是工厂类型(接口/抽象类)的子类型
			if (!factoryType.isAssignableFrom(factoryImplementationClass)) {
				throw new IllegalArgumentException(
						"Class [" + factoryImplementationName + "] is not assignable to factory type [" + factoryType.getName() + "]");
			}
			// 3. 实例化:通过反射获取无参构造函数并创建实例(ReflectionUtils 确保访问性)
			return (T) ReflectionUtils.accessibleConstructor(factoryImplementationClass).newInstance();
		}
		catch (Throwable ex) {
			throw new IllegalArgumentException(
				"Unable to instantiate factory class [" + factoryImplementationName + "] for factory type [" + factoryType.getName() + "]",
				ex);
		}
	}

可以看到,loadFactoryNames 是对外入口,负责根据指定的 SPI 接口类型,从所有已加载的 spring.factories 中取出对应的实现类名单,而 loadSpringFactories 则是底层引擎,负责扫描类路径下所有 META-INF/spring.factories 文件,将其解析、合并、去重并缓存成一个 Map,通过上面的步骤,Spring 就拿到了实现类的全限定名字符串,最后通过instantiateFactory,反射 ReflectionUtils.accessibleConstructor 来实例化对象。

相关推荐
侠***I10 小时前
探索三菱 FX 系列 C# 上位机源码之旅
spring
han_hanker10 小时前
这里使用 extends HashMap<String, Object> 和 类本身定义变量的优缺点
java·开发语言
careathers11 小时前
【JavaSE语法】面向对象初步认识
java·面向对象
coding随想11 小时前
掌控选区的终极武器:getSelection API的深度解析与实战应用
java·前端·javascript
嵌入式小能手11 小时前
飞凌嵌入式ElfBoard-文件I/O的深入学习之存储映射I/O
java·前端·学习
ChinaRainbowSea11 小时前
github 仓库主页美化定制
java·后端·github
程序猿小蒜11 小时前
基于springboot的医院资源管理系统开发与设计
java·前端·spring boot·后端·spring
程序员-周李斌11 小时前
ConcurrentHashMap 源码分析
java·开发语言·哈希算法·散列表·开源软件
ChrisitineTX12 小时前
凌晨突发Java并发问题:synchronized锁升级导致接口超时,排查过程全记录
java·数据库·oracle