目录
前言
一、依赖管理
1.1 spring-boot-starter-parent
1.2 spring-boot-starter-web
二、自动配置
2.1 引入@EnableAutoConfiguration
2.2 准备了解@EnableAutoConfiguration
2.2.1 导入普通类
2.2.2 导入配置类
2.2.3 导入ImportSelector 的实现类
2.2.4 导入ImportBeanDefinitionRegistrar
2.3 @EnableAutoConfiguration
2.3.1 @AutoConfigurationPackage
2.3.2 @Import(AutoConfigurationImportSelector.class)
前言
关于springboot,我们使用它的主要原因是配置极其简单,其中配置我们过去主要工作一是配置依赖,有时候甚至还要解决依赖冲突;二是需要进行大量的文件配置。springboot正是凭借着依赖管理和自动配置来实现这些功能,大大简化开发成本,当然springboot还有其他很多强大功能。今天我们就可以研究一下这两个功能是怎么实现的。
一、依赖管理
在Spring Boot入门程序中,项目pom.xml文件有两个核心依赖,分别是spring-boot-starter-
parent和spring-boot-starter-web。你会发现有了这两个导入就可以跑起来一个web服务,依赖一些常用第三方框架在导入dependency时不需要指定版本。实际上spring-boot-starter-parent父依赖启动器的主要作用是进行版本统一管理,spring-boot-starter-web依赖启动器的主要作用是基于依赖传递打包了Web开发场景所需的底层所有依赖(springboot的引入第三方框架starter都是相同原理,这里以spring-boot-starter-web举例剖析)。
1.1 spring-boot-starter-parent
项目中的pom.xml文件中找到spring-boot-starter-parent依赖,示例代码如下:
<!-- Spring Boot父项目依赖管理 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
上述代码中,将spring-boot-starter-parent依赖作为Spring Boot项目的统一父项目依赖管理,使用“Ctrl+鼠标左键”进入并查看spring-boot-starter-parent底层源文件。
首先看 spring-boot-starter-parent 的 properties 节点:
<properties>
<main.basedir>${basedir}/../../..</main.basedir>
<java.version>1.8</java.version>
<resource.delimiter>@</resource.delimiter>
<!-- delimiter that doesn'tclash with Spring ${} placeholders -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
在这里 spring-boot-starter-parent 定义了:
- 工程的Java版本为 1.8
- 工程代码的编译源文件编码格式为 UTF-8
- 工程编译后的文件编码格式为 UTF-8
- Maven打包编译的版本
最后来看spring-boot-starter-parent的父依赖 spring-boot-dependencies,spring-boot dependencies的properties节点,这个才是SpringBoot项目的真正管理依赖的项目,里面定义了SpringBoot相关的版本。
<properties>
<activemq.version>5.15.13</activemq.version>
<antlr2.version>2.7.7</antlr2.version>
<appengine-sdk.version>1.9.81</appengine-sdk.version>
<artemis.version>2.10.1</artemis.version>
<aspectj.version>1.9.6</aspectj.version>
<assertj.version>3.13.2</assertj.version>
<atomikos.version>4.0.6</atomikos.version>
<awaitility.version>4.0.3</awaitility.version>
<bitronix.version>2.1.4</bitronix.version>
<build-helper-maven-plugin.version>3.0.0</build-helper-maven-plugin.version>
<byte-buddy.version>1.10.13</byte-buddy.version>
<caffeine.version>2.8.5</caffeine.version>
<cassandra-driver.version>3.7.2</cassandra-driver.version>
<classmate.version>1.5.1</classmate.version>
<commons-codec.version>1.13</commons-codec.version>
<commons-dbcp2.version>2.7.0</commons-dbcp2.version>
<commons-lang3.version>3.9</commons-lang3.version>
<commons-pool.version>1.6</commons-pool.version>
。。。。
</properties>
spring-boot-starter-parent 通过继承 spring-boot-dependencies 从而实现了SpringBoot的版本依
赖管理,所以我们的SpringBoot工程继承spring-boot-starter-parent后已经具备版本锁定等配置了,这也就是在 Spring Boot 项目中部分依赖不需要写版本号的原因。
1.2 spring-boot-starter-web
spring-boot-starter-parent父依赖启动器的主要作用是进行版本统一管理,那么项目运行依赖的JAR包是从何而来的?
查看spring-boot-starter-web依赖文件源码,核心代码具体如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.2.9.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.2.9.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.2.9.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.2.9.RELEASE</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<artifactId>tomcat-embed-el</artifactId>
<groupId>org.apache.tomcat.embed</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.8.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.8.RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
从上述代码可以发现,spring-boot-starter-web依赖启动器的主要作用是打包了Web开发场景所需的底层所有依赖(基于依赖传递,当前项目也存在对应的依赖jar包)正是如此,在pom.xml中引入spring-boot-starter-web依赖启动器时,就可以实现Web场景开发,而不需要额外导入Tomcat服务器以及其他Web依赖文件等。
Spring Boot除了提供有上述介绍的Web依赖启动器外,还提供了其他许多开发场景的相关依赖:
Spring Boot官方并不是针对所有场景开发的技术框架都提供了场景启动器,各类第三方技术框架所在的开发团队主动与Spring Boot框架进行了整合,实现了各自的依赖启动器。在starter命名规范上一般可以区分,一般springboot整合的框架名称在后面,如spring-boot-starter-web,第三方命名自己框架名称在前(如druid-spring-boot-starter)。
二、自动配置
2.1 引入@EnableAutoConfiguration
大家都知道启动springboot,只需要在主启动类上标注 @SpringBootApplication
注解。下面我们看下他的源码。
@Target({ElementType.TYPE}) //注解的适用范围,Type表示注解可以描述在类、接口、注解或枚举中
@Retention(RetentionPolicy.RUNTIME) //表示注解的生命周期,Runtime运行时
@Documented //表示注解可以记录在javadoc中
@Inherited //表示可以被子类继承该注解
@SpringBootConfiguration // 标明该类为配置类
@EnableAutoConfiguration // 启动自动配置功能
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes =
TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes =AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
。。。。
}
实际这个注解是一个组合注解,包括3个主要注解。标注它之后就会触发自动配置(@EnableAutoConfiguration
)和组件扫描(@ComponentScan
)。
@ComponentScan
默认扫描当前配置类所在包及子包下的所有组件, exclude
属性会将主启动类、自动配置类屏蔽掉。
@SpringBootConfiguration
并没有对@Configuration
其做实质性扩展,仅仅还是标注一个配置类,被spring当做配置文件使用。
实际做自动配置的是@EnableAutoConfiguration,下面我们重点说一下这个注解。
2.2 准备了解@EnableAutoConfiguration
在了解 @EnableAutoConfiguration
之前,先了解 SpringFramework 的原生手动装配机制,这对后续阅读 @EnableAutoConfiguration
有很大帮助,因为它运用了这块的装配机制。
在原生的 SpringFramework 中,装配组件有三种方式:
- 使用模式注解
@Component
等(Spring2.5+) - 使用配置类
@Configuration
与@Bean
(Spring3.0+) - 使用模块装配
@EnableXXX
与@Import
(Spring3.1+)
@Component
及衍生注解很常见,咱开发中常用的套路,不再赘述,但模式注解只能在自己编写的代码中标注,无法控制是否装配jar包中的组件。为此可以使用 @Configuration
与 @Bean
,手动装配组件,但这种方式不是自动的装配,一旦注册过多,会导致编码成本高,维护不灵活等问题。SpringFramework 提供了模块装配功能,通过给配置类标注 @EnableXXX
注解,再在注解上标注 @Import
注解,即可完成组件自动装配的效果,如果第三方框架使用的是这种方式加载一些类,需要时加上注解就可以,不需要不加注解,就不会装配进容器。
@Import
可以传入四种类型:
- 普通类
- 配置类
-
ImportSelector
的实现类 -
ImportBeanDefinitionRegistrar
的实现类
2.2.1 导入普通类
@Import({Red.class})
public @interface EnableColor {
}
@EnableColor
@Configuration
public class ColorConfiguration {
}
public class App {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ColorConfiguration.class);
String[] beanDefinitionNames = ctx.getBeanDefinitionNames();
Stream.of(beanDefinitionNames).forEach(System.out::println);
}
}
2.2.2 导入配置类
@Configuration
public class ColorRegistrarConfiguration {
@Bean
public Yellow yellow() {
return new Yellow();
}
}
@Import({Red.class,ColorRegistrarConfiguration.class})
public @interface EnableColor {
}
ColorConfiguration类和APP同2.2.1
这样Red、ColorRegistrarConfiguration 和 Yellow 都已注册到IOC容器中。
2.2.3 导入ImportSelector
的实现类
public class ColorImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[] {Blue.class.getName(), Green.class.getName()};
}
}
@Import({Red.class, ColorRegistrarConfiguration.class, ColorImportSelector.class})
public @interface EnableColor {
}
ColorImportSelector 没有注册到IOC容器中,两个新的颜色类被注册。
2.2.4 导入ImportBeanDefinitionRegistrar
public class ColorImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
registry.registerBeanDefinition("black", new RootBeanDefinition(Black.class));
}
}
@Import({Red.class, ColorRegistrarConfiguration.class, ColorImportSelector.class, ColorImportBeanDefinitionRegistrar.class})
public @interface EnableColor {
}
ColorImportBeanDefinitionRegistrar 没有注册到IOC容器中,black类被注册。
以上就是 SpringFramework 的手动装配方法。那 SpringBoot 又是如何做自动装配的呢?
2.3 @EnableAutoConfiguration
我们先来看下@EnableAutoConfiguration的源码:
// 自动配置包
@AutoConfigurationPackage
// Spring的底层注解@Import,给容器中导入一个组件;
// 导入的组件是AutoConfigurationPackages.Registrar.class
@Import(AutoConfigurationImportSelector.class)
// 告诉SpringBoot开启自动配置功能,这样自动配置才能生效。
public @interface EnableAutoConfiguration {
。。。。
}
这里我们看到@EnableAutoConfiguration也是一个组合注解。
2.3.1 @AutoConfigurationPackage
@Import(AutoConfigurationPackages.Registrar.class) // 导入Registrar中注册的组件
public @interface AutoConfigurationPackage {
}
咱从一开始学 SpringBoot 就知道一件事:主启动类必须放在所有自定义组件的包的最外层,以保证Spring能扫描到它们。由此可知是它起的作用。它的实现原理是在注解上标注了 @Import
,导入了一个 AutoConfigurationPackages.Registrar
。
/**
* {@link ImportBeanDefinitionRegistrar} to store the base package from the importing
* configuration.用于保存导入的配置类所在的根包。
*/
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImport(metadata).getPackageName());
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImport(metadata));
}
}
很明显,它就是实现把主配置所在根包保存起来以便后期扫描用的。分析源码:
Registrar
实现了 ImportBeanDefinitionRegistrar
接口,它向IOC容器中要手动注册组件。
在重写的 registerBeanDefinitions
方法中,它要调用外部类 AutoConfigurationPackages
的register方法,这里传入的参数就是主启动类的所在包。再看register方法:
private static final String BEAN = AutoConfigurationPackages.class.getName();
public static void register(BeanDefinitionRegistry registry, String... packageNames) {
// 判断 BeanFactory 中是否包含 AutoConfigurationPackages
if (registry.containsBeanDefinition(BEAN)) {
BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues();
// addBasePackages:添加根包扫描包
constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames));
}
else {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(BasePackages.class);
beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(BEAN, beanDefinition);
}
}
AutoConfigurationPackages.Registrar这个类就干一个事,注册一个 Bean ,这个 Bean 就是org.springframework.boot.autoconfigure.AutoConfigurationPackages.BasePackages ,它有一个参数,这个参数是使用了 @AutoConfigurationPackage 这个注解的类所在的包路径,保存自动配置
类以供之后的使用。
2.3.2 @Import(AutoConfigurationImportSelector.class)
这里@Import导入的就是一个ImportSelector类,我们来看这个AutoConfigurationImportSelector。
它的核心部分,就是实现的 ImportSelector
的 selectImport
方法,然后里面重要的方法getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata)
。
/**
* Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata}
* of the importing {@link Configuration @Configuration} class.
*
* 根据导入的@Configuration类的AnnotationMetadata返回AutoConfigurationImportSelector.AutoConfigurationEntry。
*/
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);
}
这个方法里有一个非常关键的集合:configurations,这个 集合的数据,都是通过 getCandidateConfigurations
方法来获取:
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
// SPI机制加载自动配置类
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;
}
我们继续跟踪这个loadFactoryNames方法:
源码中使用 classLoader 去加载了指定常量路径下的资源: FACTORIES_RESOURCE_LOCATION
,而这个常量指定的路径实际是:META-INF/spring.factories ,这个文件内容:
# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer
# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
# 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,\
......
之后拿到这个资源文件,以 Properties 的形式加载,并取出 org.springframework.boot.autoconfigure.EnableAutoConfiguration
指定的所有自动配置类(是一个很大的字符串,里面都是自动配置类的全限定类名),装配到IOC容器中,之后自动配置类就会通过 ImportSelector
和 @Import
的机制被创建出来,之后就生效了。
这也就解释了为什么 即便没有任何配置文件,SpringBoot的Web应用都能正常运行,实际就是采取了SPI的机制。