SpringBoot源码解析二

前言

要说 SpringBoot 有什么优点的话,比较核心的就是简化配置和自动配置了。前面我们分析了 SpringBoot 的启动流程还有一些监听器相关的源码,今天来分析下 SpringBoot 的自动配置具体怎么实现的,以便日后遇到相关错误知道从哪里入手。

自动化配置报错

初学 SpringBoot 的我们在启动自己的第一个 SpringBoot demo 的时候,可能都遇到了以下这个报错:

1
2
3
4
5
6
7
8
9
10
11
12
Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class


Action:

Consider the following:
If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).

日志意思很明显是配置 datasource 的时候出错了,我们都没有配置 datasource 呢怎么就报错了。当时心想什么垃圾 SpringBoot!网上搜索以下,在启动类上添加一个注解:

1
@EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class)

或者去掉 pom 文件中的一个依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

然后启动成功后暗自高心了一把,SpringBoot 也没什么嘛,这么简单!

自动化配置解析

@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 {};
}

关键点在于用 @Import 导入的 AutoConfigurationImportSelector。该类实现了 ImportSelector 接口,这个接口只有一个方法,代码如下:

1
2
3
4
5
6
7
8
9
public interface ImportSelector {

/**
* Select and return the names of which class(es) should be imported based on
* the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
*/
String[] selectImports(AnnotationMetadata importingClassMetadata);

}

注释写的很清楚,这个方法返回的 name 对应的 bean 都是要注入到 Spring 容器里的。然后我们再看实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
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 StringUtils.toStringArray(configurations);
}

其中参数 AnnotationMetadata 封装了注解的相关信息。
这个方法具体在什么调用,我们可以一层层往上看,会发现是在刷新上下文的时候调用。
现在直接看重点 getCandidateConfigurations(annotationMetadata, attributes); 代码如下:

1
2
3
4
5
6
7
8
9
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;
}

代码很熟悉,在前一篇分析启动流程的时候看到过,主要就是从 META-INF/spring.factories 文件中获取。spring-boot-autoconfigure 包下这个文件中
内置了很多要自动配置的类(但是这些类并不会全部配置,例如前面说到的可以通过 @EnableAutoConfiguration(exclude = XXX.class) 去排除),其中就包括一个叫 DataSourceAutoConfiguration 的类。下面以这个类举例。

DataSourceAutoConfiguration

这个类上有几个注解:

1
2
3
4
5
@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
DataSourceInitializationConfiguration.class })

  1. @Configuration 表示这是一个配置类。
  2. @ConditionalOnClass 是一个条件,当存在指定的类时该条件才满足,当前类才配置。
  3. @EnableConfigurationProperties 启用配置属性。
  4. @Import 导入指定的类。

注意这里 @ConditionalOnClass 条件中的 EmbeddedDatabaseType 类在 spring-jdbc 包里,这也是为什么前面说到去掉 pom 文件里 jdbc 的依赖就不会报错了。因为去掉 jdbc 的依赖后,这里的 @ConditionalOnClass 不满足,DataSourceAutoConfiguration 也就不会自动配置了。

接着看为什么 pom 文件里有 jdbc 依赖并且不配置 datasource 相关信息时就会报错呢?

先看 DataSourceAutoConfiguration 的一个内部类:

1
2
3
4
5
6
7
8
9
@Configuration
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class,
DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {

}

这里导入 DataSourceConfiguration 的几个内部类 Hikari、Tomcat、Dbcp2、Generic。但是这里只有 Hikari 上的条件注解是满足的,其相关代码如下:

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
protected static <T> T createDataSource(DataSourceProperties properties,
Class<? extends DataSource> type) {
return (T) properties.initializeDataSourceBuilder().type(type).build();
}

/**
* Hikari DataSource configuration.
*/
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true)
static class Hikari {

@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public HikariDataSource dataSource(DataSourceProperties properties) {
// 创建 HikariDataSource
HikariDataSource dataSource = createDataSource(properties,
HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}

}

DataSourceProperties 中相关代码如下:

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
public DataSourceBuilder<?> initializeDataSourceBuilder() {
return DataSourceBuilder.create(getClassLoader()).type(getType())
.driverClassName(determineDriverClassName()).url(determineUrl())
.username(determineUsername()).password(determinePassword());
}

public String determineDriverClassName() {
if (StringUtils.hasText(this.driverClassName)) {
Assert.state(driverClassIsLoadable(),
() -> "Cannot load driver class: " + this.driverClassName);
return this.driverClassName;
}
String driverClassName = null;
if (StringUtils.hasText(this.url)) {
driverClassName = DatabaseDriver.fromJdbcUrl(this.url).getDriverClassName();
}
if (!StringUtils.hasText(driverClassName)) {
driverClassName = this.embeddedDatabaseConnection.getDriverClassName();
}
// 这里就是前言自动化配置报错的地方,同理配置了驱动但是不配置url也会报错
if (!StringUtils.hasText(driverClassName)) {
throw new DataSourceBeanCreationException(
"Failed to determine a suitable driver class", this,
this.embeddedDatabaseConnection);
}
return driverClassName;
}

当我们在配置文件中配置了相关数据源信息后,这里就会创建一个 HikariDataSource。

最后 DataSourceAutoConfiguration 上还会通过注解注入其他的相关配置类,例如后置处理器什么的。

自定义 starter

springboot 开箱即用的特点是因为它针对现在的开发技术提供了很多现成的starter,我们可以理解为一种服务。

自动化配置功能就是 spring-boot-autoconfigure 这个 starter 为我们提供的。下面我们自定义一个 starter 看看它是怎么实现的。

创建一个 starter 项目

首先我们需要创建一个简单的自动配置类 MyRunnableAutoConfiguration 和一个 MyRunnable 类:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@ConditionalOnClass(MyRunnable.class)
public class MyRunnableAutoConfiguration {

@Value("${myRunnableName:#{null}}")
String myRunnableName;

@Bean
public MyRunnable myRunnable() {
return new MyRunnable(myRunnableName);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyRunnable implements Runnable {

private static final String DEFAULT_NAME = "myRunnable";

private String name;

public MyRunnable(String name) {
this.name = (name == null ? DEFAULT_NAME : name);
}

@Override
public void run() {
System.out.println(this.toString());
}

@Override
public String toString() {
return "MyRunnable{" +
"name='" + name + '\'' +
'}';
}
}
  • 这里 @ConditionalOnClass 条件很明显是满足的。

  • 然后用 @Value 注入我们配置的名称,没有则是 null。

  • 最后创建一个 MyRunnable 对象。

引入自定义 starter 并测试 MyRunnable

我们在原来的项目中添加自定义的 starter 依赖:

1
2
3
4
5
6
<!-- 自定义 starter -->
<dependency>
<groupId>com.lollipop</groupId>
<artifactId>my-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

然后在 resources 目录下创建目录 META-INF, 再在里面创建 spring.factories 文件并配置:

1
2
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.lollipop.starter.MyRunnableAutoConfiguration

最后在启动类中添加测试代码:

1
2
3
4
ConfigurableApplicationContext context = application.run(args);
//获取 my-starter 项目中定义的bean runnable
Runnable runnable = (Runnable) context.getBean("myRunnable");
runnable.run();

启动项目,控制台打印MyRunnable{name='myRunnable'}。证明我们自定义的 starter 中的自动配置类配置 MyRunnable 成功。名称默认为 myRunnable。

我们在 yml 配置文件中添加配置:

1
myRunnableName: myRunnable12345

重新启动,控制台打印MyRunnable{name='myRunnable12345'}。证明我们的配置生效。

总结

至此,我们分析了 spring-boot-autoconfigure 是怎么通过 @EnableAutoConfiguration 注解帮我完成自动配置的。以及实现了一个自定义的 starter 并自动配置我们需要的 bean。