文章目录
- 2.4 SPI机制(Service Provider Interface)
- 2.4.1 JDK原生SPI
- 1.定义接口+实现类
- 2.声明SPI文件
- 3.测试
- 2.4.2 SpringFramework 3.2 的SPI
- 1.声明SPI文件
- 2.测试
- 3.Spring SPI机制的实现原理
2.4 SPI机制(Service Provider Interface)
- 问题:依赖倒转原则提到,应该依赖接口而不是实现类,但接口最终要有实现类落地。如果因为业务调整需要替换某个接口的实现类,就不得不改动实现类,也就是修改源码。
- 解决:SPI机制解决了这个问题。通过一种“服务寻找”的机制,动态地加载接口/抽象类对应的具体实现类。把接口的具体实现类的定义和声明交给了外部化的配置文件。
- 如下图,一个接口可以有多个实现类,通过SPI机制,可以将一个接口需要创建的实现类的对象都罗列到一个特殊的文件中,SPI机制会依次将这些实现类的对象进行创建并返回。
2.4.1 JDK原生SPI
简单了解即可,使用范围有限,只能通过接口或抽象类来加载具体的实现类。
1.定义接口+实现类
模拟一套Dao接口的不同数据库访问支持
public interface DemoDao {
}
public class DemoMysqlDao implements DemoDao {
}
public class DemoOracleDao implements DemoDao {
}
2.声明SPI文件
JDK的SPI需要遵循以下规范:
- 所有定义的SPI文件都必须放在项目的META-INF/services目录下
- 文件名必须命名为接口或抽象类的全限定名
- 文件内容为接口或抽象类的具体实现类的全限定名;如果有多个,则每行声明一个具体实现类的全限定名,多个类之间没有分隔符
具体步骤:
(1)在resources目录下创建新目录META-INF/services
(2)新建文件:com.star.springboot.spi.DemoDao
(3)输入文件内容:
com.star.springboot.spi.DemoMysqlDao
com.star.springboot.spi.DemoOracleDao
3.测试
public class JdkSpiApplication {
public static void main(String[] args) {
ServiceLoader<DemoDao> serviceLoader = ServiceLoader.load(DemoDao.class);
serviceLoader.iterator().forEachRemaining(dao -> {
System.out.println(dao);
});
}
}
输出结果:
com.star.springboot.spi.DemoMysqlDao@65b3120a
com.star.springboot.spi.DemoOracleDao@6f539caf
控制台成功打印出DemoDao的两个实现类对象,这说明JDK原生的SPI机制已成功使用。
2.4.2 SpringFramework 3.2 的SPI
SpringFramework中的SPI比JDK原生的SPI更高级实用,因为它不仅限于接口或抽象类,而可以是任何一个类、接口或注解。
SpringBoot中大量用到SPI机制加载自动配置类和特殊组件等(如@EnableAutoConfiguration)。
1.声明SPI文件
SpringFramework的SPI需要遵循以下规范:
- SPI文件必须放在项目的META-INF目录下。
- 文件名必须命名为spring.factories(实际上是一个properties文件)。
- 文件内容:被检索的类/接口/注解的全限定名作为properties的key,具体要检索的类的全限定名作为value,多个类之间用英文逗号隔开。
具体步骤:
(1)在resources/META-INF目录下新建文件:spring.factories
(2)输入文件内容:
com.star.springboot.spi.DemoDao=com.star.springboot.spi.DemoMysqlDaoImpl,com.star.springboot.spi.DemoOracleDaoImpl
2.测试
public class SpringSpiApplication {
public static void main(String[] args) {
List<DemoDao> demoDaos = SpringFactoriesLoader.loadFactories(DemoDao.class, SpringSpiApplication.class.getClassLoader());
demoDaos.forEach(dao -> {
System.out.println(dao);
});
System.out.println("----------");
List<String> daoClassNames = SpringFactoriesLoader.loadFactoryNames(DemoDao.class, SpringSpiApplication.class.getClassLoader());
daoClassNames.forEach(className -> {
System.out.println(className);
});
}
}
输出结果:
com.star.springboot.spi.DemoMysqlDaoImpl@52d455b8
com.star.springboot.spi.DemoOracleDaoImpl@4f4a7090
----------
com.star.springboot.spi.DemoMysqlDaoImpl
com.star.springboot.spi.DemoOracleDaoImpl
控制台成功打印出DemoDao的两个实现类对象及其全限定名,这说明SpringFramework的SPI机制已成功使用。
延伸:
SpringFactoriesLoader不仅可以加载声明的类的对象(loadFactories),还可以直接把预定义好的全限定名提取出来(loadFactoryNames)。
3.Spring SPI机制的实现原理
SPI的核心使用方法是SpringFactoriesLoader.loadFactoryNames,通过这个方法可以获得指定全限定名对应配置的所有类的全限定名。
// 规定SPI文件名称及位置
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// 存储SPI机制加载的类及其映射
private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
// 利用缓存机制提高加载速度
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 解析之前先检查缓存,有则直接返回
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
// 真正的加载动作,利用类加载器加载所有的spring.factories(多个,包括我们自定义框架本身自带的),并逐个配置解析
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
// 提取出每个spring.factories文件
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 以properties的方式读取
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
// 逐个收集key和value
String factoryTypeName = ((String) entry.getKey()).trim();
// 如果一个key配置了多个value,使用英文逗号分割
for (StrinfactoryImplementationName:StringUtils.commaDelimitedListToStringArray((Strinentry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
// 存入缓存中
cache.put(classLoader, result);
return result;
} catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location ["+
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
逻辑梳理:SpringFactoriesLoader中有一块缓存区,这块缓存区会在SPI机制第一次被利用时,将项目类路径下所有的spring.factories文件都加载并解析,然后存入缓存区。解析的具体逻辑,是将每一个spring.factories文件都当作properties文件解析,提取每一对映射关系,保存到Map中,最终存入全局缓存。
通过Debug,可以看到SPI机制不仅读取自定义的spring.factories,还读取了框架自带的:
最终保存到Map的映射关系非常多,但返回给main只有自己定义的:
······