SpringBoot(九)自动配置原理

引言

传统的Spring项目,需要写一大堆的的xml配置文件,或者以注解的形式定义大量的配置类。而SpringBoot引入了自动配置的特性,简化了这些繁琐的配置过程。
SpringBoot遵循“约定优于配置”的原则,对一些常规的配置项作为默认配置,减少或不使用xml配置,让项目可以很快速地搭建运行起来。SpringBoot还为许多常用框架封装了starter,只需要引入一个starter,在不需要配置或只需很少的配置情况下,就能够使用这个框架的功能。

本文主要分析SpringBoot是怎么自动配置的。

源码分析

@SpringBootApplication

我们知道SpringBoot项目启动类上都会标注@SpringBootApplication,SpringBoot的自动配置也要从这个注解开始讲起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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 {


//省略...
}

@SpringBootApplication是一个复合注解,它包含了3个重要的注解:

  • @SpringBootConfiguration:该注解表示这是一个 SpringBoot 的配置类。
  • @ComponentScan:开启组件扫描。这是Spring的底层注解,作用是Spring会扫描basePackages属性指定的包下带有@Component注解的类,为这些类创建代理对象,并注入到IOC容器中。@Service@Controller等这些注解包含了@Component注解的语义,所以也会被扫描。
  • @EnableAutoConfiguration:开启自动配置。

自动配置是就是通过@EnableAutoConfiguration这个注解实现的,所以接着分析@EnableAutoConfiguration

@EnableAutoConfiguration

1
2
3
4
5
6
7
8
9
10
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

//省略...
}

@EnableAutoConfiguration也是一个复合注解,包含了两个重要注解:

  • @AutoConfigurationPackage:表示注解的这个类所在包及其子包中的将通过AutoConfigurationPackages类自动注册到Spring容器中。
  • @Import(AutoConfigurationImportSelector.class)@Import是Spring的底层注解,通过快速导入的方式将实例注册到Spring的IOC容器中。这里导入了一个AutoConfigurationImportSelector实例。

小总结:@AutoConfigurationPackage的功能是自动扫描注册bean;@AutoConfigurationImportSelector的功能是自动配置。

AutoConfigurationImportSelector

AutoConfigurationImportSelector类中有个重要的方法getAutoConfigurationEntry(),Spring Boot项目在启动的时候会调用该方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

/**
* Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata}
* of the importing {@link Configuration @Configuration} class.
* @param autoConfigurationMetadata the auto-configuration metadata
* @param annotationMetadata the annotation metadata of the configuration class
* @return the auto-configurations that should be imported
*/
protected AutoConfigurationEntry getAutoConfigurationEntry(
AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//获取所有候选配置
List<String> configurations = getCandidateConfigurations(annotationMetadata,
attributes);
//去重
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);
}


/**
* 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) {
//扫描类路径下的所有META-INF/spring.factories文件,并返回自动配置类的全类名
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;
}

SpringFactoriesLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

/**
* The location to look for factories.
* <p>Can be present in multiple JAR files.
*/
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";



/**
* 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
* @throws IllegalArgumentException if an error occurs while loading factory names
* @see #loadFactories
*/
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

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

try {
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 = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
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);
}
}

根据上面的分析,spring.factories是一个关键的文件,在很多starter里都会带一个spring.factories文件,里面都是一些配置信息,格式为key-value形式,多个Value时使用英文逗号“,”隔开。
SpringBoot项目都会自动引入spring-boot-autoconfigurejar包,这个jar里的spring.factories文件中很多关于类的配置,最终这些key-value会保存到一个MultiValueMap<String, String>中(interface MultiValueMap<K, V> extends Map<K, List<V>>),然后在经过去重、对要排除的配置项过滤等操作,得到最终的结果。(@SpringBootApplication@EnableAutoConfiguration注解都可以通过exclude属性来指定需要排除掉那些自动配置项)。
而真正使自动配置生效的key是org.springframework.boot.autoconfigure.EnableAutoConfiguration。这个key的value包含Spring Boot中可能会用到的许多自动配置类的全类名。

上面的urls表示的是如果引入的jar里有多个spring.factories文件,urls里存储的是多个spring.factories文件的绝对路径。比如spring-boot-autoconfigure下的spring.factories文件只包含了基本的自动配置文件,当引入了Spring Cloud的一些组件之后,比如Feign、Ribbon、Hystrix之后,都会有对应的spring.factories文件被引入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 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,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.jest.JestAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\
org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\
org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\
org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
org.springframework.boot.autoconfigure.reactor.core.ReactorCoreAutoConfiguration,\

//省略....

这些 xxxAutoConfiguration 这样的类如果在最终在上面map中,那最终会被实例化加入到spring容器中。
这就是Springboot自动配置之源,也就是自动配置的开始。

示例分析

下面以其中一个自动配置类HttpEncodingAutoConfiguration(Http编码自动配置)为例解释自动配置原理。

HttpEncodingAutoConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration  //表示这是Spring的一个配置类
@EnableConfigurationProperties(HttpProperties.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(CharacterEncodingFilter.class)
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled",
matchIfMissing = true)
public class HttpEncodingAutoConfiguration {

private final HttpProperties.Encoding properties;

//只有一个构造器的情况下,构造器的参数值会从容器中取
public HttpEncodingAutoConfiguration(HttpProperties properties) {
this.properties = properties.getEncoding();
}

@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));
return filter;
}
}

下面分析一下这个类上的几个注解:

  • @EnableConfigurationProperties(HttpProperties.class)
    使指定类的ConfigurationProperties功能生效(此处是HttpProperties类)。我们知道如果一个配置类只配置@ConfigurationProperties注解,而没有使用@Component,那么在spring容器中是获取不到 properties 配置文件转化的bean。说白了,此处的@EnableConfigurationProperties(HttpProperties.class)可以让HttpProperties获取到properties或yml配置文件里的值,并且生成一个HttpProperties实例注入到spring容器中。

  • @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
    这个注解基于Spring的底层注解:@Conditional。表示这个配置类里的配置生效还需要一个条件:实在一个web应用中,并且是一个servlet的web应用。

  • @ConditionalOnClass(CharacterEncodingFilter.class)
    表示类路径下要有CharacterEncodingFilter这个类。

  • @ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)
    表示配置文件中要有spring.http.encoding.enabled这个配置项,并且等于havingValue的值或者havingValue的值为空且配置的值不为空才生效。matchIfMissing的意思缺少该配置属性时是否可以加载。如果为true,没有该配置属性时也会正常加载;反之则不会生效。关于havingValue的使用示例如下:

havingValue的规则举例

属性值 havingValue=”” havingValue=”true” havingValue=”false” havingValue=”abc”
“true” yes yes no no
“false” no no yes no
“abc” yes no no yes

HttpEncodingAutoConfiguration中配置了一个CharacterEncodingFilter实例,这个过滤器里的参数是从properties属性获取。另外一点就是,向spring容器中注入的类,如果类只有一个构造器,那么构造器的参数会从容器中反向注入。

HttpProperties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@ConfigurationProperties(prefix = "spring.http")
public class HttpProperties {


/**
* HTTP encoding properties.
*/
private final Encoding encoding = new Encoding();


public Encoding getEncoding() {
return this.encoding;
}

//....


public static class Encoding {

public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

/**
* Charset of HTTP requests and responses. Added to the "Content-Type" header if
* not set explicitly.
*/
private Charset charset = DEFAULT_CHARSET;

/**
* Whether to force the encoding to the configured charset on HTTP requests and
* responses.
*/
private Boolean force;

/**
* Whether to force the encoding to the configured charset on HTTP requests.
* Defaults to true when "force" has not been specified.
*/
private Boolean forceRequest;

/**
* Whether to force the encoding to the configured charset on HTTP responses.
*/
private Boolean forceResponse;

}


}

HttpEncodingProperties类上面标注了@ConfigurationProperties注解,实例化时会从配置文件中获取指定的值和bean的属性进行绑定。HttpProperties示例持有一个Encoding变量,Encoding中有4个属性:charset、force、forceRequest和forceResponse,所以我们在配置文件中可以配置的就是这4个参数。

小结论:所有在配置文件中能配置的属性都是在 xxxxProperties 类中封装着;配置文件能配置什么就可以参照某个功能对应的这个属性类。配置文件中的提示功能是来自于spring-configuration-metadata.json文件。

再回过来看HttpEncodingAutoConfiguration中配置的CharacterEncodingFilter示例,这个过滤器里的参数就是我们在配置文件中

1
2
3
4
5
6
7
8
9
@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));
return filter;
}

总结

  1. SpringBoot启动会加载大量的自动配置类;
  2. 首先,看我们需要的功能Spring Boot有没有已经写好的自动配置类;
  3. 如果有,就再看这个自动配置类中到底配置了哪些组件,如果有这个组件了,就在配置文件中按照规则来配置。
  4. 如果没有我们需要的组件,那么就需要我们自己来写一个配置类来配置需要的功能组件。

拓展

@Conditional派生注解

作用:必须是@Conditional指定的条件成立,才给容器中注入实例,配置类的内容才生效;

@Conditional扩展注解 作用(判断是否满足当前指定条件)
@ConditionalOnJava 系统的java版本是否符合要求
@ConditionalOnBean 容器中存在指定Bean
@ConditionalOnMissingBean 容器中不存在指定Bean
@ConditionalOnExpression 满足SpEL表达式指定
@ConditionalOnClass 系统中有指定的类
@ConditionalOnMissingClass 系统中没有指定的类
@ConditionalOnSingleCandidate 容器中只有一个指定的Bean,或者这个Bean是首选Bean
@ConditionalOnProperty 系统中指定的属性是否有指定的值
@ConditionalOnResource 类路径下是否存在指定资源文件
@ConditionalOnWebApplication 当前是web环境
@ConditionalOnNotWebApplication 当前不是web环境
@ConditionalOnJndi JNDI存在指定项

怎么知道哪些自动配置类生效了

在配置文件中设置:debug=true,项目启动时将会打印自动配置报告。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

//启用了的自动配置类
Positive matches:
-----------------

CodecsAutoConfiguration matched:
- @ConditionalOnClass found required class 'org.springframework.http.codec.CodecConfigurer' (OnClassCondition)

CodecsAutoConfiguration.JacksonCodecConfiguration matched:
- @ConditionalOnClass found required class 'com.fasterxml.jackson.databind.ObjectMapper' (OnClassCondition)


//没有启用的自动配置类
Negative matches:
-----------------

ActiveMQAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class 'javax.jms.ConnectionFactory' (OnClassCondition)

AopAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class 'org.aspectj.lang.annotation.Aspect' (OnClassCondition)

------ 本文完 ------