自动配置到底配了什么?

对于一个Spring项目,主要就是有两种配置:

  1. 一种是类似端口号、数据库地址、用户名密码等
  2. 一种是各种Bean,比如整合Mybatis需要配置的MapperFactoryBean,比如整合事务需要配置DataSourceTransactionManager

SpringBoot中的自动配置,更多的是配置各种Bean,因为对于第一种配置,SpringBoot也无法去配置,比如数据库地址、密码之类的,SpringBoot肯定是无法知道的,但是对于端口号这些配置,SpringBoot也是会提供一种默认值的,也相当于一种自动配置。

那SpringBoot是如何自动的帮助我们来配置这些Bean的呢?并且如果某些Bean程序员自己也配置了,那SpringBoot是如何进行选择的呢?

自动配置类

SpringBoot要自动帮我们配置Bean,那要支持的就多了:

  1. 比如Spring整合各种Servlet容器(Tomcat、Jetty)的Bean
  2. 比如Spring整合各种消息队列的Bean
  3. 等等

所以SpringBoot中肯定不能把所有的Bean都定义在一个配置类中,需要分门别类,这样就针对不同的场景定义了不同的自动配置类,比如:

  1. ServletWebServerFactoryAutoConfiguration:配置了Servlet Web场景中所需要的一些Bean
  2. TransactionAutoConfiguration:配置了事务场景中所需要的一些Bean
  3. AopAutoConfiguration:配置了AOP场景中所需要的一些Bean
  4. RabbitAutoConfiguration:配置了Rabbitmq场景中所需要的一些Bean
  5. 等等

使用这种结构后,SpringBoot就能让程序员更为方便的来控制某个Bean或某些Bean要不要生效,如果某个自动配置类不生效,那该配置类中所定义的Bean则都不会生效。

那SpringBoot中是如何控制某个自动配置类或某个Bean生不生效呢?

条件注解

SpringBoot中的条件注解有:

  1. ConditionalOnBean:是否存在某个某类或某个名字的Bean
  2. ConditionalOnMissingBean:是否缺失某个某类或某个名字的Bean
  3. ConditionalOnSingleCandidate:是否符合指定类型的Bean只有一个
  4. ConditionalOnClass:是否存在某个类
  5. ConditionalOnMissingClass:是否缺失某个类
  6. ConditionalOnExpression:指定的表达式返回的是true还是false
  7. ConditionalOnJava:判断Java版本
  8. ConditionalOnJndi:JNDI指定的资源是否存在
  9. ConditionalOnWebApplication:当前应用是一个Web应用
  10. ConditionalOnNotWebApplication:当前应用不是一个Web应用
  11. ConditionalOnProperty:Environment中是否存在某个属性
  12. ConditionalOnResource:指定的资源是否存在
  13. ConditionalOnWarDeployment:当前项目是不是以War包部署的方式运行
  14. ConditionalOnCloudPlatform:是不是在某个云平台上

当然我们也可以利用@Conditional来自定义条件注解。

条件注解是可以写在类上和方法上的,如果某个条件注解写在了自动配置类上,那该自动配置类会不会生效就要看当前条件能不能符合,或者条件注解写在某个@Bean修饰的方法上,那这个Bean生不生效就看当前条件符不符合。

具体原理是:

  1. Spring在解析某个自动配置类时,会先检查该自动配置类上是否有条件注解,如果有,则进一步判断该条件注解所指定的条件当前能不能满足,如果满足了则继续解析该配置类,如果不满足则不进行解析了,也就是配置类所定义的Bean都得不到解析,也就是相当于没有这些Bean了。
  2. 同理,Spring在解析某个@Bean的方法时,也会先判断方法上是否有条件注解,然后进行解析,如果不满足条件,则该Bean不会生效

我们可以发现,SpringBoot的自动配置,实际上就是SpringBoot的源码中预先写好了一些配置类,预先定义好了一些Bean,我们在用SpringBoot时,这些配置类就已经在我们项目的依赖中了,而这些自动配置类或自动配置Bean到底生不生效,就看具体所指定的条件了。

Starter机制

那SpringBoot中的Starter和自动配置又有什么关系呢?

其实首先要明白一个Starter,就是一个Maven依赖,当我们在项目的pom.xml文件中添加某个Starter依赖时,其实就是简单的添加了很多其他的依赖,比如:

  1. spring-boot-starter-web:引入了spring-boot-starter、spring-boot-starter-json、spring-boot-starter-tomcat等和Web开发相关的依赖包
  2. spring-boot-starter-tomcat:引入了tomcat-embed-core、tomcat-embed-el、tomcat-embed-websocket等和Tomcat相关的依赖包

如果硬要把Starter机制和自动配置联系起来,那就是通过@ConditionalOnClass这个条件注解,因为这个条件注解的作用就是用来判断当前应用的依赖中是否存在某个类或某些类,比如:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
static class EmbeddedTomcat {
    
    @Bean
    TomcatServletWebServerFactory tomcatServletWebServerFactory(
        ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
        ObjectProvider<TomcatContextCustomizer> contextCustomizers,
        ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        
        // orderedStream()调用时会去Spring容器中找到TomcatConnectorCustomizer类型的Bean,默认是没有的,程序员可以自己定义
        factory.getTomcatConnectorCustomizers()
            .addAll(connectorCustomizers.orderedStream().collect(Collectors.toList()));
        factory.getTomcatContextCustomizers()
            .addAll(contextCustomizers.orderedStream().collect(Collectors.toList()));
        factory.getTomcatProtocolHandlerCustomizers()
            .addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
        return factory;
    }
    
}

上面代码中就用到了@ConditionalOnClass,用来判断项目中是否存在Servlet.class、Tomcat.class、UpgradeProtocol.class这三个类,如果存在就满足当前条件,如果项目中引入了spring-boot-starter-tomcat,那就有这三个类,如果没有spring-boot-starter-tomcat那就可能没有这三个类(除非你自己单独引入了Tomcat相关的依赖)。

所以这就做到了,如果我们在项目中要用Tomcat,那就依赖spring-boot-starter-web就够了,因为它默认依赖了spring-boot-starter-tomcat,从而依赖了Tomcat,从而Tomcat相关的Bean能生效。

而如果不想用Tomcat,那就得这么写:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	<exclusions>
		<exclusion>
			<groupId>org.springframework.boot</groupId>
      		<artifactId>spring-boot-starter-tomcat</artifactId>
    	</exclusion>
	</exclusions>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

得把spring-boot-starter-tomcat给排除掉,再添加上spring-boot-starter-jetty的依赖,这样Tomcat的Bean就不会生效,Jetty的Bean就能生效,从而项目中用的就是Jetty。

@ConditionalOnMissingBean

@ConditionalOnMissingBean的作用是用来判断某个Bean是否缺失,如果不存在某个Bean,那就符合该条件,理解起来比较简单,但是细细想一下就会存在一个问题,就是顺序问题,还是拿上面的代码举例:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
static class EmbeddedTomcat {
    
    @Bean
    TomcatServletWebServerFactory tomcatServletWebServerFactory(
        ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
        ObjectProvider<TomcatContextCustomizer> contextCustomizers,
        ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        
        // orderedStream()调用时会去Spring容器中找到TomcatConnectorCustomizer类型的Bean,默认是没有的,程序员可以自己定义
        factory.getTomcatConnectorCustomizers()
            .addAll(connectorCustomizers.orderedStream().collect(Collectors.toList()));
        factory.getTomcatContextCustomizers()
            .addAll(contextCustomizers.orderedStream().collect(Collectors.toList()));
        factory.getTomcatProtocolHandlerCustomizers()
            .addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
        return factory;
    }
    
}

这个代码中就用到了@ConditionalOnMissingBean,意思是如果当前不存在ServletWebServerFactory类型的Bean,那就符合条件,结合整体代码,意思就是,:

  1. 如果用户自己没有定义ServletWebServerFactory类型的Bean,那代码中所定义的Bean就会生效,
  2. 如果用户自己定义了ServletWebServerFactory类型的Bean,那代码中定义的Bean就不生效

所以这个注解是非常重要的,SpringBoot利用这个注解来决定到底用用户自己的Bean,还是用SpringBoot自动配置的。

关键问题在于,不管是自动配置类中定义的Bean,还是用户定义的Bean,都是需要解析的,而且是有一个顺序的。

如果是:

  1. 先解析的SpringBoot自动配置类,比如上面的EmbeddedTomcat类
  2. 再解析程序员定义的Bean

那@ConditionalOnMissingBean的判断结果是有问题的,因为是先解析的EmbeddedTomcat,在解析的时候是没有发现程序员所定义的Bean的,就会认为符合@ConditionalOnMissingBean的条件,而实际上程序员是定义了的,只是还没有解析到,所以这就需要SpringBoot把这个顺序控制好,控制为:

  1. 先解析用户的定义的Bean,也就是解析用户定义的配置类(包含了扫描和@Bean的解析)
  2. 再解析SpringBoot中的自动配置类

不管是用户定义的配置类还是自动配置类,都是配置类(简单理解就是加了@Configuration注解)。SpringBoot启动时,最核心的也就是创建一个Spring容器,而创建Spring容器的过程中会注解做几件事情:

  1. 把SpringApplication.run(MyApplication.class)传入进来的MyApplication类做为配置类进行解析
  2. 由于MyApplication类上定义了@SpringBootApplication,相当于定义了@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan注解
  3. 所以SpringBoot会进一步解析这些注解
    a) @EnableAutoConfiguration:通过@import注解导入AutoConfigurationImportSelector这个配置类,因为它实现了DeferredImportSelector接口,所以Spring会在把其他配置类都解析完之后才解析AutoConfigurationImportSelector(Spring Framework中的知识)
    b) @ComponentScan:扫描,扫描时会扫描到用户所定义的配置类,并解析用户的配置类,注意:扫描是扫描不到SpringBoot的自动配置的类,因为扫描的包路径不匹配,SpringBoot的包都是org.springframework.boot.xxxx,用户都是自己的包路径。

通过上述过程我们发现,Spring会在最后才来解析AutoConfigurationImportSelector这个配置类,而这个类的作用就是用来解析SpringBoot的自动配置类,那既然无法扫描到SpringBoot中的自动配置类,那怎么知道SpringBoot中有哪些自动配置类呢,这就需要spring.factories文件,默认情况下,SpringBoot会提供一个spring.factories文件,并把所有自动配置类的名字记录在这个文件中,到时候启动过程中解析这个文件就知道有哪些自动配置类了,并且这件事也是发生在解析完用户的配置类之后的。

自动配置开启原理

我们在使用SpringBoot时,通常使用的是@SpringBootApplication这个注解,比如:

@SpringBootApplication
public class MyApplication {

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

}

而这个注解的定义为:

@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 {
// ...
}

可以发现这个注解上有另外三个注解:

  1. @SpringBootConfiguration
  2. @EnableAutoConfiguration
  3. @ComponentScan

所以我们可以认为@SpringBootApplication是一个三合一注解,也就是我们也可以这么用:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
public class MyApplication {

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

如果我们这么用就能自己控制要不要用@EnableAutoConfiguration这个注解,如果用就表示开启自动配置,如果不用就表示不开启自动配置,那开启和不开启自动配置到底该怎么理解呢?

我们前面分析过,SpringBoot的自动配置就是SpringBoot帮助程序员配置一些Bean,从而让程序员在用SpringBoot时可以少去配置很多Bean,所以如果我们开启了自动配置,那最终Spring容器中就有SpringBoot帮我们配置的Bean,如果没有开启自动配置,那Spring容器中就没有这些Bean,就需要程序员去配置。

那我们来看看@EnableAutoConfiguration这个注解是如何工作的?先看源码定义:

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

其中非常核心的就是:

@Import(AutoConfigurationImportSelector.class)

查找自动配置类

AutoConfigurationImportSelector实现了DeferredImportSelector这个接口,Spring容器在启动时,会在解析完其他所有程序员定义的配置类之后,来调用AutoConfigurationImportSelector中的selectImports方法,然后把该方法返回的类名对应的类作为配置类进行解析。

org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#selectImports

public String[] selectImports(AnnotationMetadata annotationMetadata) {
	if (!isEnabled(annotationMetadata)) {
		return NO_IMPORTS;
	}
	// 查找自动配置类
	AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
	return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

该方法会利用SpringFactoriesLoader找到所有的META-INF/spring.factories文件中key为EnableAutoConfiguration.class的value值,也就是众多自动配置类的类名。拿到这些类名后会进行去重,去重完之后,就会看是否存在某些自动配置类需要排除,我们可以通过@EnableAutoConfiguration注解的exclude属性,或者spring.autoconfigure.exclude配置来指定一些自动配置类的名字,然后把它们从自动配置类集合中排除掉。

org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#getAutoConfigurationEntry

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
	if (!isEnabled(annotationMetadata)) {
		return EMPTY_ENTRY;
	}
	AnnotationAttributes attributes = getAttributes(annotationMetadata);
	// 扫描 spring.factories
	List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
	// 去重
	configurations = removeDuplicates(configurations);
	Set<String> exclusions = getExclusions(annotationMetadata, attributes);
	checkExcludedClasses(configurations, exclusions);
	// 去除排除的自动配置类
	configurations.removeAll(exclusions);
	// 使用AutoConfigurationImportFilter进一步进行筛选
	configurations = getConfigurationClassFilter().filter(configurations);
	fireAutoConfigurationImportEvents(configurations, exclusions);
	return new AutoConfigurationEntry(configurations, exclusions);
}

然后会继续利用ConfigurationClassFilter对自动配置类进行进一步筛选,ConfigurationClassFilter会利用AutoConfigurationMetadata进行筛选,而AutoConfigurationMetadata对象对应的是"META-INF/spring-autoconfigure-metadata.properties"文件中的内容,这是一种加快SpringBoot启动速度的机制,默认是开启了的(不过要通过maven或gradle的方式引入springboot的依赖来使用才能看到效果,因为这个文件的内容是在SpringBoot源码工程编译的时候自动生成出来的,当然我们也可以手动创建这个文件,以及这个文件的内容),自动生成的这个文件内容样例:

org.springframework.boot.actuate.autoconfigure.amqp.RabbitHealthContributorAutoConfiguration=
org.springframework.boot.actuate.autoconfigure.amqp.RabbitHealthContributorAutoConfiguration.AutoConfigureAfter=org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
org.springframework.boot.actuate.autoconfigure.amqp.RabbitHealthContributorAutoConfiguration.ConditionalOnBean=org.springframework.amqp.rabbit.core.RabbitTemplate
org.springframework.boot.actuate.autoconfigure.amqp.RabbitHealthContributorAutoConfiguration.ConditionalOnClass=org.springframework.amqp.rabbit.core.RabbitTemplate
org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration=
org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration.ConditionalOnBean=org.springframework.boot.actuate.audit.AuditEventRepository
org.springframework.boot.actuate.autoconfigure.audit.AuditEventsEndpointAutoConfiguration=
org.springframework.boot.actuate.autoconfigure.audit.AuditEventsEndpointAutoConfiguration.AutoConfigureAfter=org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration
org.springframework.boot.actuate.autoconfigure.availability.AvailabilityHealthContributorAutoConfiguration=
org.springframework.boot.actuate.autoconfigure.availability.AvailabilityHealthContributorAutoConfiguration.AutoConfigureAfter=org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration
org.springframework.boot.actuate.autoconfigure.availability.AvailabilityProbesAutoConfiguration=
org.springframework.boot.actuate.autoconfigure.availability.AvailabilityProbesAutoConfiguration.AutoConfigureAfter=org.springframework.boot.actuate.autoconfigure.availability.AvailabilityHealthContributorAutoConfiguration,org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration
org.springframework.boot.actuate.autoconfigure.cache.CachesEndpointAutoConfiguration=

内容格式为:自动配置类名.条件注解=条件

有了这个文件的内容,SpringBoot会在通过spring.facotries文件找到所有的自动配置类后,会把这个文件中的内容读出来,然后利用AutoConfigurationImportFilter对所有的自动配置类进行条件匹配,这里的条件判断,只会判断所需要的类是否存在,如果需要的类,或者需要的Bean对应的类,都不存在,那么肯定不符合条件了,对于像@ConditionalOnMissingBean这样的条件,在这一步是不会去判断的,最后条件匹配成功的自动配置类就会记录下来,并最终返回给Spring容器,继续进行其他条件的匹配。

所以通过这个机制,使得Spring并不需要解析所有的自动配置类,从而提高了效率。

当然在这个过滤的过程中,如果日志级别等于trace级别,那么会把所有条件不匹配的自动配置类记录到日志中,如果日志框架配置了打印到控制台,那就会打印到控制台,比如:

logging.level.root=trace

日志如下:

2022-09-05 10:16:21.328 TRACE 3704 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Returning cached instance of singleton bean 'autoConfigurationReport'
2022-09-05 10:16:21.442 TRACE 3704 --- [           main] o.s.b.a.condition.OnClassCondition       : Condition OnClassCondition on org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration did not match due to @ConditionalOnClass did not find required class 'com.rabbitmq.client.Channel'
2022-09-05 10:16:21.447 TRACE 3704 --- [           main] o.s.b.a.condition.OnClassCondition       : Condition OnClassCondition on org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration did not match due to @ConditionalOnClass did not find required class 'org.springframework.batch.core.launch.JobLauncher'
2022-09-05 10:16:21.447 TRACE 3704 --- [           main] o.s.b.a.condition.OnClassCondition       : Condition OnClassCondition on org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration did not match due to @ConditionalOnClass did not find required class 'com.datastax.oss.driver.api.core.CqlSession'
2022-09-05 10:16:21.447 TRACE 3704 --- [           main] o.s.b.a.condition.OnClassCondition       : Condition OnClassCondition on org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration did not match due to @ConditionalOnClass did not find required class 'com.couchbase.client.java.Cluster'
2022-09-05 10:16:21.448 TRACE 3704 --- [           main] o.s.b.a.condition.OnClassCondition       : Condition OnClassCondition on org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration did not match due to @ConditionalOnClass did not find required class 'org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor'
2022-09-05 10:16:21.448 TRACE 3704 --- [           main] o.s.b.a.condition.OnClassCondition       : Condition OnClassCondition on org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration did not match due to @ConditionalOnClass did not find required class 'com.datastax.oss.driver.api.core.CqlSession'

在SpringBoot中,还有一个更加强大的统计自动配置类匹配结果的功能,就是可以配置debug=true,只要开启了这个配置,那么Spring在解析每一个自动配置类时,就会将是否匹配的结果进行记录,最后再一起打印出来,上面的是每解析一个就打印一个。

比如开启了debug=true,我们可以在控制台看到:

============================
CONDITIONS EVALUATION REPORT
============================


Positive matches:
-----------------

   AopAutoConfiguration matched:
      - @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition)

   AopAutoConfiguration.ClassProxyingConfiguration matched:
      - @ConditionalOnMissingClass did not find unwanted class 'org.aspectj.weaver.Advice' (OnClassCondition)
      - @ConditionalOnProperty (spring.aop.proxy-target-class=true) matched (OnPropertyCondition)

   DispatcherServletAutoConfiguration matched:
      - @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition)
      - found 'session' scope (OnWebApplicationCondition)

   DispatcherServletAutoConfiguration.DispatcherServletConfiguration matched:
      - @ConditionalOnClass found required class 'javax.servlet.ServletRegistration' (OnClassCondition)
      - Default DispatcherServlet did not find dispatcher servlet beans (DispatcherServletAutoConfiguration.DefaultDispatcherServletCondition)

可以看到这个匹配结果分别记录了:

  1. 哪些自动配置类的条件是匹配的
  2. 哪些自动配置类的条件是不匹配的,并且具体原因也会打印出来,比如是哪个类不存在
  3. 哪些自动配置类是无条件的

这个功能的实现,是Spring解析具体的自动配置类上的各种条件注解的时候统计的,每解析一个条件注解,就会把结果记录在ConditionEvaluationReport对象中,当Spring容器启动完成后,会发布一个ContextRefreshedEvent事件,而SpringBoot提供了一个ConditionEvaluationReportLoggingListener会处理这个事件,接收到这个事件后就会把统计结果进行打印。

自动配置类解析的大体流程为:

  1. 读取spring.factories中的所有自动配置类
  2. 看是否配置了需要排除的自动配置类,进行排除
  3. 然后利用spring-autoconfigure-metadata.properties文件来过滤掉一些自动配置类(条件中指定的类不存在的自动配置类)
  4. 解析过滤后自动配置类,判断自动配置类所有的条件注解,条件全部符合才会真正去解析自动配置类上的其他内容,比如@Bean(也会进行条件判断)