一、总结Springboot自动配置的过程
抓住三点:
1.分析main方法中SpringApplication.run方法,该方法会创建spring容器并刷新容器;
2.分析复合注解@SpringApplication,该注解下面的@EnableAutoConfiguration下的@import注解;
3.SPI加载机制,加载META-INF/spring.fatories配置文件,按需加载;
Springboot启动时,是依靠启动类的main方法来进行启动的,而main方法中执行的是SpringApplication.run() 方法,而 SpringApplication.run() 方法中会创建spring的容器,并且刷新容器。而在刷新容器的时候就会去解析启动类,然后就会去解析启动类上的@SpringBootApplication 注解,而这个注解是个复合注解,这个注解中有一个@EnableAutoConfiguration 注解,这个注解就是开启自动配置,这个注解中又有 @Import注解引入了一个 AutoConfigurationImportSelector 这个类,这个类会进过一些核心方法,然后去扫描我们所有jar包下的 META-INF 下的 spring.factories 文件,而从这个配置文件中取找key为 EnableAutoConfiguration 类的全路径的值下面的所有配置都加载,这些配置里面都是有条件注解的,然后这些条件注解会根据你当前的项目依赖的jar包以及是否配置了符合这些条件注解的配置来进行装载的。
换成大白话,再精炼一下:
SpringBoot在启动的时候会调用run()方法,run()方法会刷新容器,刷新容器的时候,会扫描classpath下面的的包中META-INF/spring.factories文件,在这个文件中记录了好多的自动配置类,在刷新容器的时候会将这些自动配置类加载到容器中,然后在根据这些配置类中的条件注解,来判断是否将这些配置类在容器中进行实例化,这些条件主要是判断项目是否有相关jar包或是否引入了相关的bean。这样springboot就帮助我们完成了自动装配。
二、 源码分析
1. 分析SpringApplication.run方法:
StopWatch stopWatch = new StopWatch();
# 记录spring启动的时间标识
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
# 设置系统属性Headless的值
this.configureHeadlessProperty();
# 创建启动监听器
SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting();
Collection exceptionReporters;
try {
# 配置容器参数,将启动容器时的命令进行封装
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
# 加载外部配置到环境容器变量(包括配置要使用的PropertySource以及profile,并通知监听器容器要的环境变量配置好了)
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
# 跳过BeanInfo类的搜索(通常用于首先没有为应用程序中的Bean定于定义此类的情况)
this.configureIgnoreBeanInfo(environment);
# 判断环境变量中是否有banner的标识,有则打印
Banner printedBanner = this.printBanner(environment);
# 1 创建Application容器
context = this.createApplicationContext();
# 统一处理的异常信息
exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
# 创建容器的一些操作
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
# 刷新Application容器
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);
}
}
复制代码
SpringApplication.run() 方法中
- 第一个就是创建ApplicationContext容器。
- 第二个是刷新ApplicationContext容器。
在创建ApplicationContext时,会根据用户是否明确设置了 ApplicationContextClass 类型以及初始化阶段的推断结果,决定为当前SpringBoot应用创建什么类型的ApplicationContext。
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch(this.webApplicationType) {
case SERVLET:
contextClass = Class.forName("org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext");
break;
case REACTIVE:
contextClass = Class.forName("org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext");
break;
default:
contextClass = Class.forName("org.springframework.context.annotation.AnnotationConfigApplicationContext");
}
} catch (ClassNotFoundException var3) {
throw new IllegalStateException("Unable create a default ApplicationContext, please specify an ApplicationContextClass", var3);
}
}
return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass);
}
复制代码
创建完成ApplicationContext容器后,我们接着回到 SpringApplication.run() 方法中。
下面开始初始化各种插件在异常失败后给出的提示。
然后执行准备刷新上下文的一些操作。其实 prepareContext() 方法也是非常关键的,它起到了一个承上启下的作用。下面我们来看一下 prepareContext() 方法里面具体执行了什么。
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);
this.postProcessApplicationContext(context);
this.applyInitializers(context);
listeners.contextPrepared(context);
if (this.logStartupInfo) {
this.logStartupInfo(context.getParent() == null);
this.logStartupProfileInfo(context);
}
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory)beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
# 关键:这里加载用配置,注意source加载出来就是XXXApplication这个启动类,从这里开始使用注解了
Set<Object> sources = this.getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
# 加载启动类
this.load(context, sources.toArray(new Object[0]));
# 所有监听器开始加载如ApplicationContext容器中
listeners.contextLoaded(context);
}
复制代码
关键的地方主要就是 getAllSoures() 方法,这个方法中,获取到的一个source就是启动类XXXApplication。
这样就通过获取这个启动类就可以在后load()方法中取加载这个启动类到容器中。
然后,后面再通过 listeners.contextLoaded(context) ;
将所有监听器加载到ApplicationContext容器中。
最后就是我们上面说的核心的第二部刷新ApplicationContext容器操作,如果没有这一步操作上面的内容也都白做的,通过 SpringApplication的refreshContext(context) 方法完成最后一道工序将启动类上的注解配置,刷新到当前运行的容器环境中。
2. 启动类上的注解
上面我们说到在SpringApplication的 run() 方法中,通过调用自己的 prepareContext() 方法,在 prepareContext() 方法中又调用 getAllSources() 方法,然后去获取启动类,然后通过SpringApplication的 load() 方法,去加载启动类,然后在刷新容器的时候就会去将启动类在容器中进行实例化。
在刷新ApplicationContext容器时,就开始解析启动类上的注解了。
启动类 DemoApplication 就只有一个注解 @SpringBootApplication ,那么下面来看一下这个注解:
可以看到这个注解是一个复合注解,有三个关键注解需要说明一下。
@SpringBootConfiguration
@SpringBootConfiguration 这个注解说明再点进去查看详情发现就是一个@Configuration 注解,这说明启动类就是一个配置类。支持Spring以JavaConfig的形式启动。
@ComponentScan
这个注解,从字面的意思上也能看出来,就是组件扫描的意思,即默认扫描当前package以及其子包下面的spring的注解,例如: @Controller 、 @Service 、 @Component 等等注解。
@EnableAutoConfiguration
@EnableAutoConfiguration 这个注解也是一个复合注解:
这个注解是比较核心的一个注解,springboot的主要自动配置原理基本上都来自@EnableAutoConfiguration这个注解的配置,那么我们通过看这个注解的源码可以发现有两个注解比较重要的。
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
复制代码
@AutoConfigurationPackage 这个注解字面的意思是 自动配置包 ,那么我们点进去看看里面是什么样的。
还是一个复合注解,但是最终依赖的确实 @Import 这个注解,这个注解后面我们会介绍,现在先明白它就是给Spring容器引入组件的功能的一个注解。
那么我们接着来看看 AutoConfigurationPackages.Registrar.class 这个类里面的代码。
这两张图就是这个 AutoConfigurationPackages.Registrar 这个类的关键部分,说实话,我是没看出来什么东西。但是网上搜到的是这个register()方法的作用是,用来自动注册一些组件中的配置,例如JPA的 @Entity 这个注解,这里就是会开启自动扫描这类注解的功能。
** @Import(AutoConfigurationImportSelector.class)**
我们接着回来看 @EnableAutoConfiguration 下的@Import(AutoConfigurationImportSelector.class) 这个注解的功能。进入到AutoConfigurationImportSelector 这个类里面后源码如下:
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
# getAutoConfigurationEntry 此方法很关键
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
复制代码
然后我们进入 getAutoConfigurationEntry() 方法来看看:
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
# getCandidateConfigurations 很关键
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);
}
}
复制代码
我们继续进入 getCandidateConfigurations() 方法:
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;
}
复制代码
看来最核心的方法是 SpringFactroiesLoader.loadFactoryNames() 方法了,我们再进入看看:
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
复制代码
包的好深,居然还有一层,那么继续进入 loadSpringFactories() 方法。
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);
}
}
}
复制代码
终于到最后一层了,算是“拨开云雾见天日,守得云开见月明”,下面就来梳理一下loadSpringFactories()方法。
首先 FACTORIES_RESOURCE_LOCATION 这个常量的值是:
"META-INF/spring.factories"
/**
* The location to look for factories.
* <p>Can be present in multiple JAR files.
*/
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
复制代码
所以第一个端核心代码的意思是:
启动的时候会扫描所有jar包下 META-INF/spring.factories 这个文件。第二段代码的意思是将这些扫描到的文件转成Properties对象,后面两个核心代码的意思就是说将加载到的Properties对象放入到缓存中。
然后 getCandidateConfigurations() 方法,是只获取了key是EnableAutoConfiguration.class 的配置。
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;
}
复制代码
我们看到 getCandidateConfigurations() 方法,通过SpringFactoriesLoader.loadFactoryNames() 获取到了118个配置(待确认)。
那么我们以第一个配置类:
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration 来看一下,这些类都是如果实现的。
打开org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration 的源码:
我们看到这个类有三个注解 @Configuration 、 @AutoConfigureAfter 、@ConditionalOnProperty 、因为有 @Configuration 注解所以它也是一个配置类,然后第二注解中的参数类 JmxAutoConfiguration.class 进入之后是这样的:
也是存在 @ConditionalOnProperty 注解的。那看来关键点就是 @ConditionalOnProperty这个注解了。
这个注解其实是一个条件判断注解,这个条件注解后面的参数的意思是当存在系统属性前缀为spring.application.admin ,并且属性名称为 enabled ,并且值为 true 时,才加载当前这个Bean并进行实例化。
这种spring4.0后面出现的的条件注解,可以极大的增加了框架的灵活性和扩展性,可以保证很多组件可以通过后期配置,而且阅读源码的人,通过这些注解就能明白在什么情况下才会实例化当前Bean。