spring-boot starter原理

在上一篇自定义spring-boot starter步骤中从实践角度写了spring-boot的starter demo,今天,从原理的角度来解析下starter的原理。

1.父pom文件

1
2
3
4
5
6
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
1
2
3
4
5
6
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath>../../spring-boot-dependencies</relativePath>
</parent>

这里面定义了几乎所有常用的依赖的版本和依赖管理声明、插件管理声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<properties>
<activemq.version>5.15.9</activemq.version>
<antlr2.version>2.7.7</antlr2.version>
<appengine-sdk.version>1.9.76</appengine-sdk.version>
<artemis.version>2.6.4</artemis.version>
<aspectj.version>1.9.4</aspectj.version>
<assertj.version>3.11.1</assertj.version>
<atomikos.version>4.0.6</atomikos.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.9.16</byte-buddy.version>
<caffeine.version>2.6.2</caffeine.version>
<cassandra-driver.version>3.6.0</cassandra-driver.version>
<classmate.version>1.4.0</classmate.version>
<commons-codec.version>1.11</commons-codec.version>
<commons-dbcp2.version>2.5.0</commons-dbcp2.version>
<commons-lang3.version>3.8.1</commons-lang3.version>
<commons-pool.version>1.6</commons-pool.version>
<commons-pool2.version>2.6.2</commons-pool2.version>
<couchbase-cache-client.version>2.1.0</couchbase-cache-client.version>
<couchbase-client.version>2.7.7</couchbase-client.version>
<derby.version>10.14.2.0</derby.version>
<dom4j.version>1.6.1</dom4j.version>
...

你肯定会好奇,这么多依赖为什么并没有全部引入,导致我们的jar包异常庞大?这就是maven的依赖管理知识(dependencyManagement)了,只有子类声明了相应的依赖才会导入,而且不需要声明版本,因为spring-boot已经做了。

2.starter的本质

当我们引入以下依赖时,我们到底在干什么?

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

根据官方文档:https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot-starter,starter由2部门构成:

spring-boot-starter + 功能场景,因此又叫功能场景启动器

因此,starter的本质就是一个功能的依赖提取,比如spring-boot-starter-web:引入了tomcat、spring的webmvc等依赖

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.1.7.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.1.7.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.1.7.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.17.Final</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.1.9.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.9.RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>

接下来你肯定要问这些依赖是如何被加载到程序中的,比如,我们没有显示声明tomcat依赖,但运行时会存在,这是如何加载的,下面就会说明。

3.@SpringBootApplication注解

该注解的代码如下:

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

我们分析每一个注解。

@SpringBootConfiguration

这是一个spring-boot项目的注解类,查看其定义:

1
2
3
4
5
6
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}

事实上只是@Configuration的另一种声明,只是一个是spring的注解,一个是spring-boot的,那意义就很明确了:

声明在一个类上,标识这是一个配置类,也就是以前的一个spring的配置文件,而本质上,其也是一个component

1
2
3
4
5
6
7
8
9
10
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
@AliasFor(
annotation = Component.class
)
String value() default "";
}

@EnableAutoConfiguration

从意思上就知道这是一个允许自动配置的注解,啥是自动配置呢?

显然,我们以前使用spring会写一个个配置文件,而现在都不用写了,并不是不存在配置了,而是spring-boot帮我们做了,这就是自动配置。

而这个注解如何做到的呢,看源码:

1
2
3
4
5
6
7
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration{}

这里又有2个注解,不要急,慢慢来。

@AutoConfigurationPackage

显然,这是一个允许自动配置包的注解。

1
2
3
4
5
6
7
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}

到这里已经发现,@Import是一个spring底层注解,作用是给容器中导入一个组件,这里导入的组件由AutoConfigurationPackages.Registrar类指定,那我们就继续看。

1
2
3
4
5
6
7
8
9
10
11
12
13
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));
}

}

我们在这打个断点,如下:

可以看到这个注解是在SpringBootPkgWarApplication类上注入的,运行的表达式结果为com.jimo包,也就是说,spring-boot会将所在主类(@SpringBootApplication标注的类)所在包下所有的组件扫描到容器中,也就是说:你在com.jimo外的包下声明的组件是不会被注入的,不信试试。

@Import(AutoConfigurationImportSelector.class)

再看另一个注解:这个也是直接导入,不过是自动配置导入的选择器。

这个自动导入选择器的类定义如下:

1
2
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered{}

里面有一个方法:作用是返回一个全名称限制的类迭代器,这些类都是组件类

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,
() -> String.format("Only %s implementations are supported, got %s",
AutoConfigurationImportSelector.class.getSimpleName(),
deferredImportSelector.getClass().getName()));
AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
.getAutoConfigurationEntry(getAutoConfigurationMetadata(), annotationMetadata);
this.autoConfigurationEntries.add(autoConfigurationEntry);
for (String importClassName : autoConfigurationEntry.getConfigurations()) {
this.entries.putIfAbsent(importClassName, annotationMetadata);
}
}

我们进入getAutoConfigurationEntry这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}

打个断点可以看到导入的一些配置类:总共有118个

那么这些配置类从哪来的呢? 进入getCandidateConfigurations方法:

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

最终可以跟踪到: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
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}

try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}

这里有个常量:public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

到了这里,也就明白这个配置文件的作用了:

另外,特别注意loadFactoryNames这个方法:

1
2
3
4
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

如果打个断点在这里,可以看看factoryClassName变量的变化,都是spring.factories里声明的各个组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Initializers
org.springframework.context.ApplicationContextInitializer=\

# Application Listeners
org.springframework.context.ApplicationListener=\

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
...