Java的SPI

SPI全称为Service Provider Interface,是jdk内置的一种服务提供发现机制。简单来说,它就是一种动态替换发现的机制。

SPI规定,所有要预先声明的类都应该放在META-INF/services中。配置的文件名是接口/抽象类的全限定名,文件内容是抽象类的子类或接口的实现类的全限定类名,如果有多个,借助换行符,一行一个。

具体使用时,使用jdk内置的ServiceLoader类来加载预先配置好的实现类。

举个例子:

META-INF/services中声明一个文件名为site.milanchen.demo.SpiDemoInterface的文件,文件内容为:

site.milanchen.demo.SpiDemoInterfaceImpl

site.milanchen.demo包下新建一个接口,类名必须跟上面配置的文件名一样:SpiDemoInterface

在接口中声明一个test()方法:

1
2
3
public interface SpiDemoInterface {
void test();
}

接下来再新建一个SpiDemoInterfaceImpl,并实现SpiDemoInterface

1
2
3
4
5
6
public class SpiDemoInterfaceImpl implements SpiDemoInterface {
@Override
public void test() {
System.out.println("SpiDemoInterfaceImpl#test() run...");
}
}

编写主运行类,测试效果:

1
2
3
4
5
6
public class App {
public static void main(String[] args) {
ServiceLoader<SpiDemoInterface> loaders = ServiceLoader.load(SpiDemoInterface.class);
loaders.foreach(SpiDemoInterface::test);
}
}

运行结果:

SpiDemoInterfaceImpl#test() run...

Spring的SPI

SpringFramework 利用 SpringFactoriesLoader 都是调用 loadFactoryNames 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Load the fully qualified class names of factory implementations of the
* given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
* class loader.
* @param factoryClass the interface or abstract class representing the factory
* @param classLoader the ClassLoader to use for loading resources; can be
* {@code null} to use the default
* @throws IllegalArgumentException if an error occurs while loading factory names
* @see #loadFactories
*/
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

使用给定的类加载器从 META-INF/spring.factories 中加载给定类型的工厂实现的全限定类名。

结合之前SpringBoot自动配置提到的spring.factories内容,可以知道它只是key-value的关系。

这么设计的好处:不再局限于接口-实现类的模式,key可以随意定义。
(如上面的 org.springframework.boot.autoconfigure.EnableAutoConfiguration是一个注解)

来看方法实现,第一行代码获取的是要被加载的接口/抽象类的全限定名,下面的 return 分为两部分:loadSpringFactoriesgetOrDefaultgetOrDefault方法很明显是Map中的方法,不再解释,主要来详细看loadSpringFactories方法。

loadSpringFactories

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();

// 这个方法仅接收了一个类加载器
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 第一次取不到
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}

try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}

它拿到每一个文件,并用Properties方式加载文件,之后把这个文件中每一组键值对都加载出来,放入MultiValueMap中。

如果一个接口/抽象类有多个对应的目标类,则使用英文逗号隔开。StringUtils.commaDelimitedListToStringArray会将大字符串拆成一个一个的全限定类名。

整理完后,整个result放入cache中。下一次再加载时就无需再次加载spring.factories文件了。