Spring Boot自动装配
背景
最近由于做需求,需要对系统升级,此次升级,我们以spring boot为基础搭建了项目,在使用过程中,体验了spring boot中的诸多特性,其中的自动配置特性极大的简化了程序开发中的工作(不用写一行XML)。但是也因为能够实现自动化配置,在没有深入了解的情况下,在开发过程中会遇到一些问题,不知道是为什么,不知道为什么有些配置莫名其妙的就跑起来了,所以针对这种情况,去学习了一下spring boot是如何做到自动配置的。
1. 从一个异常开始
刚接触springboot的时候踩过这样一个坑:在pom文件中不小心加了db相关的依赖,然后启动就报错了,比如
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
看报错中框出来的部分——数据源没有指定url。可是这时候仅仅是新建的项目,还没有到配置数据库这一步。
按照以前的写法,如果需要引入数据库的config,我们需要:
<!-- - - - -数据源配置 - - - - - -->
<bean id="dataSource" class="com.xxxx.ComboPooledDataSource"
destroy-method="close">
<property name="driverClass" value="${driverClass}"/>
<property name="jdbcUrl" value="${jdbcUrl}"/>
<property name="user" value="${user}"/>
<property name="password" value="${password}"/>
</bean>
在spring配置xml中配置一个datasource的bean,注册进ioc容器。可是我现在什么都还没弄呢,为啥就给我报这个错~然后谷歌了一下,发现让我这样:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
再启动,这次启动成功了,但是到底是什么原因导致我仅仅添加了一个依赖(至于怎么定位到时因为加了依赖的原因,是因为全局搜索只有依赖里存在jdbc相关的关键词)就引发了这个错误,查资料之后,最终定位到@SpringBootApplication这个注解上。
2. 源码分析自动配置是如何实现
2.1 基于Java代码对Spring进行配置
在以往使用spring 都是使用XML搭配注解的方式对spring容器进行配置,例如在XML文件中使用配置指定spring需要扫描package的根路径。
<context:component-scan base-package="**"/>
在使用SpringBoot的时候,我们只需要如下方式即可直接启动一个Web程序:
@SpringBootApplication
public class PromiseApi extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(PromiseApi.class, args);
}
}
2.2 @SpringBootApplication注解
@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 {
}
2.3 SpringBootApplication 注解详情
@SpringBootConfiguration:我们点进去以后可以发现底层是 Configuration 注解,说白了就是支持 JavaConfig 的方式来进行配置 (使用 Configuration 配置类等同于 XML 文件)。
@EnableAutoConfiguration:开启自动配置功能,后面详细介绍,自动装配的核心
@ComponentScan:这个注解,应该不会陌生,就是扫描注解,默认是扫描当前类下的 package。将@Controller/@Service/@Component/@Repository等注解加载到 IOC 容器中。
2.3.1 EnableAutoConfiguration
简单来说,这个注解可以帮助我们自动载入应用程序所需要的所有默认配置。我们点进去看一下,发现有两个比较重要的注解。
EnableAutoConfiguration 注解详情:
@AutoConfigurationPackage 自动配置包
@Import(AutoConfigurationImportSelector.class) 给 IOC 容器导入组件
2.3.2 AutoConfigurationPackage
这个注解可以解释成自动配置包,我们也看看@AutoConfigurationPackage里边有什么。也是用@import导入的:
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}
@Import 注解是什么意思呢?它对应 XML 形式下的 <import resource/>,就是导入资源,把多个分布在不同容器下的配置合并在一个配置中。@Import 注解可以配置三种不同的 class :
1. 普通 Bean 或者带有 @Configuration 的配置文件
2. 实现 ImportSelector 接口进行动态注入
3. 实现 ImportBeanDefinitionRegistrar 接口进行动态注入
然后看看Registrar干了什么
核心的就是registerBeanDefinitions:表示的含义就是在默认的情况下就是将:主配置类 (@SpringBootApplication) 的所在包及其子包里边的组件扫描到 Spring 容器中。
2.3.3 AutoConfigurationImportSelector
这里导入的是上面第二种 importSelector,这是一种动态注入 Bean 的技术,我们把 AutoConfigurationImportSelector 点进去,发现它实现了 ImportSelector 接口。
找到实现方法 selectImports ,该方法的作用就是找到相应的 Bean 注入到容器中。
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
再从 getAutoConfigurationEntry 方法点进去,这里面做了许多事情,就是把找到的 Bean 进行排除、过滤、去重,我们可以看到 removeDuplicates、remove、filter 等方法。
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);
}
看到方法名getCandidateConfigurations,获取候选配置,这一步就是springboot获取所有用@Configuration注解修饰的配置类的名称,那么为什么叫做“候选”配置呢?往下看,根据方法名,我们就能知道方法做了什么,接下来就是从这里获取的候选配置的list里,剔除重复部分,再剔除一开始我们@SpringbootApplication 注解里exclude掉的配置,最终才得到配置类名集合。
继续往下看,接下来就是造成咱们一开始的异常的根本原因了:
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
if (result != null) {
return result;
} else {
try {
// 类加载器对象存在则用这个加载器获取上面说的常量路径里的资源,不存在则用系统类加载器去获取 //当前classloader是appclassloader,getResources能获取所有依赖jar里面的META-INF/spring.factories的完整路径
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
LinkedMultiValueMap result = new LinkedMultiValueMap();
...
//开始遍历上面返回的url,并且解析
while(urls.hasMoreElements()) {
}
}
简单梳理:
- Spring 启动的时候会扫描所有 jar 路径下的META-INF/spring.factories,将其文件包装成 Properties 对象
- 从 Properties 对象获取到 key 值为EnableAutoConfiguration的数据,然后添加到容器里边。
- springboot自身的autoconfigure包里有大量的java配置类,我们也可以在自己的工程中写这些配置类,这些配置类需要在相应的META-INF/spring.facotries文件中配置好,如下
这样就会因为在@EnableAutoConfiguration注解的存在,这些配置类里面的bean被注册进ioc容器,不过也是有条件的,条件注解ConditionOnxxx。下面列一些常用的Condition注解:
@ConditionalOnBean(仅仅在当前上下文中存在某个对象时,才会实例化一个Bean)
@ConditionalOnExpression(当表达式为true的时候,才会实例化一个Bean)
@ConditionalOnMissingBean(仅仅在当前上下文中不存在某个对象时,才会实例化一个Bean)
@ConditionalOnMissingClass(某个class类路径上不存在的时候,才会实例化一个Bean)
@ConditionalOnNotWebApplication(不是web应用)
@ConditionalOnClass 当注解于类上,某个class位于类路径上,否则不解析该注解修饰的配置类
3. 异常解决
结合上面的内容,我们猜想 spring-boot-starter-jdbc的jar里面,有个META-INF目录,目录里有spring.factories文件,文件里配置了springboot的自动配置类的名字,所以加了这个依赖,springboot就会自动加载spring.factories里配置的自动配置类。
从jar包里发现,并没有spring.factories。懵逼脸~~
前面有提到,遇到这个异常的时候,发现加上下面这行配置就对了;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
DataSourceAutoConfiguration 点进去看发现线面这个条件注解,当存在DataSorce.class和EmbeddedDatabaseType.class被classloader加载,则这个配置类生效。
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
验证:加入依赖之后,触发了加载这个类的条件,jdbc的jar包里面不存在
spring.factories文件,那么这个类应该在springboot 自带的spring.factories中,经搜索发现:
验证:触发条件是在引入
spring-boot-starter-jdbc之后,因为DataSource.class是jdk自带, EmbeddedDatabaseType.class必然存在于此jar包中,最终在spring-boot-starter-jdbcjar包中,找到了EmbeddedDatabaseType.class。
再来看看,springboot是怎么读取配置文件的呢?url、userName、password之类的参数。
3.1 DataSourceAutoConfiguration
看看DataSourceAutoConfiguration是怎么读取我们的配置文件的:

@EnableConfigurationProperties(DataSourceProperties.class) //读取yml配置文件中的属性到DataSourceProperties中
3.1.1 DataSourceProperties
通过读取yml文件中以 spring.datasource 开头的配置:
@ConfigurationProperties(prefix = "spring.datasource")
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
private ClassLoader classLoader;
/**
* Name of the datasource. Default to "testdb" when using an embedded database.
*/
private String name;
/**
* Fully qualified name of the JDBC driver. Auto-detected based on the URL by default.
*/
private String driverClassName;
/**
* JDBC URL of the database.
*/
private String url;
/**
* Login username of the database.
*/
private String username;
/**
* Login password of the database.
*/
private String password;
从上面可以看到,yml文件里面的配置刚好和
DataSourceProperties 对象的参数对应,到这里,基本验证成功。
tips:yml配置文件的读取:
driverClassName: 在yml文件中可以识别 driver-class-name 、 driver_class_name、driverClassName 三种方式。
总结
- 在引入一个新的依赖时,得先了解引入starter需要注意的东西,比如说他里面的触发条件等;上面提到的jdbc只是因为引入依赖之后存在某个class就触发了jdbc配置类的加载,需要注意的是如果是其他的starter可能触发条件不一样。
- 加有@SpringBootApplication注解的启动类最好是放在项目包最外层(比如com包下),用来定义容器扫描的范围,用来加载classpath环境中一些bean,如果需要自定义,可以自行添加@ComponentScan并制定需要扫描的包路径。
- springboot自身的autoconfigure包里有大量的java配置类,我们也可以在自己的工程中写这些配置类,这些配置类需要在相应的META-INF/spring.facotries文件中配置好。