写在前面

最近在重构一个日志模块,需要将其从项目中抽离出来,于是考虑将其做成一个starter便于后续使用。本篇就来聊聊SpringBoot的自动装配原理,只有了解原理才知道如何将自己的项目制作为starter。

管中窥豹

当你新建一个SpringBoot项目的时候,项目入口类中自动会添加@SpringBootApplication,那么问题来了,这个@SpringBootApplication注解的作用是什么呢,查看一下该注解的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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 {
}

可以看到这里注解上面除了几个常用的注解外,还有三个特别的注解:@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan

@SpringBootConfiguration

首先看一下@SpringBootConfiguration注解的源码:

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

可以看到这个注解上面除了元注解外,只有一个@Configuration注解,也就是说这个注解其实就相当于@Configuration注解,即标识此类为配置类,同时不仅可以注册一些额外的Bean对象,还可以导入一些额外的配置。

既然看到这里,那就再看一下@Configuration注解的源码:

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 "";
}

可以看到@Configuration注解其核心就是@Component注解,这说明Spring配置类其实也是Spring容器管理的对象。

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

可以看到这个注解用于开启自动配置功能,它其实也是一个组合注解,除了元注解外还有@AutoConfigurationPackage@Import({AutoConfigurationImportSelector.class})注解。

@AutoConfigurationPackage

首先分析@AutoConfigurationPackage注解,它表示让包中的类及子包中的类能够被自动扫描到Spring容器中。查看一下该注解的源码:

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

可以看到这个注解中除了元注解外,还是用@Import注解导入了Registrar这个字节码类,看一下这个Registrar对象的信息:

1
2
3
4
5
6
7
8
9
10
11
12
 static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
Registrar() {
}

public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(registry, (new AutoConfigurationPackages.PackageImport(metadata)).getPackageName());
}

public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new AutoConfigurationPackages.PackageImport(metadata));
}
}

可以发现它其实是AutoConfigurationPackages这个抽象类中的静态内部类,里面有两个方法:registerBeanDefinitionsdetermineImportsregisterBeanDefinitions方法用于获取扫描的包路径并将其注册到能被Spring容器扫描的包中,通过Debug可以看到这个(new AutoConfigurationPackages.PackageImport(metadata)).getPackageName()对象其实就是项目入口类所在的包:

那么问题来了,传入的metadata是什么呢?可以发现它就是被@SpringBootApplication注解所修饰的类,即项目的入口类:

也就是说将被@SpringBootApplication注解所修饰的类(项目入口类)所在的包及子包下的所有类都扫描加载到Spring容器中。这也从另一个侧面反映了项目结构,项目入口类必须放在项目的最外层目录中。

@Import({AutoConfigurationImportSelector.class})

再来分析@Import({AutoConfigurationImportSelector.class})注解,我们知道@Import注解的作用就是导入一些其他的类到Spring容器中,这里导入了AutoConfigurationImportSelector这个字节码对象,它是一个选择器。首先看一下这个对象的继承关系:

可以看到AutoConfigurationImportSelector实现了DeferredImportSelector接口,而DeferredImportSelector接口又是实现了ImportSelector接口,查看一下这个ImportSelector接口的源码:

1
2
3
4
5
6
7
8
public interface ImportSelector {
String[] selectImports(AnnotationMetadata var1);

@Nullable
default Predicate<String> getExclusionFilter() {
return null;
}
}

可以看到它只有两个方法,其中selectImports用于将所有需要导入的类以全路径方式返回,形成一个字符串数组,之后这些类就会被添加到Spring容器中。可以查看该方法的具体实现逻辑:

1
2
3
4
5
6
7
8
9
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}

首先调用AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader) 方法来将对象的加载器导入其中:

1
2
3
static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {
return loadMetadata(classLoader, "META-INF/spring-autoconfigure-metadata.properties");
}

可以看到它实际上调用的是loadMetadata方法,传入的路径是META-INF/spring-autoconfigure-metadata.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader, String path) {
try {
Enumeration<URL> urls = classLoader != null ? classLoader.getResources(path) : ClassLoader.getSystemResources(path);
Properties properties = new Properties();

while(urls.hasMoreElements()) {
properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource((URL)urls.nextElement())));
}

return loadMetadata(properties);
} catch (IOException var4) {
throw new IllegalArgumentException("Unable to load @ConditionalOnClass location [" + path + "]", var4);
}
}

通过分析可以看到它首先判断classLoader是否为空,如果不为空则通过调用classLoader.getResources(path)方法从path路径中获取信息,而这个path就是之前传入的META-INF/spring-autoconfigure-metadata.properties

通过定位到spring-autoconfigure-metadata.properties文件,可以看到其实就是加载这个文件中内容到Spring容器,而这些都是对应组件的自动配置类。有了这些自动配置类,开发者就不再需要手动编写配置类并注入Spring容器中了。

接着回到selectImports方法,然后执行的是如下代码:

1
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);

可以看到它调用的是AutoConfigurationImportSelector#getAutoConfigurationEntry()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.filter(configurations, autoConfigurationMetadata);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}

获取自动配置的入口,其实就是获取到spring-autoconfigure-metadata.properties文件中被加载到Spring容器的自动配置类。该方法内部调用了getCandidateConfigurations()方法来获取系统中已经加载好的类:

1
2
3
4
5
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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()方法:

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
39
40
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

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 {
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
LinkedMultiValueMap result = new LinkedMultiValueMap();

while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();

while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
int var10 = var9.length;

for(int var11 = 0; var11 < var10; ++var11) {
String factoryImplementationName = var9[var11];
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}

cache.put(classLoader, result);
return result;
} catch (IOException var13) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
}
}
}

可以看到它会从META-INF/spring.factories中获取资源,然后通过PropertiesLoaderUtils.loadProperties()方法来加载资源:

也就是说SpringBoot会在启动的时候,从类路径下的META-INF/spring.factories文件中获取EnableAutoConfiguration设置的值(而这些设置的值都是以AutoConfiguration结尾,即自动配置类)并导入到Spring容器中,自动配置类会帮助我们进行自动配置:

@ComponentScan

接着我们再来看一下@ComponentScan注解:

1
2
3
4
5
6
7
8
9
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)

可以看到这个注解用于扫描包,然后将其放入Spring容器中。

小结@SpringBootApplication

接下来我们总结一下@SpringBootApplication注解,该注解用于扫描项目入口类所在的包或者子包内的类,并将其导入Spring容器中,同时加载META-INF/spring.factories文件中的自动配置类到Spring容器中。

run方法

接下来我们再来看项目入口类的main方法,里面其实调用的是run方法:

1
2
3
4
5
6
7
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class[]{primarySource}, args);
}

再来看一下实际上它所调用的run方法的源码:

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
39
40
41
42
43
44
45
46
47
public ConfigurableApplicationContext run(String... args) {
//定义一个计时器
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
this.configureHeadlessProperty();
//获取监听器
SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting();

Collection exceptionReporters;
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
this.configureIgnoreBeanInfo(environment);
Banner printedBanner = this.printBanner(environment);
//创建应用上下文
context = this.createApplicationContext();
exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
//预刷新上下文
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
//刷新上下文
this.refreshContext(context);
//刷新之后的上下文
this.afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
}

listeners.started(context);
this.callRunners(context, applicationArguments);
} catch (Throwable var10) {
this.handleRunFailure(context, var10, exceptionReporters, listeners);
throw new IllegalStateException(var10);
}

try {
listeners.running(context);
//返回上下文
return context;
} catch (Throwable var9) {
this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
throw new IllegalStateException(var9);
}
}

我们比较关注的是refreshContext这个用于刷新上下文的方法:

1
2
3
4
5
6
7
8
9
10
private void refreshContext(ConfigurableApplicationContext context) {
this.refresh(context);
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
} catch (AccessControlException var3) {
}
}

}

再来看一下这个refresh方法:

1
2
3
4
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
((AbstractApplicationContext)applicationContext).refresh();
}

查看一下这个refresh方法,其实际源码如下所示:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
   @Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 准备刷新此上下文
prepareRefresh();

// 告诉子类刷新内部的bean工厂
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// 准备此上下文中使用的bean工厂
prepareBeanFactory(beanFactory);

try {
// 允许在此上下文子类中对bean工厂进行后处理
postProcessBeanFactory(beanFactory);

// 调用在上下文中注册为bean的工厂处理器
invokeBeanFactoryPostProcessors(beanFactory);

// 注册拦截bean创建的bean处理器
registerBeanPostProcessors(beanFactory);

// 为此上下文初始化消息源
initMessageSource();

// 为此上下文初始化事件多播器。
initApplicationEventMulticaster();

// 初始化特定上下文子类中的其他特殊bean。
onRefresh();

// 检查监听器bean并注册它们
registerListeners();

// 实例化所有剩余的(非惰性初始化)单例
finishBeanFactoryInitialization(beanFactory);

// 最后一步:发布相应的事件
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// 销毁已经创建的单例以避免占用资源
destroyBeans();

// 重置“活动”标志
cancelRefresh(ex);

// 将异常传播给调用者
throw ex;
}

finally {
// 重置 Spring 核心中的常见自省缓存,因为我们可能不再需要单例 bean 的元数据......
resetCommonCaches();
}
}
}

可以看到这其实就是一个Spring加载Bean的过程。里面有一个用于初始化特定上下文子类中的其他特殊bean的onRefresh方法:

1
2
3
   protected void onRefresh() throws BeansException {
// For subclasses: do nothing by default.
}

这个方法居然没有实现,因此可以看其实现类:

1
2
3
4
5
6
7
8
9
protected void onRefresh() {
super.onRefresh();

try {
this.createWebServer();
} catch (Throwable var2) {
throw new ApplicationContextException("Unable to start reactive web server", var2);
}
}

可以看到里面调用了createWebServer()方法,用于创建Web服务:

1
2
3
4
5
6
7
8
9
10
11
private void createWebServer() {
ReactiveWebServerApplicationContext.ServerManager serverManager = this.serverManager;
if (serverManager == null) {
String webServerFactoryBeanName = this.getWebServerFactoryBeanName();
ReactiveWebServerFactory webServerFactory = this.getWebServerFactory(webServerFactoryBeanName);
boolean lazyInit = this.getBeanFactory().getBeanDefinition(webServerFactoryBeanName).isLazyInit();
this.serverManager = ReactiveWebServerApplicationContext.ServerManager.get(webServerFactory, lazyInit);
}

this.initPropertySources();
}

可以看到这个方法里面调用了getWebServerFactory()方法用于创建Web容器:

1
2
3
protected ReactiveWebServerFactory getWebServerFactory(String factoryBeanName) {
return (ReactiveWebServerFactory)this.getBeanFactory().getBean(factoryBeanName, ReactiveWebServerFactory.class);
}

这个方法返回了一个ReactiveWebServerFactory对象,其实这是一个接口:

1
2
3
public interface ReactiveWebServerFactory {
WebServer getWebServer(HttpHandler httpHandler);
}

之所以设计为接口,那是因为Web容器有很多种,常用的有Tomcat、Netty、Jetty和Undertow:

TomcatReactiveWebServerFactory这一实现类为例,其getWebServer方法的源码如下所示:

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
public WebServer getWebServer(HttpHandler httpHandler) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}

Tomcat tomcat = new Tomcat();
File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
this.customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
this.configureEngine(tomcat.getEngine());
Iterator var5 = this.additionalTomcatConnectors.iterator();

while(var5.hasNext()) {
Connector additionalConnector = (Connector)var5.next();
tomcat.getService().addConnector(additionalConnector);
}

TomcatHttpHandlerAdapter servlet = new TomcatHttpHandlerAdapter(httpHandler);
this.prepareContext(tomcat.getHost(), servlet);
return this.getTomcatWebServer(tomcat);
}

这其实就是创建一个内置的Tomcat容器,然后返回,所以开发者在将项目打包成jar包以后,不需要自己再手动启动Tomcat容器。

小结

本篇通过阅读源码对SpringBoot的自动装配原理有了一个较为深刻的理解,后续就可以在此基础上自定义自己的启动器(starter)。