首先,程序的主入口,也就是xxxApplication类中main方法下的run方法
SpringApplication.run(AlumniApplication.class, args);
如果我们自动补全对象,会得到,这个run方法的返回值是一个ConfigurableApplicationContext类型的对象
ConfigurableApplicationContext run = SpringApplication.run(AlumniApplication.class, args);
当我们调出ConfigurableApplicationContext类的关系图时,可以发现他有很多级的父级类,上一级是一个叫ApplicationContext的类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C6Iw9JXf-1639019234455)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20211130232429630.png)]
而这个类我们并不陌生,在初学Spring时我们常用这个类加载xxxbean.xml文件,即以通用方式加载文件资源的能力,
那我们便得到合理推测,run方法可能就是通过加载一些配置文件,完成springboot项目的自动装配
为了验证猜想,我们通过逐层进入run方法进行验证,最终我们来到最后的一个run方法
/**
* 运行应用程序,创建并刷新一个新的应用程序上下文
*
* @param args
* @return
*/
public ConfigurableApplicationContext run(String... args) {
/**
* StopWatch: 简单的秒表,允许定时的一些任务,公开每个指定任务的总运行时间和运行时间。
* 这个对象的设计不是线程安全的,没有使用同步。SpringApplication是在单线程环境下,使用安全。
*/
StopWatch stopWatch = new StopWatch();
// 设置当前启动的时间为系统时间startTimeMillis = System.currentTimeMillis();
stopWatch.start();
// 创建一个应用上下文引用
ConfigurableApplicationContext context = null;
// 异常收集,报告启动异常
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
/**
* 系统设置headless模式(一种缺乏显示设备、键盘或鼠标的环境下,比如服务器),
* 通过属性:java.awt.headless=true控制
*/
configureHeadlessProperty();
/*
* 获取事件推送监器,负责产生事件,并调用支某类持事件的监听器
* 事件推送原理看上面的事件推送原理图
*/
SpringApplicationRunListeners listeners = getRunListeners(args);
/**
* 发布一个启动事件(ApplicationStartingEvent),通过上述方法调用支持此事件的监听器
*/
listeners.starting();
try {
// 提供对用于运行SpringApplication的参数的访问。取默认实现
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
/**
* 构建容器环境,这里加载配置文件
*/
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
// 对环境中一些bean忽略配置
configureIgnoreBeanInfo(environment);
// 日志控制台打印设置
Banner printedBanner = printBanner(environment);
// 创建容器
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context);
/**
* 准备应用程序上下文
* 追踪源码prepareContext()进去我们可以发现容器准备阶段做了下面的事情:
* 容器设置配置环境,并且监听容器,初始化容器,记录启动日志,
* 将给定的singleton对象添加到此工厂的singleton缓存中。
* 将bean加载到应用程序上下文中。
*/
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
/**
* 刷新上下文
* 1、同步刷新,对上下文的bean工厂包括子类的刷新准备使用,初始化此上下文的消息源,注册拦截bean的处理器,检查侦听器bean并注册它们,实例化所有剩余的(非延迟-init)单例。
* 2、异步开启一个同步线程去时时监控容器是否被关闭,当关闭此应用程序上下文,销毁其bean工厂中的所有bean。
* 。。。底层调refresh方法代码量较多
*/
refreshContext(context);
afterRefresh(context, applicationArguments);
// stopwatch 的作用就是记录启动消耗的时间,和开始启动的时间等信息记录下来
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 发布一个已启动的事件
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
// 发布一个运行中的事件
listeners.running(context);
}
catch (Throwable ex) {
// 启动异常,里面会发布一个失败的事件
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();//监听执行时间
stopWatch.start();//监听开始
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;//申明了一个上下文对象
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();//创建了上下文对象
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);//刷新容器对象
afterRefresh(context, applicationArguments);
stopWatch.stop();//监听结束
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
这里有个小插曲,顺便分析一下springboot如何加载application.properties配置文件的,在上面的run方法中,有这么一行,作用是构造容器环境,加载配置文件
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
进入listeners,其中就包含加载application.properties的类,org.springframework.boot.context.config.ConfigFileApplicationListener
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cXUAqtd5-1639019234457)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20211201093606917.png)]
进入ConfigFileApplicationListener,看一下实现方式
private static final String DEFAULT_NAMES = "application";//默认加载的配置文件名为application
ConfigFileApplicationListener 实现onApplicationEvent方法和supportsEventType方法
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType)
|| ApplicationPreparedEvent.class.isAssignableFrom(eventType);
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
throw new IllegalStateException("ConfigFileApplicationListener [" + getClass().getName()
+ "] is deprecated and can only be used as an EnvironmentPostProcessor");
}
ApplicationEnvironmentPreparedEvent类中会找到一个load方法
private void load(String location, String name, Profile profile,
DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
if (!StringUtils.hasText(name)) {
for (PropertySourceLoader loader : this.propertySourceLoaders) {
if (canLoadFileExtension(loader, location)) {
load(loader, location, profile,
filterFactory.getDocumentFilter(profile), consumer);
return;
}
}
}
Set<String> processed = new HashSet<>();
for (PropertySourceLoader loader : this.propertySourceLoaders) {
for (String fileExtension : loader.getFileExtensions()) {
if (processed.add(fileExtension)) {
//加载配置文件,文件路径 //location+name++“."+fileExtension(文件扩展名)
//profile 是不同环境的配置文件
loadForFileExtension(loader, location + name, "." + fileExtension,
profile, filterFactory, consumer);
}
}
}
profile代表不同环境 将profile传入loadForFileExtension()方法中如下:
loadForFileExtension(loader, location + name, "." + fileExtension,profile, filterFactory, consumer)
进入方法看一下核心逻辑
if (profile != null) {
// Try profile-specific file & profile section in profile file (gh-340)
String profileSpecificFile = prefix + "-" + profile + fileExtension;
load(loader, profileSpecificFile, profile, defaultFilter, consumer);
load(loader, profileSpecificFile, profile, profileFilter, consumer);
// Try profile specific sections in files we've already processed
for (Profile processedProfile : this.processedProfiles) {
if (processedProfile != null) {
String previouslyLoaded = prefix + "-" + processedProfile
+ fileExtension;
load(loader, previouslyLoaded, profile, profileFilter, consumer);
}
}
}
对于激活文件也就是不同环境各自的配置文件和默认的文件的优先级
下面的代码根据profile进行判断 profile=null的 addFirst profile!=null 的addLast
很明显,先加载环境配置为空的也就是application.properties文件,在加载激活的环境配置文件 application-xxx.properties文件
public void load() {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
initializeProfiles();
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
if (profile != null && !profile.isDefaultProfile()) {
addProfileToEnvironment(profile.getName());
}
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
resetEnvironmentProfiles(this.processedProfiles);
load(null, this::getNegativeProfileFilter,
addToLoaded(MutablePropertySources::addFirst, true));
addLoadedPropertySources();
}
关于文件扩展名由此接口PropertySourceLoader 加载
这个接口里有一个方法String[] getFileExtensions();一共有两种实现方式
@Override
public String[] getFileExtensions() {
return new String[] { "properties", "xml" };
}
@Override
public String[] getFileExtensions() {
return new String[] { "yml", "yaml" };
}
所以配置文件的后缀可以有四种不同的方式 加载先后顺序为 properties,xml,yml,yaml 后加载的覆盖先加载
最后具体的路径就是 location + name + “-” + profile + “.” + ext
话归正题
我们摘出run方法关键代码,该方法中申明并创建了了一个上下文对象,并刷新了这个上下文对象最终返回
//申明
ConfigurableApplicationContext context = null;
//创建
context = createApplicationContext();
//刷新
refreshContext(context);
//返回
return context;
重点进入refresh方法,可以很清晰的看到,这里面执行了一些初始化方法,将创建的实例交由bean factory管理(即spring的ioc思想),但是没有我们想看到的与自动装配相关的方法
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
contextRefresh.end();
}
总结,run方法进行的是spring ioc的初始化
第1步中我们并没有得到自动装配相关的线索
我们将目光转移向启动类上的注解@SpringBootApplication,进入@SpringBootApplication注解,会发现他还有7个注解,前四个为元注解,所以我们重点看后三个
@Target(ElementType.TYPE)//规定注解可以使用在类,接口上
@Retention(RetentionPolicy.RUNTIME)//规定注解在程序运行时生效
@Documented//注解若被修饰会被注册到Api文档中
@Inherited//@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
@ComponentScan注解的作用为扫描包,若未指定扫描路径,则会扫描当前注解所修饰的类所在的包下所有被@Component注解修饰的类.
@SpringBootConfiguration注解下依然有多个注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration//此注解标记的类为java配置类
@Indexed//为@ComponentScan提升性能
在项目中使用了@Indexed
之后,编译打包的时候会在项目中自动生成META-INT/spring.components
文件。
当Spring应用上下文执行ComponentScan
扫描时,META-INT/spring.components
将会被CandidateComponentsIndexLoader
读取并加载,转换为CandidateComponentsIndex
对象,这样的话@ComponentScan
不在扫描指定的package,而是读取CandidateComponentsIndex
对象,从而达到提升性能的目的。
简单点说,使用了@Indexed注解后,原来@ComponentScan可能需要扫描非常多的类,现在只需要在启动后对spring.components进行一次io,将类路径写入文件,后续将只需要读取文件即可获得所有类路径
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qtQbIG3c-1639019234458)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20211201002333837.png)]
进入@EnableAutoConfiguration注解,又有以下组合注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
照例,前四个不管,我们进入@AutoConfigurationPackage注解,发现都有一个叫做@Import的注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
@Import(value = {User.class,Student.class})//将类注入spring容器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jtZuA9Tt-1639019234459)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20211201004513944.png)]
了解了@Import注解,我们进入AutoConfigurationImportSelector类,AutoConfigurationImportSelector类实现了DeferredImportSelector接口,如上述,我们需要找selectImports方法
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
进入getAutoConfigurationEntry,有一行为候选的配置信息
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
进入getCandidateConfigurations
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;
}
进入loadFactoryNames,一步步进入关键路径,得到最终访问的文件"META-INF/spring.factories",也就是说getCandidateConfigurations就是加载所有的META-INF/spring.factories文件
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
debug打上断点以后,就能清楚地看到spring.factories文件中加载了多少个Java类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sFPmQCvD-1639019234460)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20211201020454130.png)]
我们找到这个文件,内容完全对应,文件中是加载到内存中的java类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gZ14kRrb-1639019234461)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20211201021949806.png)]
而spring.factories文件其实有多个,如下,mybatis-plus-starter中同样也有这个spring.factories
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ULdColn4-1639019234462)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20211201022611876.png)]
进入com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration,能够轻松找到mybatis的SqlSessionFactory核心类,也就是说,spring将核心类注入了spring容器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h1m3Oqqw-1639019234463)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20211201022755262.png)]
顺便对getAutoConfigurationEntry方法做一下解析
protected AutoConfigurationEntry getAutoConfigurationEntry(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);
//根据META-INF/spring-autoconfigure-metadata.properties中的条件进行过滤,如果能找到对应的class,则加载,否则,过滤
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
protected static final String PATH = "META-INF/spring-autoconfigure-metadata.properties";
Spring Boot 对开发效率的提升是全方位的,我们可以简单做一下对比:
在没有使用 Spring Boot 之前我们开发一个 web 项目需要做哪些工作:
1)配置 web.xml,加载 Spring 和 Spring mvc
2)配置数据库连接、配置 Spring 事务
3)配置加载配置文件的读取,开启注解
4)配置日志文件
n) 配置完成之后部署 tomcat 调试
而springboot可以直接下载组件,通过application.properties直接进行统一的配置
自带springboot-test,测试简单
部署更加便捷,自带maven容器,一键打jar包或者war包即可进行部署
讲的特别好,也顺带把整个springboot自动装配讲完了
SpringBoot启动tomcat源码解读 - darendu - 博客园 (cnblogs.com)
这个讲解的顺序最好
springboot的启动时的一个自动装配过程 - IT-QI - 博客园 (cnblogs.com)
:
在没有使用 Spring Boot 之前我们开发一个 web 项目需要做哪些工作:
1)配置 web.xml,加载 Spring 和 Spring mvc
2)配置数据库连接、配置 Spring 事务
3)配置加载配置文件的读取,开启注解
4)配置日志文件
n) 配置完成之后部署 tomcat 调试
而springboot可以直接下载组件,通过application.properties直接进行统一的配置
自带springboot-test,测试简单
部署更加便捷,自带maven容器,一键打jar包或者war包即可进行部署
讲的特别好,也顺带把整个springboot自动装配讲完了
SpringBoot启动tomcat源码解读 - darendu - 博客园 (cnblogs.com)
这个讲解的顺序最好
springboot的启动时的一个自动装配过程 - IT-QI - 博客园 (cnblogs.com)