Skip to content

SpringBoot 源码解析二 #16

Description

@Jimmy2Angel

前言

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

自动化配置报错

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

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!网上搜索以下,在启动类上添加一个注解:

@EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class)

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

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

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

自动化配置解析

@EnableAutoConfiguration

上面说到 @EnableAutoConfiguration 注解意思开启自动配置。源码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented@Inherited@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
StringENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}

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

publicinterfaceImportSelector {
/** * 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(AnnotationMetadataimportingClassMetadata);
}

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

@OverridepublicString[] selectImports(AnnotationMetadataannotationMetadata) {
if (!isEnabled(annotationMetadata)) {
returnNO_IMPORTS;
}
AutoConfigurationMetadataautoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AnnotationAttributesattributes = 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);
returnStringUtils.toStringArray(configurations);
}

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

protectedList<String> getCandidateConfigurations(AnnotationMetadatametadata,
AnnotationAttributesattributes) {
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.");
returnconfigurations;
}

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

DataSourceAutoConfiguration

这个类上有几个注解:

@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 的一个内部类:

@Configuration@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class,
DataSourceJmxConfiguration.class })
protectedstaticclassPooledDataSourceConfiguration {
}

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

protectedstatic <T> TcreateDataSource(DataSourcePropertiesproperties,
Class<? extendsDataSource> 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)
staticclassHikari {
@Bean@ConfigurationProperties(prefix = "spring.datasource.hikari")
publicHikariDataSourcedataSource(DataSourcePropertiesproperties) {
// 创建 HikariDataSourceHikariDataSourcedataSource = createDataSource(properties,
HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
returndataSource;
}
}

DataSourceProperties 中相关代码如下:

publicDataSourceBuilder<?> initializeDataSourceBuilder() {
returnDataSourceBuilder.create(getClassLoader()).type(getType())
.driverClassName(determineDriverClassName()).url(determineUrl())
.username(determineUsername()).password(determinePassword());
}
publicStringdetermineDriverClassName() {
if (StringUtils.hasText(this.driverClassName)) {
Assert.state(driverClassIsLoadable(),
() -> "Cannot load driver class: " + this.driverClassName);
returnthis.driverClassName;
}
StringdriverClassName = 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)) {
thrownewDataSourceBeanCreationException(
"Failed to determine a suitable driver class", this,
this.embeddedDatabaseConnection);
}
returndriverClassName;
}

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

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

自定义 starter

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

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

创建一个 starter 项目

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

@Configuration@ConditionalOnClass(MyRunnable.class)
publicclassMyRunnableAutoConfiguration {
@Value("${myRunnableName:#{null}}")
StringmyRunnableName;
@BeanpublicMyRunnablemyRunnable() {
returnnewMyRunnable(myRunnableName);
}
}
publicclassMyRunnableimplementsRunnable {
privatestaticfinalStringDEFAULT_NAME = "myRunnable";
privateStringname;
publicMyRunnable(Stringname) {
this.name = (name == null ? DEFAULT_NAME : name);
}
@Overridepublicvoidrun() {
System.out.println(this.toString());
}
@OverridepublicStringtoString() {
return"MyRunnable{" +
"name='" + name + '\'' +
'}';
}
}
  • 这里 @ConditionalOnClass 条件很明显是满足的。

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

  • 最后创建一个 MyRunnable 对象。

引入自定义 starter 并测试 MyRunnable

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

<!-- 自定义 starter -->
<dependency>
<groupId>com.lollipop</groupId>
<artifactId>my-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

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

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

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

ConfigurableApplicationContextcontext = application.run(args);
//获取 my-starter 项目中定义的bean runnableRunnablerunnable = (Runnable) context.getBean("myRunnable");
runnable.run();

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

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

myRunnableName: myRunnable12345

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

总结

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions