springboot|springboot SPI 扩展机制

springboot的扩展解耦,仿照java的SPI机制,使用SpringFactoriesLoader加载spring.factories文件实现。
java SPI(Service Provider Inteface) SPI全称Service Provider Inteface,在java中是为厂商或插件设计的扩展机制。总结下java SPI机制的思想。

  • 系统中抽象的各个模块,比如日志模块,xml解析模块、jdbc模块等,每个模块有多种实现方案。
  • 面向对象程序设计中,一般推荐模块间基于接口编程,模块间不对实现类进行硬编码。一旦代码中涉及具体的实现类,就违反了可拔插的原则。
  • 如果需要替换一种实现,就需要修改代码。为了实现在模块装配时,能不在程序里动态指明,就需要一种服务发现机制。
java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。类似IOC的思想,将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
简单说就是, Java提供的SPI接口和调用方在java的核心类库,接口的具体实现类由厂商或插件设计开发,
java虚拟机中采用双亲委派模型进行类的加载,而java SPI实现类的加载,不适用双亲委派模型,因而有了破坏双亲委派模型的说法。为什么说 Java SPI 的设计违反双亲委派原则
虽然用了“破坏”这个词,但并不带有贬义,只要有足够意义和理由,突破已有的原则就可认为是创新。秉持这个观点,我们再来看spring 私有框架的扩展类加载过程,并不符合传统的双亲委派模型的类加载,仍然是值得学习的,弄懂了其实现,就可以掌握在spring基础上扩展的各类组件及工具集。诸如spring boot、spring cloud 等。
spring SPI扩展机制 在Spring中也有一种类似与Java SPI的扩展加载机制。在META-INF/spring.factories文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化。这种自定义的SPI机制是Spring Boot Starter实现的基础。
在开始介绍SpringFactoriesLoader加载spring.factories文件前,先了解java类加载器加载资源的过程
java的类加载器除加载 class 外,还有一个重要功能就是加载资源,从 jar 包中读取任何资源文件,如ClassLoader.getResources(Stringname) 方法读取 jar 包中的资源文件,代码如下:
public URL getResource(String name) { URL url; if (parent != null) { url = parent.getResource(name); } else { url = getBootstrapResource(name); } if (url == null) { url = findResource(name); } return url; }

加载资源的过程,与双亲委派模型类加载的过程一样,首先判断父类是否为空,不为空则将任务委派给父类加载器执行资源加载,直到启动类加载器BootstrapClassLoader。最后才轮到自己查找,而不同的类加载器负责扫描不同路径下的 jar 包,就如同加载 class 一样,最后会扫描所有的 jar 包,找到符合条件的资源文件。
类加载器的 ClassLoader.findResources(name) 方法会遍历其负责加载的所有 jar 包,找到 jar 包中名称为 name 的资源文件,这里的资源可以是任何文件,甚至是 .class 文件,比如下面的示例,用于查找 ConcurrentHashMap.class 文件:
public static void main(String[] args) throws IOException { String name = "java/util/concurrent/ConcurrentHashMap.class"; Enumeration urls = Thread.currentThread() .getContextClassLoader().getResources(name); while (urls.hasMoreElements()) { URL url = urls.nextElement(); System.out.println(url.toString()); } }

运行的结果
jar:file:/C:/Program%20Files/Java/jdk1.8.0_25/jre/lib/rt.jar!/java/util/concurrent/ConcurrentHashMap.class

有了ClassLoder加载资源文件的知识,接下来了解SpringFactoriesLoader加载spring.factories文件过程
spring-core包里定义了SpringFactoriesLoader类,在这个类中定义了两个对外的方法
  • loadFactories 根据接口类获取其实现类的实例,这个方法返回的是对象列表。
  • loadFactoryNames 根据接口获取其接口类的名称,这个方法返回的是类名的列表。
我们看loadFactoryNames 方法,源码如下:
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; //spring.factories文件的格式为:key=value1,value2,value3 //从所有jar文件中找到MET-INF/spring.factories文件 //然后从文件中解析初key=factoryClass类名称的所有value值 public static List loadFactoryNames(Class factoryClass, ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); try { //取得资源文件的URL Enumeration urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); List result = new ArrayList(); //遍历所有的URL while (urls.hasMoreElements()) { URL url = urls.nextElement(); //根据资源文件的url解析properties Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); String factoryClassNames = properties.getProperty(factoryClassName); //组装数据并返回 result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames))); } return result; } catch (IOException ex) { throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() + "] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); }}

有了前面关于 ClassLoader 的知识,再来理解这段代码,从 CLASSPATH 下的每个 Jar 包中搜寻所有 META-INF/spring.factories 配置文件,然后解析 properties 文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去 ClassPath 路径下查找,会扫描所有路径下的 Jar 包,只不过这个文件只会在 Classpath 下的 jar 包中。来简单看下spring.factories 文件内容:
# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ org.springframework.boot.autoconfigure.aop.AopAutoConfiguration ...

执行 loadFactoryNames(EnableAutoConfiguration.class,classLoader) 后,得到对应的一组 @Configuration 类,我们就可以通过反射实例化这些类然后注入到 IOC 容器中,最后容器里就有了一系列标注了 @Configuration的JavaConfig 形式的配置类。
这就是 SpringFactoriesLoader,它本质上属于 Spring 框架私有的一种扩展方案,类似于 SPI,即不采用双亲委派模型加载类,Spring Boot 在 Spring 基础上的很多核心功能都是基于此。
spring.factories文件加载过程详解 1、启动类入口如下
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class); } }

2、springboot使用启动类注解@SpringBootApplication
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication {

3、进入@EnableAutoConfiguration
@SuppressWarnings("deprecation") @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import(EnableAutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration {

发现加载了EnableAutoConfigurationImportSelector类,@Import注解的作用是,spring IOC容器中没有注入EnableAutoConfigurationImportSelector类,但是springboot的启动需要用到,因此通过@Import注解将该类注入到spring容器中。 可以理解为springboot是通过@SpringBootApplication注解,在spring的基础上进行扩展。扩展的功能在@Import(EnableAutoConfigurationImportSelector.class)中实现。由@SpringBootApplication注解进来看到的一系列代码,被注入到spring容器,暂时没有被调用,调用过程在spring的容器启动中,稍候讲解。
spring IOC容器启动时,用户自定义的类,无法被自动扫描进容器的。 springboot为spring之上构建的应用程序。在不侵入spring的前提下,使用@SringBootApplication注解,通过@Import导入ImportSelector接口的实现类EnableAutoConfigurationImportSelector,该实现类由springboot项目开发,引入了springboot所具有的功能。此处给我们提供了在spring之上构建自定义组件提供了参考样本。spring cloud config/ spring cloud eureka等均通过该方式构建。
提示,EnableAutoConfigurationImportSelector为ImportSelector的实现类,所有实现ImportSelector的类,都会在spring容器启动时被ConfigurationClassParser中的processImports进行实例化,并执行selectImports方法。
springboot启动,是先进行注解@SpringBootApplication的扫描,还是先运行SpringApplication.run(Application.class),由上述分析可知,注解扫描优先
4、EnableAutoConfigurationImportSelector类
@Deprecated public class EnableAutoConfigurationImportSelector extends AutoConfigurationImportSelector {

EnableAutoConfigurationImportSelector继承AutoConfigurationImportSelector 类,并覆盖其isEnabled方法,后续实例化EnableAutoConfigurationImportSelector时,调用的isEnabled方法,取自覆盖后的方法。
5、AutoConfigurationImportSelector的selectImports方法
@Override public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; } try { AutoConfigurationMetadata autoConfigurationMetadata = https://www.it610.com/article/AutoConfigurationMetadataLoader .loadMetadata(this.beanClassLoader); AnnotationAttributes attributes = getAttributes(annotationMetadata); List configurations = getCandidateConfigurations(annotationMetadata, attributes); configurations = removeDuplicates(configurations); configurations = sort(configurations, autoConfigurationMetadata); Set exclusions = getExclusions(annotationMetadata, attributes); checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = filter(configurations, autoConfigurationMetadata); fireAutoConfigurationImportEvents(configurations, exclusions); return configurations.toArray(new String[configurations.size()]); } catch (IOException ex) { throw new IllegalStateException(ex); } }

通过方法可知是为了选出想要加载的import类,而如何获取这些类呢?其实是通过一个SpringFactoriesLoader去加载对应的spring.factories。
下面展示如何和此类建立关系。
【springboot|springboot SPI 扩展机制】进入selectImports方法中的getCandidateConfigurations
/** * Return the auto-configuration class names that should be considered. By default * this method will load candidates using {@link SpringFactoriesLoader} with * {@link #getSpringFactoriesLoaderFactoryClass()}. * @param metadata the source metadata * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation * attributes} * @return a list of candidate configurations */ 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; }

getCandidateConfigurations方法的注释中写道,使用SpringFactoriesLoader类、AutoConfigurationImportSelector.getSpringFactoriesLoaderFactoryClass()组合,加载指定资源。
仔细看SpringFactoriesLoader.loadFactoryNames所需的参数,里面有个getSpringFactoriesLoaderFactoryClass()。下面看下这个方法的返回值是什么?
/** * Return the class used by {@link SpringFactoriesLoader} to load configuration * 返回SpringFactoriesLoader类加载配置需要的接口类 * candidates. * @return the factory class */ protected Class getSpringFactoriesLoaderFactoryClass() { return EnableAutoConfiguration.class; }

可以看出返回的是一个EnableAutoConfiguration.class,接下来SpringFactoriesLoader会根据这个interface去所有spring.factories找EnableAutoConfiguration.class所对应的values,并返回。
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 * @see #loadFactories * @throws IllegalArgumentException if an error occurs while loading factory names */ //factoryClass传入的参数值为EnableAutoConfiguration.class public static List loadFactoryNames(Class factoryClass, ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); //factoryClassName的取值为“EnableAutoConfiguration" try { //FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories" //urls为查找到的spring.factories文件列表 Enumeration urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); List result = new ArrayList(); while (urls.hasMoreElements()) { //遍历spring.factories资源文件 URL url = urls.nextElement(); //解析文件中的属性值,存入Properties Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); //根据键EnableAutoConfiguration找到配置文件中对应的属性值 String factoryClassNames = properties.getProperty(factoryClassName); result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames))); //将取到的values按逗号分隔,并转换成list } return result; //返回取值list } catch (IOException ex) { throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() + "] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); } }

返回值为list,取值从org.springframework.boot.autoconfigure.EnableAutoConfiguration的value=https://www.it610.com/article/{‘org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration’,‘org.springframework.boot.autoconfigure.aop.AopAutoConfiguration’,‘org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration’,,,,,,}
@Import注解 spring.factories的加载详解,有使用@Import(EnableAutoConfigurationImportSelector.class),
@Import注解,看spring对它的注释

@Import和xml配置的 标签作用一样,允许通过它引入 @Configuration 注解的类 (java config), 引入ImportSelector接口(要通过它去判定要引入哪些@Configuration) 和 ImportBeanDefinitionRegistrar 接口的实现, 也包括 @Component注解的普通类。但是如果要引入另一个xml 文件形式配置的 bean, 则需要通过 @ImportResource 注解。
ImportSelector接口类 @Import 的实现很多时候需要借助 ImportSelector 接口, 需要通过这个接口的实现去决定要引入哪些 @Configuration。 它如果实现了以下四个Aware 接口, 那么spring保证会在调用它之前先调用Aware接口的方法。至于为什么要保证调用Aware, 我个人觉得应该是你可以通过这些Aware去感知系统里边所有的环境变量, 从而决定你具体的选择逻辑。

Springboot 对@Import注解的处理过程 Springboot对注解的处理都发生在
AbstractApplicationContext.refresh() -> AbstractApplicationContext.invokeBeanFactoryPostProcessors(beanFactory) ->PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory, List beanFactoryPostProcessors) ->ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) -> ConfigurationClassPostProcessor.processConfigBeanDefinitions(BeanDefinitionRegistry registry)方法中。

  • springboot初始化的普通context(非web) 是AnnotationConfigApplicationContext(spring注解容器)
  • 在初始化的时候会初始化两个工具类, AnnotatedBeanDefinitionReader 和 ClassPathBeanDefinitionScanner 分别用来从 annotation driven 的配置和xml的配置中读取beanDefinition并向context注册,
  • 那么在初始化 AnnotatedBeanDefinitionReader 的时候, 会向BeanFactory注册一个ConfigurationClassPostProcessor 用来处理所有的基于annotation的bean, 这个ConfigurationClassPostProcessor 是 BeanFactoryPostProcessor 的一个实现,springboot会保证在 invokeBeanFactoryPostProcessors(beanFactory) 方法中调用注册到它上边的所有的BeanFactoryPostProcessor
  • 因此,在spring容器启动时,会调用ConfigurationClassPostProcessor .postProcessBeanDefinitionRegistry()方法。
ConfigurationClassParser 在ConfigurationClassPostProcessor .postProcessBeanDefinitionRegistry()方法中实例化ConfigurationClassParser调用
// Parse each @Configuration class ConfigurationClassParser parser = new ConfigurationClassParser( this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry);

那么在 ConfigurationClassParser -> processConfigurationClass() -> doProcessConfigurationClass() 方法中我们找到了 (这里边的流程还是很清楚的, 分别按次序处理了@PropertySource, @ComponentScan, @Import, @ImportResource, 在处理这些注解的时候是通过递归处理来保证所有的都被处理了)
取重点代码段
// Process any @Import annotations processImports(configClass, sourceClass, getImports(sourceClass), true);

processImports流程如下:
  • 首先,判断如果被import的是 ImportSelector.class 接口的实现, 那么初始化这个被Import的类, 然后调用它的selectImports方法去获得所需要的引入的configuration, 然后递归处理
  • 其次,判断如果被import的是 ImportBeanDefinitionRegistrar 接口的实现, 那么初始化后将对当前对象的处理委托给这个ImportBeanDefinitionRegistrar (不是特别明白, 只是我的猜测)
  • 最后, 将import引入的类作为一个正常的类来处理 ( 调用最外层的doProcessConfigurationClass())
综上, 如果引入的是一个正常的component, 那么会作为@Component或者@Configuration来处理, 这样在BeanFactory里边可以通过getBean拿到, 但如果你是 ImportSelector 或者 ImportBeanDefinitionRegistrar 接口的实现, 那么spring并不会将他们注册到beanFactory中,而只是调用他们的方法。
回顾本文之前所讲springboot启动,@SpringBootApplication注解通过@Import注入到spring容器的EnableAutoConfigurationImportSelector类,其继承的父类方法AutoConfigurationImportSelector.selectImports()在此处被调用。
processImports实现代码块如下
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, Collection importCandidates, boolean checkForCircularImports) throws IOException { ,,,, for (SourceClass candidate : importCandidates) { if (candidate.isAssignable(ImportSelector.class)) { // Candidate class is an ImportSelector -> delegate to it to determine imports Class candidateClass = candidate.loadClass(); ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class); ParserStrategyUtils.invokeAwareMethods( selector, this.environment, this.resourceLoader, this.registry); if (this.deferredImportSelectors != null && selector instanceof DeferredImportSelector) { this.deferredImportSelectors.add( new DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector)); } else { String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata()); Collection importSourceClasses = asSourceClasses(importClassNames); processImports(configClass, currentSourceClass, importSourceClasses, false); } } else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { // Candidate class is an ImportBeanDefinitionRegistrar -> // delegate to it to register additional bean definitions Class candidateClass = candidate.loadClass(); ImportBeanDefinitionRegistrar registrar = BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class); ParserStrategyUtils.invokeAwareMethods( registrar, this.environment, this.resourceLoader, this.registry); configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata()); } else { // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar -> // process it as an @Configuration class this.importStack.registerImport( currentSourceClass.getMetadata(), candidate.getMetadata().getClassName()); processConfigurationClass(candidate.asConfigClass(configClass)); } } ,,,, } }

    推荐阅读