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()
方法:
public interface SpiDemoInterface {
void test();
}
接下来再新建一个SpiDemoInterfaceImpl
,并实现SpiDemoInterface
:
public class SpiDemoInterfaceImpl implements SpiDemoInterface {
@Override
public void test() {
System.out.println("SpiDemoInterfaceImpl#test() run...");
}
}
编写主运行类,测试效果:
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 方法:
/**
* 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 分为两部分:loadSpringFactories
和 getOrDefault
。getOrDefault
方法很明显是Map中的方法,不再解释,主要来详细看loadSpringFactories
方法。
loadSpringFactories
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
文件了。