SpringBoot的SPI机制


Java中自带了所谓SPI机制,按照约定去META-INF/services目录里找各个接口的配置文件,找到接口的实现类,然后使用当前线程上线文类加载器定位到实现类加载器,通过其加载实现类,然后再反射newInstance得到实现类的实例。

Spring里也有类似的SPI,思路根上面类似,从classpath下所有jar包的META-INF/spring.factories 配置文件中加载标识为EnableAutoConfiguration的配置类,然后将其中定义的bean注入到Spring容器。

笔者认为,跟Java的SPI更多的是为了面向接口编程和克服双亲委派局限不同,Spring的这种SPI可能更多的是体现一种框架的可扩展性:在springboot工程中我们都知道,默认是会加载主类所在目录及其所有子目录下的自动注入bean的,比如主类在com.wangan,则com.wangan.controller, com.wangan.service等等都会加载并注入;但如果第三方开发的jar包、大概率情况下目录是跟工程目录不同的比如wangan公司的合作伙伴,lb公司开发了一个组件用的是com.lb.*,这个组件的类就没法自动的注入到spring,而通过上面讲的SPI机制就可以解决这个问题。

按照上面的思路,我们从spring boot工程的主类开始分析一下相关的源代码:

源代码走读

@SpringBootApplication实际上由三个注解组成:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
																  @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

@SpringBootConfiguration查看代码就是个@Configuration,所以上面的3注解相当于就是@EnableAutoConfiguration@Configuration@ComponentScan

先研究下@EnableAutoConfiguration

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
	String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
	Class<?>[] exclude() default {};
	String[] excludeName() default {};

}

@Import(AutoConfigurationPackages.Registrar.class)

@Import(AutoConfigurationImportSelector.class)

protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
	List configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
			getBeanClassLoader());
	Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
			+ "are using a custom packaging, make sure that file is correct.");
	return configurations;
}

SpringFactoriesLoader的方法

loadFactoryNames( getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader() )

两个参数,前一个就是EnableAutoConfiguration.class, 后一个getBeanClassLoader()是spring bean的classLoader(这个跟整个工程deploy有关,最后通过gradle打出来的jar包解压出来,有个目录专门放的就是classLoader,应该是一个自定义的classLoader,稍后研究一下)

loadFactoryNames调的是loadSpringFactories,

/*
从loadSpringFactories返回的 Map>里边,
按照name=EnableAutoConfiguration获取list,如果没有值就返回个空的list。
*/
loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());

loadSpringFactories方法很有意思,从spring.factories文件加载里边的k-v,然后一个key可能会有多个逗号分隔的value,所以这里最后返回的是个LinkedMultiValueMap。加载的时候会建立一个cache,下次就不用重复从文件里加载了直接读cache:

private static Map> loadSpringFactories(@Nullable ClassLoader classLoader) {
    //根据classLoader查cache这个map如果查到了说明factories已经加载过了,直接从缓存返回就行了
	MultiValueMap result = cache.get(classLoader);
	if (result != null) {
		return result;
	}
	//所以下面的逻辑就是怎么填充这个cache
	try {
        //用自定义classloader从Resource加载,没有就用system ClassLoader
		Enumeration urls = (classLoader != null ?
				classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
				ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
		result = new LinkedMultiValueMap<>();
		while (urls.hasMoreElements()) { //遍历从spring.factories查到的k-v,文件里可能是一个key多个v,逗号分隔
			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) { //抛异常,说从META-INF/spring.factories加载不了
		throw new IllegalArgumentException("Unable to load factories from location [" +
				FACTORIES_RESOURCE_LOCATION + "]", ex);
	}
}

好了,到此我们分析完了getCandidateConfigurations这条线的关键代码逻辑了,总结一下就是从spring.factories里边加载配置的那些key-values,然后找到里边key名字是是EnableAutoConfiguration的那些。

嗯,现在知道哪些类要(作为springboot插件)自动装配了,但是什么时候装配的呢?

getCandidateConfigurations所在的类AutoConfigurationImportSelector实现了DeferredImportSelector接口,然后在ConfigurationClassPostProcessor这条线中,解析parse我们的各个@Configuration注解的类的时候,会去processDeferredImportSelectors(),在这个方法里进行的bean的生成。从实验结果上来看是spring.factories配置了EnableAutoConfiguration类型的那些个类,无论用的注解是@Component、@Configuration还是@RibbonClients都给加载了,然后生成了spring bean注入到了上下文里。进一步实验可知,即使把@Component去掉也是可以注入的。

参考: