Zer0e's Blog

浅谈Spring Boot自动配置

字数统计: 2.6k阅读时长: 11 min
2020/12/13 Share

前言

好久没写技术性文章了,今天就来谈谈SpringBoot中最常见但是却很少人去了解的知识,那就是SpringBoot是如何让你免去如此多的xml配置的。
本人技术有限,文章如有错误请谅解。

正文

用过SpringBoot的人都很清楚,它很方便,不用配置xml,开箱即用,那你是否想过SpringBoot是如何实现自动配置的呢?我也很好奇,那本文就深入源码看看SpringBoot都做了些什么来自动配置的吧。

start

那我们当然是新建一个项目啦,我们很清楚,springboot项目都用一个以@SpringBootApplication为注解的主类,这是我们项目的入口点。

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class FirstDemoApplication {

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

}

那通过上面的导入,我们知道@SpringBootApplication来自autoconfigure包中,看名字就知道这是我们需要的自动配置相关的包。那我们跟进这个注解。

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

这个注解里面使用@AliasFor将多个注解组合,简单解释就是将它上面的注解合并成当前注解,即SpringBootApplication。换句话来说,@SpringBootApplication就相当于@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan三个注解一起作用。
那其中@ComponentScan是我们比较熟悉的,用于扫描bean组件装配到ioc容器中,这里我们便不展开。
然后是@SpringBootConfiguration,跟进后我们发现其实就是@Configuration中的一种,也就是配置类,这里我们也不多讲。
本文的主角就是@EnableAutoConfiguration。

深入@EnableAutoConfiguration

我们跟进@EnableAutoConfiguration。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

Class<?>[] exclude() default {};

String[] excludeName() default {};
}

有两个主要注解,@AutoConfigurationPackage和@Import({AutoConfigurationImportSelector.class})。
我们先来看看@AutoConfigurationPackage。

@AutoConfigurationPackage

1
2
3
4
5
6
7
8
9
10
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
String[] basePackages() default {};

Class<?>[] basePackageClasses() default {};
}

这里提一下,import注解是spring提供的,它的作用是将某个类实例化后加入ioc容器中。
那么我们就可以知道@AutoConfigurationPackage其实就是将Registrar.class实例化后加入ioc容器当中。继续跟进Registrar类。

1
2
3
4
5
6
7
8
9
10
11
12
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
Registrar() {
}

public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
}

public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));
}
}

我们发现是一个静态内部类,其中有两个方法registerBeanDefinitions和determineImports。我们分别在两个方法中加入断点,调试下。其中(String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0])返回的是@SpringBootApplication注解所在的类的包名,在这个项目中就是com.example.first_demo
然后就是determineImports,在项目中没有触发断点,我们通过无敌的google大法知道返回的是一组代表要导入项的对象。这对本文的主题无多大关系,因此就略过。
至此,@AutoConfigurationPackage的作用就是将主配置类所在的包及子包中的所有组件扫描到ioc容器当中。

@Import({AutoConfigurationImportSelector.class})

重新回到@EnableAutoConfiguration,发现它将AutoConfigurationImportSelector添加到容器中。这应该就是自动配置的核心所在,我们跟进这个类。这个类中的方法很多,我们挑一些重要的来讲。
像getAutoConfigurationEntry方法,源码中还特意添加了注释。这个方法用于获取需要自动装配的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata}
* of the importing {@link Configuration @Configuration} class.
* @param annotationMetadata the annotation metadata of the configuration class
* @return the auto-configurations that should be imported
*/
protected AutoConfigurationEntry getAutoConfigurationEntry(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 = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}

在这个方法中又有一个比较重要的方法,getCandidateConfigurations。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 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) {
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.loadFactoryNames,SpringFactoriesLoader是Spring框架的一个内部工具类,用于加载类。继续跟进方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Load the fully qualified class names of factory implementations of the
* given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
* class loader.
* <p>As of Spring Framework 5.3, if a particular implementation class name
* is discovered more than once for the given factory type, duplicates will
* be ignored.
* @param factoryType 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<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

这个方法返回加载类的全名,通过loadSpringFactories私用方法,我们详细来看这个。

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
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}

result = new HashMap<>();
try {
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}

// Replace all lists with unmodifiable lists containing unique elements
result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
cache.put(classLoader, result);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
return result;
}

那么简单看下源码我们可以得知,该方法就是读取每个类中的META-INF/spring.factories文件中设置的所有xxxAutoConfiguration类,并使用PropertiesLoaderUtils.loadProperties获取其所有属性值,将这些值作为自动配置类添加到ioc容器当中,相当于完成了配置,以前需要手动配置的东西,自动配置帮我们添加了默认值。
我们可以看看spring-boot-autoconfigure中的spring.factories。这些都是默认自动装配的。
简单贴一些。

1
2
3
4
5
6
7
8
9
10
11
12
# 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.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\

比如我们看看AopAutoConfiguration

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
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Advice.class)
static class AspectJAutoProxyingConfiguration {

@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
matchIfMissing = false)
static class JdkDynamicAutoProxyConfiguration {

}

@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
static class CglibAutoProxyConfiguration {

}

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.aspectj.weaver.Advice")
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
static class ClassProxyingConfiguration {

ClassProxyingConfiguration(BeanFactory beanFactory) {
if (beanFactory instanceof BeanDefinitionRegistry) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
}

}

}

可以清楚地看到,springboot提供了默认配置类,并通过条件注解来加载配置到容器中。

第三方jar自动配置

从上面我们已经可以知道springboot会扫描所有类的spring.factories,并从中读取自动配置类。就比如常用的mybatis,我们在项目中导入mybatis,并查看它的依赖。
一般来说,我们在springboot项目中都是使用mybatis-spring-boot-starter导入到pom文件中,我们查看这个包的依赖,其实就是导入mybatis,jdbc,还有mybatis-spring-boot-autoconfigure,看到autoconfigure,我们知道肯定就是这个包完成自动配置。
我们查看这个包的资源文件,果然发现了spring.factories文件。里面就几行。

1
2
3
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

这里默认配置类我就不贴了,就是通过条件注解将默认配置添加到spring中。

条件注解

上文提到了条件注解,它作用在类上就是满足一定条件时类才会进行装配,其中条件注解有:

  • ConditionalOnBean 容器中存在某个bean时生效
  • ConditionalOnClass classpath中存在某类时生效
  • ConditionalOnCloudPlatform 当指定的云平台处于活动状态时生效
  • ConditionalOnExpression SpEL表达式结果为true时生效
  • ConditionalOnJava 指定的Java版本存在时生效
  • ConditionalOnJndi 指定的JNDI存在时生效
  • ConditionalOnMissingBean 容器中不存在某个bean时生效
  • ConditionalOnMissingClass classpath中不存在某类时生效
  • ConditionalOnNotWebApplication 非Web应用环境下生效
  • ConditionalOnProperty 参数设置或者值一致时生效
  • ConditionalOnResource 指定的文件存在时生效
  • ConditionalOnSingleCandidate 容器中该类型Bean只有一个或@Primary的只有一个时生效
  • ConditionalOnWarDeployment 使用WAR部署时生效
  • ConditionalOnWebApplication Web应用环境下生效

总结

那本文简单从源码入手,讲解了springboot是如何自动配置的,@SpringBootApplication注解中包含了@EnableAutoConfiguration,这个注解帮助我们自动配置。
首先springboot在启动时扫描同一个包类的所有的组件,并检查各个jar包中是否存在META-INF/spring.factories 文件,然后如果第三方jar中存在这个文件,会根据文件中设置的自动装配类进行加载,使之生效。如此便实现了自动配置。
不过话说回来,说是自动配置,其实就是源码和第三方包中帮你把配置的功夫省下来了,节约不少时间,也能防止很多bug的出现。
那本文也留下很多没提到的,比如springboot怎么开始自动配置的,我们知道它如何装配,但是我们却不知道如何开始。这个也是个有趣的点,之后会写一写springboot的生命周期相关的文章。

ps

那本文写到这里就结束了,2020年马上也要过去了,时间是真的快,有空的话也会写写2020年的总结。

CATALOG
  1. 1. 前言
  2. 2. 正文
    1. 2.1. start
    2. 2.2. 深入@EnableAutoConfiguration
      1. 2.2.1. @AutoConfigurationPackage
      2. 2.2.2. @Import({AutoConfigurationImportSelector.class})
    3. 2.3. 第三方jar自动配置
    4. 2.4. 条件注解
  3. 3. 总结
  4. 4. ps