Spring 自动装配原理是什么?

Spring Boot 一个很大的特点就是极大的简化了原来在 Spring 中复杂的 XML 文件配置过程,让我们对 Spring 应用对搭建和开发变得极其简单。既然可以简化配置,那就意味着很多配置都是需要默认的,这也使其提出了约定大于配置自动装配的思想。一些通用的配置会默认设置好,整个组件需要的时候直接载入,不需要的时候可以整个卸载。

通过 Spring Boot 我们可以很方便的引入新的组件,只需要在依赖文件中加入对应的 xxx-starter 即可,然后把一些必要的配置比如 url 信息做个简单的设置,或者增加一个 @EnableXXX,就可以开始使用了。

这里以 Feign 为例:

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
@SpringBootApplication
@EnableFeignClients
public class SampleApplication {

   public static void main(String[] args) {
      SpringApplication.run(SampleApplication.class, args);
   }
}

只需要这样,就可以将 Feign 引入项目中了,接下来根据自己的需要定义对应的 Feign client 即可,是不是非常简单。

Spring Boot 到底是如何做到的呢?

自动装配原理

自动装配的入口

自动装配的基础,是 Spring 从 4.x 版本开始支持 JavaConfig,让开发者可以免去繁琐的 xml 配置形式,而是使用熟悉的 Java 代码加注解,通过 @Configuration、@Bean 等注解可以直接向 Spring 容器注入 Bean 信息。

那么就有种设想,如果我把一些必须的 Bean 以 Java 代码方式准备好呢,只需要引入对应的配置类,相应的 Bean 就会被加载到 Spring 容器中。所以,有了这个基础 Spring Boot 就有了实现自动装配的可能。

还是以 Feign 为例,FeignAutoConfiguration 这个类就是一个 Feign 的自动装配类,我们来探究一下他是如何生效的。

@Configuration
@ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({ FeignClientProperties.class,
      FeignHttpClientProperties.class })
public class FeignAutoConfiguration {

   @Autowired(required = false)
   // 注入了一堆FeignClientSpecification
   private List<FeignClientSpecification> configurations = new ArrayList<>();

   @Bean
   public HasFeatures feignFeature() {
      return HasFeatures.namedFeature("Feign", Feign.class);
   }

   @Bean
   public FeignContext feignContext() {
      FeignContext context = new FeignContext();
      context.setConfigurations(this.configurations);
      return context;
   }
}

在每个 Spring Boot 的启动类上,都会有这样一个复合注解 @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 {
}

……省略其他注解
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}

上面四个都是一些通用注解,关键在于下面的 @EnableAutoConfiguration注解,从名字就可以看出来,它可以开启自动配置。在之前一篇文章我们已经讲过,通过 @Import 注解我们可以导入一些自定义的 BeanDefination 信息,或者导入一些配置类。在 EnableAutoConfiguration 内部,它使用 @Import 标注了AutoConfigurationImportSelector。

继承关系

可以看出,它实现了 ImportSelector 接口,之前我们已经在讲结果 @Import 注解是如何生效的。

下面,先对后面涉及的一些类和文件做一个简单介绍。

ImportSelector

看名字就可以知道,这是用于导入的选择器。String[] selectImports() 方法会返回需要导入的配置类的全路径名。在 Spring 容器启动的过程中会调用 invokeBeanFactoryPostProcessors,然后会执行一个重要的后置处理器 ConfigurationClassPostProcessor ,完成配置类的解析,这里会处理 ImportSelector 返回的这些类,将其加载到容器中。

public interface ImportSelector {
   String[] selectImports(AnnotationMetadata importingClassMetadata);
}

DeferredImportSelector

DeferredImportSelector 继承了ImportSelector,它的作用是用于延迟导入。在所有的需要处理的配置类解析过程中,继承此接口的解析排在最后,并且在有多个 DeferredImportSelector 实现类的情况下,可以继承 Ordered 实现排序的效果。

public interface DeferredImportSelector extends ImportSelector {

   @Nullable
   default Class<? extends Group> getImportGroup() {
      return null;
   }

   interface Group {
   }
}

继续之前的内容,AutoConfigurationImportSelector 实现了 DeferredImportSelector 接口,所

在执行 ConfigurationClassParser.processImports()方法的时候,最终会调用到下面这段逻辑。一般继承 ImportSelector 会执行其 selectImport 方法。但是这里不同的是,它还继承了 DeferredImportSelector 接口,对 ImportSelector 只是间接继承。在 processImports() 方法中有这样的额外判断,如果是 DeferredImportSelector 的子类,将会执行 deferredImportSelectorHandler.handle(),最终会回调 AutoConfigurationImportSelectorprocess 方法。具体的调用过程请见下图。

执行链路

Spring Boot 2.x的版本与 1.x有所不同,1.x 是回调 selectImports 方法。

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
      Collection<SourceClass> importCandidates, boolean checkForCircularImports) {
……
    if (candidate.isAssignable(ImportSelector.class)) {
    ……
    // 执行这段逻辑
    if (selector instanceof DeferredImportSelector) {
        this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
    }
    else {
    ……
        processImports(configClass, currentSourceClass, importSourceClasses, false);
    }
    ……
}

读取配置

process() 方法中主要做了一件事,读取并解析 spring.factories 配置文件中的信息,将这些配置文件对应的全路径类名都放入 AutoConfigurationEntry 集合中。具体逻辑如下:

getAutoConfigurationMetadata() 方法读取并解析了 spring-autoconfigure-metadata.properties 文件,用于控制自动装配条件。关于这个路径信息,追踪方法可以找到,比较简单。

protected static final String PATH = "META-INF/" + "spring-autoconfigure-metadata.properties";

getAutoConfigurationEntry() 方法会获取需要自动装配的类和需要排除的类,读取的文件是 META-INF/spring.factories

关于这个文件路径是怎么指定的,可以在下面方法中看到。一直深入追踪,在 loadSpringFactories 方法中,会加载 META-INF/spring.factories 路径下的配置内容,并且这个路径是硬编码写死的。在全部读取完毕之后,会放在一个 Map 中,key 为类名,value 为对应的自定义配置类。getSpringFactoriesLoaderFactoryClass() 方法会固定返回 EnableAutoConfiguration.class,所以这里只会返回 EnableAutoConfiguration 对应的配置内容,配置文件内容如下图。

配置文件
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
   ……
   // getAutoConfigurationMetadata() 方法读取并解析了 spring-autoconfigure-metadata.properties 文件,用于控制自动装配条件
   // AutoConfigurationEntry 方法会获取需要自动装配的类和需要排除的类
   AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
         .getAutoConfigurationEntry(getAutoConfigurationMetadata(), annotationMetadata);
   // 添加到 AutoConfigurationEntry集合中等待加载
   this.autoConfigurationEntries.add(autoConfigurationEntry);
   for (String importClassName : autoConfigurationEntry.getConfigurations()) {
      this.entries.putIfAbsent(importClassName, annotationMetadata);
   }
}

// 返回自动配置的类名,加载Spring.factories中的配置信息
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
   // loadFactoryNames 会读取对应的配置文件,位置在META-INF/spring.factories中
  // getSpringFactoriesLoaderFactoryClass 返回 EnableAutoConfiguration.class
   List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
         getBeanClassLoader());
  ……
   return configurations;
}

具体执行方法的调用链路如下:

process方法执行链路

加载配置

到这里我们就明白,spring.properties 文件中的配置类是如何加载的,但是问题来了,他什么时候注册到 Spring 容器中呢?

回到之前执行过程中的 processGroupImports() 方法,这里会调用 getImports 拿到配置类信息,然后再次调用类信息,然后递归调用 processImports,这个方法之前的文章已经解释过了,如果是配置类会解析并注册 Spring 的 Bean 信息,具体请自行查看之前文章。

public void processGroupImports() {
   for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
     // getImports 得到解析后的类名
      grouping.getImports().forEach(entry -> {
         ConfigurationClass configurationClass = this.configurationClasses.get(entry.getMetadata());
         try {
           // 再次调用 processImports 递归处理,对配置类解析,注册为 Bean
            processImports(configurationClass, asSourceClass(configurationClass),
                  asSourceClasses(entry.getImportClassName()), false);
         }
         catch (BeanDefinitionStoreException ex) {
           ……
         }
      });
   }
}

另外,再额外说一下 getImports 方法。之前 process 方法并没有返回值,而是把配置信息都保存在了 autoConfigurationEntries 中,所以在执行完 process 之后会紧接着执行 selectImports()。它的功能主要是排除需要排除的类信息,并且在这里按照 spring-autoconfigure-metadata.properties 中指定的顺序排序,然后再返回类信息。

public Iterable<Group.Entry> getImports() {
   for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
   // 调用 process 逻辑
      this.group.process(deferredImport.getConfigurationClass().getMetadata(),
            deferredImport.getImportSelector());
   }
   return this.group.selectImports();
}

public Iterable<Entry> selectImports() {
    if (this.autoConfigurationEntries.isEmpty()) {
        return Collections.emptyList();
    }
    // 获取所有需要排除的类集合
    Set<String> allExclusions = this.autoConfigurationEntries.stream()
            .map(AutoConfigurationEntry::getExclusions).flatMap(Collection::stream).collect(Collectors.toSet());
    // 获取所有需要装配的类集合
    Set<String> processedConfigurations = this.autoConfigurationEntries.stream()
            .map(AutoConfigurationEntry::getConfigurations).flatMap(Collection::stream)
            .collect(Collectors.toCollection(LinkedHashSet::new));
    // 移除所有排除类
    processedConfigurations.removeAll(allExclusions);
    // 将需要加载的类排序返回,排序规则按照 spring-autoconfigure-metadata.properties 中指定的顺序
    return sortAutoConfigurations(processedConfigurations, getAutoConfigurationMetadata()).stream()
            .map((importClassName) -> new Entry(this.entries.get(importClassName), importClassName))
            .collect(Collectors.toList());
}

总结

最后,我们再次总结一下整个自动装配的过程。

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

推荐阅读更多精彩内容