springboot SPI 扩展机制

springboot的扩展解耦,仿照java的SPI机制,使用SpringFactoriesLoader加载spring.factories文件实现。

java SPI(Service Provider Inteface)


java中为厂商或插件设计的扩展机制,为什么引入?

  • 系统中抽象的各个模块,比如日志模块,xml解析模块、jdbc模块等,每个模块有多种实现方案。
  • 面向对象程序设计中,一般推荐模块间基于接口编程,模块间不对实现类进行硬编码。一旦代码中涉及具体的实现类,就违反了可拔插的原则。
  • 如果需要替换一种实现,就需要修改代码。为了实现在模块装配时,能不在程序里动态指明,就需要一种服务发现机制。

解决方案

  • java SPI, 为某个接口寻找服务实现的机制,类似IOC思想,将装配的控制权移到程序之外,在模块化设计中该机制尤其重要
    也即 Java提供的SPI接口和调用方在java的核心类库,接口的具体实现类由厂商或插件设计开发,

java虚拟机中采用双亲委派模型进行类的加载,而java SPI实现类的加载,不适用双亲委派模型,因而有了破坏双亲委派模型的说法。为什么说 Java SPI 的设计违反双亲委派原则

虽然用了“破坏”这个词,并不带有贬义,只要有足够意义和理由,突破已有的原则就可认为是创新。秉持这个观点,再来看spring 私有框架的扩展类加载过程,并不符合传统的双亲委派模型的类加载,仍值得学习,弄懂其实现,就可掌握在spring基础上扩展的各类组件及工具集。诸如spring boot、spring cloud 等。

spring SPI扩展机制


类似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<URL> 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<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
    String factoryClassName = factoryClass.getName();
    try {
//取得资源文件的URL
        Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        List<String> result = new ArrayList<String>();
//遍历所有的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);

    }

}

从 ClassPath 下的每个 Jar 包中搜寻所有 META-INF/spring.factories 配置文件,然后解析 properties 文件,找到指定名称的配置后返回。
注意,不仅在ClassPath 路径下查找,也会扫描所有路径下的 Jar 包,只不过spring.factories只存在于 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、启动类入口
2、springboot使用启动类注解@SpringBootApplication
3、进入@EnableAutoConfiguration,通过@Import加载EnableAutoConfigurationImportSelector类, @import作用是:spring IOC容器中没有注入EnableAutoConfigurationImportSelector类,但springboot启动需要用到,因此通过@Import注解将该类注入到spring容器

@SpringBootApplication
public class Application {
public static void main(String[] args) {
     SpringApplication.run(Application.class);
}


@SpringBootConfiguration
@EnableAutoConfiguration
public @interface SpringBootApplication {}


@AutoConfigurationPackage
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}

springboot通过@SpringBootApplication注解,在spring的基础上进行功能扩展。引入其他业务组建的平台逻辑(扩展操作),在@Import(EnableAutoConfigurationImportSelector.class)中实现,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 {

@Import和xml配置的 <import />标签作用一样,允许通过它引入 @Configuration 注解的类 (java config), 引入ImportSelector接口(要通过它去判定要引入哪些@Configuration) 和 ImportBeanDefinitionRegistrar 接口的实现, 也包括 @Component注解的普通类。但是如果要引入另一个xml 文件形式配置的 bean, 则需要通过 @ImportResource 注解。

EnableAutoConfigurationImportSelector继承AutoConfigurationImportSelector (继承接口ImportSelector)类,并覆盖其isEnabled方法,后续实例化EnableAutoConfigurationImportSelector时,调用的isEnabled方法,取自覆盖后的方法。

ImportSelector接口

@Import 实现,通常要借助 ImportSelector 接口的实现类决定引入哪些 @Configuration。 如果ImportSelector实现类,实现了以下四个Aware 接口的一个或多个(EnvironmentAware、BeanFactoryAware、BeanClassLoaderAware、ResourceLoaderAware), 在bean生命周期的初始化阶段, 会先回调aware接口方法的实现,织入系统变量,使得实例化bean拥有操作系统变量的能力。

/**
 * Interface to be implemented by types that determine which @{@link Configuration}
 * class(es) should be imported based on a given selection criteria, usually one or more
 * annotation attributes.
 *
 * An {@link ImportSelector} may implement any of the following
 * {@link org.springframework.beans.factory.Aware Aware} interfaces, and their respective
 * methods will be called prior to {@link #selectImports}:
 * {@link org.springframework.context.EnvironmentAware EnvironmentAware}
 * {@link org.springframework.beans.factory.BeanFactoryAware BeanFactoryAware}
 * {@link org.springframework.beans.factory.BeanClassLoaderAware BeanClassLoaderAware}
 * {@link org.springframework.context.ResourceLoaderAware ResourceLoaderAware}
 * */
public interface ImportSelector {

    /**
     * Select and return the names of which class(es) should be imported based on
     * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
     */
    String[] selectImports(AnnotationMetadata importingClassMetadata);

}

5、AutoConfigurationImportSelector.selectImports()方法

@Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        }
        try {
            AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
                    .loadMetadata(this.beanClassLoader);
            AnnotationAttributes attributes = getAttributes(annotationMetadata);
            List<String> configurations = getCandidateConfigurations(annotationMetadata,
                    attributes);
            configurations = removeDuplicates(configurations);
            configurations = sort(configurations, autoConfigurationMetadata);
            Set<String> 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

下面展示如何和此类建立关系。
进入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<String> getCandidateConfigurations(AnnotationMetadata metadata,
            AnnotationAttributes attributes) {
        List<String> 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;
    }


    /**
     * Return the class used by {@link SpringFactoriesLoader} to load configuration
     * 返回SpringFactoriesLoader类加载配置需要的接口类
     * candidates.
     * @return the factory class
     */
    protected Class<?> getSpringFactoriesLoaderFactoryClass() {
        return EnableAutoConfiguration.class;
    }

注释中的描述,使用SpringFactoriesLoader类、AutoConfigurationImportSelector.getSpringFactoriesLoaderFactoryClass()组合,加载指定资源。
SpringFactoriesLoader.loadFactoryNames所需的参数,其中一个参数通过调用getSpringFactoriesLoaderFactoryClass(),获取返回值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<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
        String factoryClassName = factoryClass.getName(); //factoryClassName的取值为“EnableAutoConfiguration"
        try {
//FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"
//urls为查找到的spring.factories文件列表
            Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                    ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
            List<String> result = new ArrayList<String>();
            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={‘org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration’,‘org.springframework.boot.autoconfigure.aop.AopAutoConfiguration’,‘org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration’,,,,,,}

Springboot 对@Import注解的处理过程
入口
AbstractApplicationContext.refresh() 
-> AbstractApplicationContext.invokeBeanFactoryPostProcessors(beanFactory)
->PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> 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<SourceClass> 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<SourceClass> 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));
                    }
                }
            ,,,,
        }
    }

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352