八、浅析SpringBoot自动配置(源码分析)

247 阅读4分钟

现在SpringBoot项目已经普及,对于@SpringBootApplication注解肯定也不陌生。另外在SpringBoot项目下,我们也进行使用自动配置功能,也就是引入某个模块的jar包,SpringBoot就会自动配置对应功能,很容易就在SpringBoot项目下进行使用。本文主要就是分析SpringBoot自动配置的底层原理。不过还是要从@SpringBootApplication注解开始。

1. @SpringBootApplication

该注解包含了三个注解,@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan

  • @SpringBootConfiguration注解内容会标记@Configuration注解,也就是会作为一个配置类,该类上面可以做一些Bean的配置,以及该类也是一个Bean会在Spring IOC容器里。
  • @ComponentScan包的扫描路径,所以在模块情况下会扫描@SpringBootApplication所在的包路径。
  • @EnableAutoConfiguration开启自动配置,也是本文重点要分析的内容。
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
      @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {}

2. @EnableAutoConfiguration

Spring Boot实现自动装配,依赖@EnableAutoConfiguration注解背后的逻辑。在@SpringBootApplication注解下,会标记@EnableAutoConfiguration注解。

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

@EnableAutoConfiguration注解内部,通过@Import注解实现配置类的扫描,其中扫描的逻辑在AutoConfigurationImportSelector中,该注解还能通过执行字段排除不需要的加载的配置类。

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

2.1 AutoConfigurationImportSelector

AutoConfigurationImportSelector实现了ImportSelector接口,所以就会实现selectImports方法。

实际上AutoConfigurationImportSelector实现的是DeferredImportSelector接口,内部有个getImportGroup方法,SpringBoot真正执行的会是DeferredImportSelector$Group类。这里的逻辑比较复杂,先以selectImports方法串通逻辑,整体逻辑实际是类似的。

主要有三个逻辑:判断是否支持自动装配;获取元信息;获取配置类信息,内部会进行过滤筛选。

//AutoConfigurationImportSelector#selectImports
public String[] selectImports(AnnotationMetadata annotationMetadata) {
   //1.是否支持自动装配
   if (!isEnabled(annotationMetadata)) {
      return NO_IMPORTS;
   }
   //2. 获取元信息
   AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
         .loadMetadata(this.beanClassLoader);
   //3. 获取配置类信息
   AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
         annotationMetadata);
   return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

2.1.1 是否支持自动装配

首先判断当前的class是不是AutoConfigurationImportSelector,现在查看的就是AutoConfigurationImportSelector,所以条件成立。然后会判断环境变量中的spring.boot.enableautoconfiguration是否有配置,如果没有配置默认为true。也就是说,可以在applicaiton.yml中配置spring.boot.enableautoconfiguration为false,关闭自动装配。

//AutoConfigurationImportSelector#isEnabled
protected boolean isEnabled(AnnotationMetadata metadata) {
   if (getClass() == AutoConfigurationImportSelector.class) {
      return getEnvironment().getProperty(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class, true);
   }
   return true;
}

2.1.2 获取元信息

逻辑就是从META-INF/spring-autoconfigure-metadata.properties文件中,读取数据,封装成PropertiesAutoConfigurationMetadata,而该文件可能有多个,也就是说每个jar包内部可能都会包含该文件。

//AutoConfigurationMetadataLoader
protected static final String PATH = "META-INF/spring-autoconfigure-metadata.properties";
static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {
   return loadMetadata(classLoader, PATH);
}

static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader, String path) {
   try {
      Enumeration<URL> urls = (classLoader != null) ? classLoader.getResources(path)
            : ClassLoader.getSystemResources(path);
      Properties properties = new Properties();
      while (urls.hasMoreElements()) {
         properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource(urls.nextElement())));
      }
      return loadMetadata(properties);
   }
   catch (IOException ex) {
      throw new IllegalArgumentException("Unable to load @ConditionalOnClass location [" + path + "]", ex);
   }
}

static AutoConfigurationMetadata loadMetadata(Properties properties) {
   return new PropertiesAutoConfigurationMetadata(properties);
}

那么spring-autoconfigure-metadata.properties怎么来的?内容是什么?以及作用是什么?

首先该文件可以手动填写(待验证),但是Spring Boot也提供了工具,也就是spring-boot-autoconfigure-processor模块,该模块会在编译期间就会根据注解信息,生成spring-autoconfigure-metadata.properties文件。spring-boot-autoconfigure模块,内部就依赖了该模块,所以从源码中看不到spring-autoconfigure-metadata.properties文件,在编译后才能看到。

以spring-boot-autoconfigure为例,查看编译后的spring-autoconfigure-metadata.properties文件,可以发现对于WebMvcAutoConfiguration配置类中的@ConditionalOnClass条件注解需要Servlet、DispatcherServlet、WebMvcConfigurer,这个文件中已经把类路径给解析出来了。那么作用也很清楚了,就是通过编译期间的扫描,减少运行时Conditional注解解析,加快启动时间。

...
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.ConditionalOnClass=javax.servlet.Servlet,org.springframework.web.servlet.config.annotation.WebMvcConfigurer,org.springframework.web.servlet.DispatcherServlet

2.1.3 获取配置类信息

//AutoConfigurationImportSelector#getAutoConfigurationEntry
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
      AnnotationMetadata annotationMetadata) {
   //判断是否自动装配,因为是个通用接口,所以这里又判断了一次 
   if (!isEnabled(annotationMetadata)) {
      return EMPTY_ENTRY;
   }
   //1. 获取注解属性 
   AnnotationAttributes attributes = getAttributes(annotationMetadata);
   //2. 获取配置类
   List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
   //3. 去重 
   configurations = removeDuplicates(configurations);
   //4. 排除配置类 
   Set<String> exclusions = getExclusions(annotationMetadata, attributes);
   checkExcludedClasses(configurations, exclusions);
   configurations.removeAll(exclusions);
   //5. 过滤
   configurations = filter(configurations, autoConfigurationMetadata);
   //6. 通知 
   fireAutoConfigurationImportEvents(configurations, exclusions);
   return new AutoConfigurationEntry(configurations, exclusions);
}

获取注解属性,就会从会@EnableAutoConfiguration注解中获取属性,也就是exclude、excludeName这两个参数。

//AutoConfigurationImportSelector#getAttributes
protected AnnotationAttributes getAttributes(AnnotationMetadata metadata) {
   String name = getAnnotationClass().getName();
   AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(name, true));
 
   return attributes;
}
protected Class<?> getAnnotationClass() {
   return EnableAutoConfiguration.class;
}

获取配置类,就是会利用SpringFactoriesLoader类进行获取配置信息,SpringFactoriesLoader内部实际会从META-INF/spring.factories文件读取所需的类,

这里的类就是EnableAutoConfiguration。记住该类的使用,后面的场景还会用到该类获取其他信息。

SpringFactoriesLoader类是由Spring-Core提供的类

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

以spring-boot-autoconfigure模块为例,resources下的spring.factories文件如下,其中可以看到EnableAutoConfiguration相关的配置类信息。

...
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
...

因为每个依赖下都可能又spring.factories文件,所以需要对获取的配置类进行去重。去重原理也很简单,就是利用Set结构。

//AutoConfigurationImportSelector#removeDuplicates
protected final <T> List<T> removeDuplicates(List<T> list) {
   return new ArrayList<>(new LinkedHashSet<>(list));
}

获取排除信息就会从注解的exclude和excludeName中获取,同时还会从环境变量spring.autoconfigure.exclude中获取信息。

//AutoConfigurationImportSelector#getExclusions
protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {
   Set<String> excluded = new LinkedHashSet<>();
   excluded.addAll(asList(attributes, "exclude"));
   excluded.addAll(Arrays.asList(attributes.getStringArray("excludeName")));
   excluded.addAll(getExcludeAutoConfigurationsProperty());
   return excluded;
}

紧接着,会把排除信息进行校验,是否合法,如果不合法则会抛出异常(在handleInvalidExcludes方法内)。

//AutoConfigurationImportSelector#checkExcludedClasses
private void checkExcludedClasses(List<String> configurations, Set<String> exclusions) {
   List<String> invalidExcludes = new ArrayList<>(exclusions.size());
   for (String exclusion : exclusions) {
      if (ClassUtils.isPresent(exclusion, getClass().getClassLoader()) && !configurations.contains(exclusion)) {
         invalidExcludes.add(exclusion);
      }
   }
   if (!invalidExcludes.isEmpty()) {
      handleInvalidExcludes(invalidExcludes);
   }
}

校验完后,会把配置类信息根据排除类,进行移除。

//AutoConfigurationImportSelector#getAutoConfigurationEntry
configurations.removeAll(exclusions);

然后会根据条件进行过滤,因为并不是所有的xxxAutoConfiguration都是需要进行加载的。getAutoConfigurationImportFilters()方法内,就会通过SpringFactoriesLoader获取AutoConfigurationImportFilter类,也就是会在spring.factories文件中获取该类。

//AutoConfigurationImportSelector#filter
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;
   for (AutoConfigurationImportFilter filter : getAutoConfigurationImportFilters()) {
      invokeAwareMethods(filter);
      boolean[] match = filter.match(candidates, autoConfigurationMetadata);
      for (int i = 0; i < match.length; i++) {
         if (!match[i]) {
            skip[i] = true;
            candidates[i] = null;
            skipped = true;
         }
      }
   }
   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]);
      }
   }
   if (logger.isTraceEnabled()) {
      int numberFiltered = configurations.size() - result.size();
      logger.trace("Filtered " + numberFiltered + " auto configuration class in "
            + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) + " ms");
   }
   return new ArrayList<>(result);
}

在spring-boot-autoconfigure模块中,AutoConfigurationImportFilter的配置为OnBeanCondition、OnClassCondition、OnWebApplicationCondition。

  • OnBeanCondition类处理@ConditionalOnBean、@ConditionalOnMissingBean、@ConditionalOnSingleCandidate注解。
  • OnClassCondition类处理@ConditionalOnClass、@ConditionalOnMissingClass注解
  • OnWebApplicationCondition类处理@ConditionalOnWebApplication、@ConditionalOnNotWebApplication注解。

至于其他注解,则需要在配置类在Spring中解析的时候,进行判断是否要配置,如:@ConditionalOnProperty注解

实际代码处理在ConfigurationClassParser类中的processConfigurationClass方法,内部会通过ConditionEvaluator判断是否跳过。

org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

过滤的逻辑比较复杂,核心会通过类是否存在进行判断,后文会做简要分析。

最后会从SpringFactoriesLoader获取AutoConfigurationImportListener类,然后会触发onAutoConfigurationImportEvent方法。业务上可以根据需要,实现自己的AutoConfigurationImportListener,在spring.factories上进行配置。

//AutoConfigurationImportSelector#fireAutoConfigurationImportEvents
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);
         listener.onAutoConfigurationImportEvent(event);
      }
   }
}

2.2 条件判断

以OnClassCondition为例,内部继承了FilteringSpringBootCondition类和SpringBootCondition类,SpringBootCondition类实现了Condition接口,提供了抽象方法getMatchOutcome(),所以看OnClassCondition中实现的getMatchOutcome()逻辑,就是条件判断逻辑。

前面提到,OnClassCondition类处理@ConditionalOnClass、@ConditionalOnMissingClass这两注解,逻辑都类似,这里主要分析@ConditionalOnClass注解的处理。

主要有两个逻辑,首先从@ConditionalOnClass注解中,找出属性值,内部会把class也转成String的方式。其次就是会过滤,找过不存在的class,方便提示。

//OnClassCondition#getMatchOutcome
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
   ClassLoader classLoader = context.getClassLoader();
   ConditionMessage matchMessage = ConditionMessage.empty();
   //从注解中找出属性值,class也会转成string
   List<String> onClasses = getCandidates(metadata, ConditionalOnClass.class);
   if (onClasses != null) {
      //过滤是否有未找到的class,这样做是为了做提示
      List<String> missing = filter(onClasses, ClassNameFilter.MISSING, classLoader);
      if (!missing.isEmpty()) {
         return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnClass.class)
               .didNotFind("required class", "required classes").items(Style.QUOTE, missing));
      }
      matchMessage = matchMessage.andCondition(ConditionalOnClass.class)
            .found("required class", "required classes")
            .items(Style.QUOTE, filter(onClasses, ClassNameFilter.PRESENT, classLoader));
   }
   //...
   return ConditionOutcome.match(matchMessage);
}

过滤的逻辑主要是调用ClassNameFilter.matches方法,根据传参,这里是ClassNameFilter.MISSING这个枚举类。

//OnClassCondition#filter
protected final List<String> filter(Collection<String> classNames, ClassNameFilter classNameFilter,
      ClassLoader classLoader) {
   if (CollectionUtils.isEmpty(classNames)) {
      return Collections.emptyList();
   }
   List<String> matches = new ArrayList<>(classNames.size());
   for (String candidate : classNames) {
      if (classNameFilter.matches(candidate, classLoader)) {
         matches.add(candidate);
      }
   }
   return matches;
}

ClassNameFilter.MISSING内的逻辑,就是判断类不存在,就是尝试用类加载器加载类,如果加载不了就说明不存在。

//FilteringSpringBootCondition$ClassNameFilter
protected enum ClassNameFilter {
   MISSING {
      @Override
      public boolean matches(String className, ClassLoader classLoader) {
         return !isPresent(className, classLoader);
      }
   };

   abstract boolean matches(String className, ClassLoader classLoader);
   static boolean isPresent(String className, ClassLoader classLoader) {
      if (classLoader == null) {
         classLoader = ClassUtils.getDefaultClassLoader();
      }
      try {
         resolve(className, classLoader);
         return true;
      }
      catch (Throwable ex) {
         return false;
      }
   }
}
//FilteringSpringBootCondition#resolve
protected static Class<?> resolve(String className, ClassLoader classLoader) throws ClassNotFoundException {
   if (classLoader != null) {
      return classLoader.loadClass(className);
   }
   return Class.forName(className);
}

综上,可以做个小结

  • OnClassCondition内部过滤的逻辑就是判断对应的class是否存在,底层实现就是通过类加载尝试加载判断,没有抛异常则说明存在。
  • OnBeanCondition内部过滤的逻辑就是判断对应的bean是否存在,底层就是通过BeanFactory查找对应的类型/名称进行判断。
  • OnWebApplicationCondition内部过滤的逻辑就是判断当前的web类型是否符合,底层实现就是对应web类型的类存在,以及对应的web配置信息进行判断。

3. 自定义Starter

从上面自动配置的逻辑,我们可以看出,主要的流程,再结合官方的一些说明,可以整理出自定义Starter需要的步骤:

  1. 首先需要设置配置类,一般命名为xxxAutoConfiguration,然后该类上标记@Configuration注解,根据需要可以标记@ConditionalOnClass等注解,当类不存在时不会进行装配。内部的Bean也可以定义成@ConditionalOnMissingBean之类的,便于把实现类进行替换。(因为其他Bean的加载,在自动装配之前,所以可以判断是否有定义其他类)
  2. 在resource下的META-INF/spring.factories文件下配置org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxx.xxxx.xxxAutoConfiguration。
  3. (可选)最好能依赖spring-boot-autoconfigure-processor模块,这样在编译的时候就会生成spring-autoconfigure-metadata.properties文件,便于自动配置时进行过滤。不过一般都会依赖到spring-boot-autoconfigure模块,该模块内部依赖spring-boot-autoconfigure-processor。(不确定是否具有传递性)

3.1 SpringBoot官方starter

Spring Boot内部有很多定义好的的Starter,比如spring-boot-starter-web,但是它们并没有定义resource,这是因为SpringBoot把这些配置全部都配置在了spring-boot-autoconfigure模块,该模块内部的META-INF/spring.factories就包括了所有SpringBoot官方提供的AutoConfiguration。

3.2 dubbo的starter

我们可以看下dubbo是怎么配置自定的spring boot starter。dubbo-spring-boot-starter模块依赖了spring-boot-starter、dubbo-spring-boot-autoconfigure模块,自动配置在dubbo-spring-boot-autoconfigure模块中。

dubbo-spring-boot-autoconfigure的META-INF/spring.factories文件内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.apache.dubbo.spring.boot.autoconfigure.DubboRelaxedBinding2AutoConfiguration

DubboRelaxedBinding2AutoConfiguration配置类如下,和前面描述的逻辑类似。

@Configuration
@ConditionalOnProperty(prefix = DUBBO_PREFIX, name = "enabled", matchIfMissing = true)
@ConditionalOnClass(name = "org.springframework.boot.context.properties.bind.Binder")
@AutoConfigureBefore(DubboRelaxedBindingAutoConfiguration.class)
public class DubboRelaxedBinding2AutoConfiguration {}

3.3 FAQ

  1. 为什么有需要有@Configuration注解,还需要把xxxAutoConfiguration添加到spring.factories文件中?虽然有@Configuration注解,但是xxxxxxAutoConfiguration所在路径不一定能被扫描到。

  2. 为什么需要设置@Configuration注解?Spring底层解析需要,没有该注解无法解析配置类。

4. 参考资料

  1. www.cnblogs.com/lifullmoon/…

  2. docs.spring.io/spring-boot…

  3. SpringBoot源码分支2.2.x