揭秘SpringBoot自动装配的魔法

前言

首先,自动装配包含了哪些的装配?

  • 自动将包下的被打上@Component注解以及其派生注解的类作为Bean注册到IOC容器中
  • 自动注册jar包中需要被注册的Bean到IOC容器中

这篇文章将从底层源码分析,SpringBoot是如何做到自动装配Bean的。其中起到关键作用的两大关键注解是

而以上两大关键注解都元标注了@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 {
  // ...
}

我们通常会将被标注该注解的类注册到Spring容器中,之后,就会自动帮我们装配jar中配置的一些需要的Bean了。就像下面这样

@SpringBootApplication
public class UserServiceApplication {
  public static void main(String[] args) {
		// 将UserServiceApplication作为Bean注册到Spring中
    SpringApplication.run(UserServiceApplication.class, args);
  }
}

这样,就开启了自动挡,自动装配Bean到Spring中。

希望读者具有ConfigurationClassPostProcessor配置注解处理类的基础,才能更好的了解自动装配的魔法。

关于ConfigurationClassPostProcessor的分析可以查看 配置注解驱动的处理

自动装配的魔法

开头的分析就给了我们一个入口,也就是@EnableAutoConfiguration、@ComponentScan这两大注解,接下来我们就以这两个注解为入口,分析自动装配的魔法。

@ComponentScan扫描并注册

首先,来一个开胃小菜,这块内容相对比较简单,所以先来分析Bean的扫描并注册的过程。

我们知道,在SpringBoot中我们需要将引导类放在包的外层,然后那些@Service、@Controller等等@Component的派生注解都会被注册到Spring中,作为SpringBean。例如下面这样的结构
在这里插入图片描述
那么这是为什么呢?Spring解析配置注解驱动类ConfigurationClassPostProcessor负责@ComponentScan注解的解析工作,在解析时,由如下逻辑进行处理

其实这段逻辑在 配置注解驱动的处理 这篇文章中就已经讲述过,这里只做大致的描述。

// Process any @ComponentScan annotations
// 这里主要拿到@ComponentScan注解的元信息,里面包含注解中的属性值
// 如果是ComponentScans,也会分解成一个个的ComponentScan
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
  sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
// 从这里可以看出,这里也会进行@Conditional的条件过滤
// 并且阶段是REGISTER_BEAN,表示要开始注册Bean了
if (!componentScans.isEmpty() &&
    !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
  for (AnnotationAttributes componentScan : componentScans) {
    // The config class is annotated with @ComponentScan -> perform the scan immediately
    // 扫描并注册Bean
    Set<BeanDefinitionHolder> scannedBeanDefinitions =
      this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
    // Check the set of scanned definitions for any further config classes and parse recursively if needed
    for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
      BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
      if (bdCand == null) {
        bdCand = holder.getBeanDefinition();
      }
      // 这里还会判断是否需要像配置类解析那样进行解析
      if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
        // 如果需要,则解析之,此方法就是刚开始的方法
        parse(bdCand.getBeanClassName(), holder.getBeanName());
      }
    }
  }
}

这里最为关键的是componentScanParser的parse方法,扫描并注册Bean

public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
  // 主要扫描的类
  ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
                                                                              componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);

  // ...略过一些ComponentScan注解属性的配置
  
  Set<String> basePackages = new LinkedHashSet<>();
  // 获取basePackages属性值
  // 下面主要针对属性值,获取需要被扫描的包名
  String[] basePackagesArray = componentScan.getStringArray("basePackages");
  for (String pkg : basePackagesArray) {
    String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
                                                           ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
    Collections.addAll(basePackages, tokenized);
  }
  // 获取需要被注册的Class
  for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
    basePackages.add(ClassUtils.getPackageName(clazz));
  }
  // 如果没有配置需要扫描的包,就使用declaringClass的包名
  if (basePackages.isEmpty()) {
    basePackages.add(ClassUtils.getPackageName(declaringClass));
  }

  // ...
  
  // 扫描并注册
  return scanner.doScan(StringUtils.toStringArray(basePackages));
}

如果没有配置包名,将扫描默认包

// 如果没有配置需要扫描的包,就使用declaringClass的包名
if (basePackages.isEmpty()) {
  basePackages.add(ClassUtils.getPackageName(declaringClass));
}

而这个默认包值declaringClass是多少呢?可以回顾上面,此参数是配置类的全限定类名,对应最开头的例子,就是UserServiceApplication的全限定类名,假设此时获得的类名为com.microservice.original.UserServiceApplication,继续会调用getPackageName方法,拿到包名

public static String getPackageName(String fqClassName) {
  Assert.notNull(fqClassName, "Class name must not be null");
  // 拿到最后一个.的位置
  int lastDotIndex = fqClassName.lastIndexOf(PACKAGE_SEPARATOR);
  // 截取,这样包名就有了
  return (lastDotIndex != -1 ? fqClassName.substring(0, lastDotIndex) : "");
}

此时,拿到的包名就是com.microservice.original,看到这里,应该就可以理解,为什么SpringBoot引导类放置的位置这么有讲究了。当然你可以在项目中打上多个@ComponentScan,或者是在注解中的basePackage属性中指定要扫描的包名,这样,扫描的范围就会很大了。

至于扫描并注册的方法scanner.doScan这里不多赘述,此内容是AnnotationConfigApplicationContext注解配置上下文的内容。

@EnableAutoConfiguration自动注册Bean

这里就是我们的重头戏了。此注解是注册jar包里的Bean的关键注解。我们之前讲过的Enable模式,其实质上是利用了@Import注解进行的Bean注册,所以这个注解也不例外,核心就在@Import注解中

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}

可以发现,其元标注了Import注解,而该注解的值AutoConfigurationImportSelector即为我们重点关注的对象。而AutoConfigurationImportSelector这个类是DeferredImportSelector的子类,也就是延迟加载的Import,会在普通Bean注册之后才会进行注册,时机是相对比较后面的。由前两篇Enable模式和注解驱动解析的铺垫,我们这里就有一个第一反应,看AutoConfigurationImportSelector的selectImports方法

public String[] selectImports(AnnotationMetadata annotationMetadata) {
  if (!isEnabled(annotationMetadata)) {
    return NO_IMPORTS;
  }
  AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
    .loadMetadata(this.beanClassLoader);
  // 拿到需要被装配的配置类信息
  AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
    autoConfigurationMetadata, annotationMetadata);
  // 这里将需要被装配的配置类全类名字符串数组返回出去,然后会被处理注册到Sring中
  return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

其方法返回值会被Spring处理,然后注册到IOC容器中去,这一点在上两篇文章都有详细的分析,不做过多的赘述。所以这里我们的关键分析点在于,如何拿到那些jar包中需要被装配的配置类信息呢?看看getAutoConfigurationEntry方法都做了什么

protected AutoConfigurationEntry getAutoConfigurationEntry(
  AutoConfigurationMetadata autoConfigurationMetadata,
  AnnotationMetadata annotationMetadata) {
  if (!isEnabled(annotationMetadata)) {
    return EMPTY_ENTRY;
  }
  
  AnnotationAttributes attributes = getAttributes(annotationMetadata);
  // 获取需要被装配的类的类名
  List<String> configurations = getCandidateConfigurations(annotationMetadata,
                                                           attributes);
  
  // 去重,主要将其交给Set集合完成去重工作
  configurations = removeDuplicates(configurations);
  
  // 获取需要排除的列表
  Set<String> exclusions = getExclusions(annotationMetadata, attributes);
  checkExcludedClasses(configurations, exclusions);
  // 排除上面的列表包含的类
  configurations.removeAll(exclusions);
  // 过滤
  configurations = filter(configurations, autoConfigurationMetadata);
  // 发布一个自动装配的事件
  fireAutoConfigurationImportEvents(configurations, exclusions);
  return new AutoConfigurationEntry(configurations, exclusions);
}

到这里可以看到,获取候选的Bean列表有以下几个步骤:

  1. 获取需要被装配的类名
  2. 排除用户定义的列表
  3. 过滤掉不需要被装配的Bean

下面,我们就这几个步骤进行详细的分析。

获取候选装配的Bean类名

直接进入getCandidateConfigurations方法,看看是如何获取需要被装配的Bean的类名的

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
                                                  AnnotationAttributes attributes) {
  List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
    getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
  // ...
  return configurations;
}

其实,这里使用了一种SpringFactory机制去加载,先看看getSpringFactoriesLoaderFactoryClass方法做了什么

protected Class<?> getSpringFactoriesLoaderFactoryClass() {
  return EnableAutoConfiguration.class;
}

也就是说,这里调用了SpringFactoriesLoader的loadFactoryNames方法,传的第一个参数是EnableAutoConfiguration这个注解类的全限定类名,第二个参数是一个类加载器,继续深入SpringFactoriesLoader的loadFactoryNames方法看看

public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
  // EnableAutoConfiguration全限定类名
  String factoryClassName = factoryClass.getName();
  // 先loadSpringFactories获取SpringFactories的信息
  // 然后getOrDefault获取EnableAutoConfiguration全限定类名对应的信息
  return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

这里首先会获取SpringFactories的信息,那么是什么信息呢?接下去看看

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
  // 缓存机制
  MultiValueMap<String, String> result = cache.get(classLoader);
  if (result != null) {
    return result;
  }

  try {
    // 使用classLoader拿到FACTORIES_RESOURCE_LOCATION路径下的资源
    // 资源是一个URL对象的形式
    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 properties = PropertiesLoaderUtils.loadProperties(resource);
      for (Map.Entry<?, ?> entry : properties.entrySet()) {
        // Properties中key为factoryClassName
        String factoryClassName = ((String) entry.getKey()).trim();
        // value为一个字符串数组
        // 也就是说factoryClassName会对应多个value
        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);
  }
}

那么,FACTORIES_RESOURCE_LOCATION路径是什么呢?

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

原来,这里会拿到classPath下的META-INF下的spring.factories文件,这里我们全局搜索一下这个文件里都有哪些内容

# 截取一小段内容作为示例
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\

也就是说,这里会将AutoConfigurationImportFilter、EnableAutoConfiguration作为key,其value作为其对应的value保存下来,这里,我们可以总结一下SpringFactories加载机制

SpringFactories加载机制

注意,这很重要,因为此加载机制贯穿了SpringBoot的很多地方。

在整个项目打包成jar的过程中, resource下的资源会合并在一个文件夹,此时就会被SpringFactories加载机制所读取,上述是会读取META-INF/spring.factories路径的文件,将其保存为一个Map<ClassLoader, Map<String, String>>这样一个数据结构,这就是SpringFactories加载机制。

获取SpringFactories中指定的factory

到这里,就加载好了spring.factories文件的内容,将调用getOrDefault方法,获取指定factoryClassName对应的value,也就是我们上面所说的,这里是EnableAutoConfiguration全类名对应的value。

public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
  String factoryClassName = factoryClass.getName();
  // 获取EnableAutoConfiguration全类名对应的value
  return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

回顾一下我们上面所说的spring.factories文件中的内容,其中EnableAutoConfiguration全类名对应的value有非常多,也就是说,此value有多少,就会返回多少出去,这里的value就是需要被自动装配的Bean。

看到这里,就明白了自动装配的魔法,其实是将需要被装配的Bean提前定义在spring.factories文件中(需要在META-INF下),然后必须以org.springframework.boot.autoconfigure.EnableAutoConfiguration为key,其value就是自动装配的Bean。

我们可以看到,在SpringBoot的jar包中,就有如下结构
在这里插入图片描述
这里,瞬间可以明白,原来自动装配的魔法,都是SpringBoot开发人员提前定义好的,也就是说,在此文件中写好一次,就可以供广大使用者直接使用,在各种starter的jar包中都是这种模式,例如你开发了一个mybatis,你就可以将其变成一个SpringBoot-starter,引入jar包就可以马上使用mybatis的功能,不需要客户端定义配置Bean。开发人员定义一次,所有用户省去重复装配劳动,可谓是一个伟大的发明。

排除候选装配的Bean

可以看到,这里拿到需要被装配的类名单之后,会做一系列的排除操作。

我们知道,要失效某些Bean的自动装配,需要如下操作

  • 代码配置方式
    • @EnableAutoConfiguration
      • exclude()
      • excludeName()
  • 外部化配置方式
    • 配置属性:spring.autoconfigure.exclude

以上方式在官方文档中也有描述。而以上操作就对应于上面方法中这一段代码

// 获取需要排除的列表
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
// 排除上面的列表包含的类
configurations.removeAll(exclusions);

看看getExclusions方法是如何获取需要被排除的列表

protected Set<String> getExclusions(AnnotationMetadata metadata,
                                    AnnotationAttributes attributes) {
  Set<String> excluded = new LinkedHashSet<>();
  // 获取注解中的exclude值
  excluded.addAll(asList(attributes, "exclude"));
  // 获取注解中的excludeName值
  excluded.addAll(Arrays.asList(attributes.getStringArray("excludeName")));
  // 获取配置属性spring.autoconfigure.exclude的值
  excluded.addAll(getExcludeAutoConfigurationsProperty());
  return excluded;
}
private static final String PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE = "spring.autoconfigure.exclude";

private List<String> getExcludeAutoConfigurationsProperty() {
  // ...
  // 获取spring.autoconfigure.exclude对应的值
  String[] excludes = getEnvironment()
    .getProperty(PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE, String[].class);
  return (excludes != null) ? Arrays.asList(excludes) : Collections.emptyList();
}

需要被排除的列表就是上述的那几个方式,很简单。

过滤候选装配的Bean

这里将过滤一些Bean,其中大部分自动装配的Bean都在这里被过滤掉了。那么是根据什么进行过滤的呢?又为什么要过滤呢?带着这两个问题往下看

configurations = filter(configurations, autoConfigurationMetadata);
private List<String> filter(List<String> configurations,
                            AutoConfigurationMetadata autoConfigurationMetadata) {
  long startTime = System.nanoTime();
  String[] candidates = StringUtils.toStringArray(configurations);
  boolean[] skip = new boolean[candidates.length];
  boolean skipped = false;
  // 获取filter
  for (AutoConfigurationImportFilter filter : getAutoConfigurationImportFilters()) {
    invokeAwareMethods(filter);
    // match列表,下标和上面configurations列表是对应的
    boolean[] match = filter.match(candidates, autoConfigurationMetadata);
    for (int i = 0; i < match.length; i++) {
      // 如果对应下标的match=false
      if (!match[i]) {
        skip[i] = true;
        // 将其从列表中移除
        candidates[i] = null;
        skipped = true;
      }
    }
  }
  // 如果skipped=false,证明没有Bean被排除,返回原列表即可
  if (!skipped) {
    return configurations;
  }
  List<String> result = new ArrayList<>(candidates.length);
  for (int i = 0; i < candidates.length; i++) {
    if (!skip[i]) {
      result.add(candidates[i]);
    }
  }
  // ...
  // 返回最终过滤的结果
  return new ArrayList<>(result);
}

这段逻辑,最为关键的就是getAutoConfigurationImportFilters获取过滤器和过滤器的match方法了。首先来看一下是如何获取过滤器的

protected List<AutoConfigurationImportFilter> getAutoConfigurationImportFilters() {
   return SpringFactoriesLoader.loadFactories(AutoConfigurationImportFilter.class,
         this.beanClassLoader);
}

又是很熟悉的SpringFactories加载机制,只不过这里获取AutoConfigurationImportFilter为key的value值,这个loadFactories方法和上面介绍的不一样,上面介绍的加载机制是loadFactoryNames方法,但也类似,因为loadFactories方法内部调用了loadFactoryNames方法,先获取了对应key的value列表,不同点只在于其会判断是否是FactroyClass的子类,也就是上述获取出来的value列表将判断是否是AutoConfigurationImportFilter的子类,如果不是会抛出异常。

总之万变不离其宗,只不过多了判断是否是子类,并且实例化出来的这一步。

这里我们搜索一下AutoConfigurationImportFilter在spring.factories文件中对应哪些value

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

其实,就是三个@Conditional的条件过滤。不过,这里的条件从哪来呢?关有filter没条件也不行啊。

这里我们追踪一下filter.match(candidates, autoConfigurationMetadata)的第二参数是从哪里传进来的。回到最开始的selectImports方法

public String[] selectImports(AnnotationMetadata annotationMetadata) {
  if (!isEnabled(annotationMetadata)) {
    return NO_IMPORTS;
  }
  // 条件元信息
  AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
    .loadMetadata(this.beanClassLoader);
  AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
    autoConfigurationMetadata, annotationMetadata);
  return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

原来,条件是从一开始我们没有分析的那一行代码中传进来的。让我们来看看这一行做了什么

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

public static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {
   return loadMetadata(classLoader, PATH);
}

原来,这里会加载META-INF下的spring-autoconfigure-metadata.properties文件中的信息,并封装为AutoConfigurationMetadata对象。这里我们看看这个文件的内容

org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration=
org.springframework.boot.autoconfigure.kafka.KafkaAnnotationDrivenConfiguration.Configuration=
org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration.ConditionalOnClass=org.influxdb.InfluxDB

这里定义了那些自动装配的Bean的装配条件。例如InfluxDbAutoConfiguration的自动装配条件是ConditionalOnClass,也就是说,其会在类路径下存在org.influxdb.InfluxDB这样的Class,就符合自动装配的条件。其作用和@ConditionalOnClass是一样的。

那么现在就可以解答我们上面的两个问题了

  • 是根据什么进行过滤的呢?

    答:根据META-INF下的spring-autoconfigure-metadata.properties文件中的信息,格式需要以需要过滤的Bean的全限定类名为key,加上你需要过滤的类型,例如ConditionalOnClass,就在key后面加上ConditionalOnClass,然后value是条件值,之后有配置这个的Bean就会像Conditional那样条件过滤了

  • 又为什么要过滤呢?

    答:由于很多自动装配的Bean不需要被装配,因为根本没有引入这样的jar包,比如JPA,我没有引入这个jar包,在classPath下自然就不会存在JAP对应的某些关键Class,此时就可以用ConditionalOnClass这个条件过滤提前把这个过滤掉,表示JPA根本不需要被自动装配到Spring中。虽然在自动装配类上打上注解进行过滤也是可以做到条件过滤的,但个人认为,这种提前过滤机制,可以极大增加加载自动装配类的速度。如果不进行提前过滤,那么将把比如100多个Bean全加载出来,然后一个个被Spring进行注解解析然后判断,这样效率太低了,在预加载其类名的时候就把它截断会来的更快一些。

发布自动装配获取完成事件

接下来就是发布一个事件了。回忆一下,发布事件的代码是这样的

protected AutoConfigurationEntry getAutoConfigurationEntry(
  // ...
  // 发布一个自动装配的事件
  fireAutoConfigurationImportEvents(configurations, exclusions);
  return new AutoConfigurationEntry(configurations, exclusions);
}
private void fireAutoConfigurationImportEvents(List<String> configurations,
                                               Set<String> exclusions) {
  // 获取监听器
  List<AutoConfigurationImportListener> listeners = getAutoConfigurationImportListeners();
  if (!listeners.isEmpty()) {
    // 封装一个事件
    AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this,
                                                                          configurations, exclusions);
    for (AutoConfigurationImportListener listener : listeners) {
      invokeAwareMethods(listener);
      // 调用监听器的onAutoConfigurationImportEvent方法
      listener.onAutoConfigurationImportEvent(event);
    }
  }
}

这里首先调用了getAutoConfigurationImportListeners方法获取到监听器列表,然后逐个调用监听器的onAutoConfigurationImportEvent方法。那么是如何获取到列表的呢?

protected List<AutoConfigurationImportListener> getAutoConfigurationImportListeners() {
  return SpringFactoriesLoader.loadFactories(AutoConfigurationImportListener.class,
                                             this.beanClassLoader);
}

又是熟悉的SpringFactories加载机制。这里获取AutoConfigurationImportListener实例。接下来我们自定义一个监听器

自定义自动装配监听器

首先先定义一个AutoConfigurationImportListener监听器实现类

public class CustomerAutoConfigurationListener implements AutoConfigurationImportListener {
    @Override
    public void onAutoConfigurationImportEvent(AutoConfigurationImportEvent event) {
        
        // 获取classLoader
        ClassLoader classLoader = event.getClass().getClassLoader();

        // 候选的自动装配Bean列表
        List<String> candidates = SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, classLoader);

        // 实际的自动装配Bean列表
        List<String> candidateConfigurations = event.getCandidateConfigurations();

        // 排除的Bean列表
        Set<String> exclusions = event.getExclusions();

        System.out.println("自动装配Class名单");
        System.out.println("候选数量: " + candidates.size());
        System.out.println("实际数量: " + candidateConfigurations.size());
        System.out.println("被排除的数量: " + exclusions.size());

        System.out.println("实际被装配的Bean名单");
        candidateConfigurations.forEach(System.out::println);

        System.out.println("被排除的Bean名单");
        exclusions.forEach(System.out::println);
        
    }
}

在MATA-INF下创建一个spring.fatcories文件,将以上实现放入该文件中

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
com.mytest.condition.listener.CustomerAutoConfigurationListener

接着编写引导类,我们这里排除一个会被自动装配的Bean

@EnableAutoConfiguration(exclude = SpringApplicationAdminJmxAutoConfiguration.class)
public class ContextBootstrap {
  public static void main(String[] args) {
    // 配置ContextBootstrap为引导Bean
    new SpringApplicationBuilder(ContextBootstrap.class)
      // 非WEB方式
      .web(WebApplicationType.NONE)
      // 启动上下文
      .run(args)
      // 关闭上下文
      .close();
  }

这里的EnableAutoConfiguration注解会使其产生自动装配,从而会发布一个自动装配事件,事件将会被我们上面的监听器所处理到。接下来看看结果
在这里插入图片描述
可以看到,有118个Bean会被自动装配,实际上排除+过滤了110个Bean,在最开始的地方截断了大部分的Bean的装配,有利于SpringBoot应用启动的时间。

总结

到这里,整个SpringBoot自动装配的流程大致就结束了。在@EnableAutoConfiguration的Import处理中,最终会返回一个需要被装配的Bean列表,其将会交给ConfigurationClassPostProcessor类进行处理,最终将此列表注册到Spring容器中,并将其视作配置类进行解析(@Bean、@ComponentScan、@Import之类的解析工作),而这段解析工作在 配置注解驱动的处理 这篇文章中将会有详细的描述。

其实,SpringBoot自动装配的魔法源自于Spring的能力,@Import导入类+SpringFactories加载机制,这些都是Spring3.0、4.0为注解驱动作出的贡献,在Spring5.0中,注解驱动逐渐完善。然而施法的人是开发jar包的人,这些人可以自定义starter(带有魔法的符咒)给用户使用,用户只需要加入starter的jar包,即可享受带有魔法的自动装配。一次劳动解决了万千开发重复配置的劳动。

发布了84 篇原创文章 · 获赞 85 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/qq_41737716/article/details/98028738