共计 9100 个字符,预计需要花费 23 分钟才能阅读完成。
在 Spring Boot 呈现之前,咱们要运行一个 Java Web 利用,首先须要有一个 Web 容器(例如 Tomcat 或 Jetty),而后将咱们的 Web 利用打包后放到容器的相应目录下,最初再启动容器。
在 IDE 中也须要对 Web 容器进行一些配置,才可能运行或者 Debug。而应用 Spring Boot 咱们只须要像运行一般 JavaSE 程序一样,run 一下 main() 办法就能够启动一个 Web 利用了。这是怎么做到的呢?明天咱们就一探到底,剖析一下 Spring Boot 的启动流程。
概览
回看咱们写的第一个 Spring Boot 示例,咱们发现,只须要上面几行代码咱们就能够跑起一个 Web 服务器:
@SpringBootApplication
public class HelloApplication {
public static void main(String[] args) {SpringApplication.run(HelloApplication.class, args); | |
} |
}
复制代码
去掉类的申明和办法定义这些样板代码,外围代码就只有一个 @SpringBootApplication 注解和 SpringApplication.run(HelloApplication.class, args) 了。而咱们晓得注解相当于是一种配置,那么这个 run() 办法必然就是 Spring Boot 的启动入口了。
接下来,咱们沿着 run() 办法来顺藤摸瓜。进入 SpringApplication 类,来看看 run() 办法的具体实现:
public class SpringApplication {
...... | |
public ConfigurableApplicationContext run(String... args) { | |
// 1 利用启动计时开始 | |
StopWatch stopWatch = new StopWatch(); | |
stopWatch.start(); | |
// 2 申明上下文 | |
DefaultBootstrapContext bootstrapContext = createBootstrapContext(); | |
ConfigurableApplicationContext context = null; | |
// 3 设置 java.awt.headless 属性 | |
configureHeadlessProperty(); | |
// 4 启动监听器 | |
SpringApplicationRunListeners listeners = getRunListeners(args); | |
listeners.starting(bootstrapContext, this.mainApplicationClass); | |
try { | |
// 5 初始化默认利用参数 | |
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); | |
// 6 筹备应用环境 | |
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments); | |
configureIgnoreBeanInfo(environment); | |
// 7 打印 Banner(Spring Boot 的 LOGO)Banner printedBanner = printBanner(environment); | |
// 8 创立上下文实例 | |
context = createApplicationContext(); | |
context.setApplicationStartup(this.applicationStartup); | |
// 9 构建上下文 | |
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner); | |
// 10 刷新上下文 | |
refreshContext(context); | |
// 11 刷新上下文后处理 | |
afterRefresh(context, applicationArguments); | |
// 12 利用启动计时完结 | |
stopWatch.stop(); | |
if (this.logStartupInfo) { | |
// 13 打印启动工夫日志 | |
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); | |
} | |
// 14 公布上下文启动实现事件 | |
listeners.started(context); | |
// 15 调用 runners | |
callRunners(context, applicationArguments); | |
} | |
catch (Throwable ex) { | |
// 16 利用启动产生异样后的解决 | |
handleRunFailure(context, ex, listeners); | |
throw new IllegalStateException(ex); | |
} | |
try { | |
// 17 公布上下文就绪事件 | |
listeners.running(context); | |
} | |
catch (Throwable ex) {handleRunFailure(context, ex, null); | |
throw new IllegalStateException(ex); | |
} | |
return context; | |
} | |
...... |
}
复制代码
Spring Boot 启动时做的所有操作都这这个办法外面,当然在调用下面这个 run() 办法之前,还创立了一个 SpringApplication 的实例对象。因为下面这个 run() 办法并不是一个静态方法,所以须要一个对象实例能力被调用。
能够看到,办法的返回值类型为 ConfigurableApplicationContext,这是一个接口,咱们真正失去的是 AnnotationConfigServletWebServerApplicationContext 的实例。通过类名咱们能够晓得,这是一个基于注解的 Servlet Web 利用上下文(咱们晓得上下文(context)是 Spring 中的外围概念)。
下面对于 run() 办法中的每一个步骤都做了简略的正文,接下来咱们抉择几个比拟有代表性的来详细分析。
利用启动计时
在 Spring Boot 利用启动实现时,咱们常常会看到相似上面内容的一条日志:
Started AopApplication in 2.732 seconds (JVM running for 3.734)
复制代码
利用启动后,会将本次启动所破费的工夫打印进去,让咱们对于启动的速度有一个大抵的理解,也不便咱们对其进行优化。记录启动工夫的工作是 run() 办法做的第一件事,在编号 1 的地位由 stopWatch.start() 开启工夫统计,具体代码如下:
public void start(String taskName) throws IllegalStateException {
if (this.currentTaskName != null) {throw new IllegalStateException("Can't start StopWatch: it's already running"); | |
} | |
// 记录启动工夫 | |
this.currentTaskName = taskName; | |
this.startTimeNanos = System.nanoTime(); |
}
复制代码
而后到了 run() 办法的根本工作实现的时候,由 stopWatch.stop()(编号 12 的地位)对启动工夫做了一个计算,源码也很简略:
public void stop() throws IllegalStateException {
if (this.currentTaskName == null) {throw new IllegalStateException("Can't stop StopWatch: it's not running"); | |
} | |
// 计算启动工夫 | |
long lastTime = System.nanoTime() - this.startTimeNanos; | |
this.totalTimeNanos += lastTime; | |
...... |
}
复制代码
最初,在 run() 中的编号 13 的地位将启动工夫打印进去:
if (this.logStartupInfo) {
// 打印启动工夫
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
复制代码
打印 Banner
Spring Boot 每次启动是还会打印一个本人的 LOGO,如图 8-6:
图 8-6 Spring Boot Logo
这种做法很常见,像 Redis、Docker 等都会在启动的时候将本人的 LOGO 打印进去。Spring Boot 默认状况下会打印那个标志性的“树叶”和“Spring”的字样,上面带着以后的版本。
在 run() 中编号 7 的地位调用打印 Banner 的逻辑,最终由 SpringBootBanner 类的 printBanner() 实现。这个图案定义在一个常量数组中,代码如下:
class SpringBootBanner implements Banner {
private static final String[] BANNER = {""," . ____ _ __ _ _"," /\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\", | |
"(()\\___ |'_ | '_| |'_ \\/ _` | \\ \\ \\ \\"," \\\\/ ___)| |_)| | | | | || (_| |) ) ) )"," '|____| .__|_| |_|_| |_\\__, | / / / /", | |
"=========|_|==============|___/=/_/_/_/" | |
}; | |
...... | |
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream printStream) {for (String line : BANNER) {printStream.println(line); | |
} | |
...... | |
} |
}
复制代码
手工格式化了一下 BANNER 的字符串,轮廓曾经清晰可见了。真正打印的逻辑就是 printBanner() 办法外面的那个 for 循环。
记录启动工夫和打印 Banner 代码都十分的简略,而且都有很显著的视觉反馈,能够清晰的看到后果。拿进去咱们做个热身,配合断点去 Debug 会有更加直观的感触,尤其是打印 Banner 的时候,能够看到整个内容被一行一行打印进去,让我想起了早些年用那些配置极低的电脑(还是 CRT 显示器)运行着 Win98,常常会看到屏幕内容一行一行加载显示。
创立上下文实例
上面咱们来到 run() 办法中编号 8 的地位,这里调用了一个 createApplicationContext() 办法,该办法最终会调用 ApplicationContextFactory 接口的代码:
ApplicationContextFactory DEFAULT = (webApplicationType) -> {
try {switch (webApplicationType) { | |
case SERVLET: | |
return new AnnotationConfigServletWebServerApplicationContext(); | |
case REACTIVE: | |
return new AnnotationConfigReactiveWebServerApplicationContext(); | |
default: | |
return new AnnotationConfigApplicationContext();} | |
} | |
catch (Exception ex) { | |
throw new IllegalStateException("Unable create a default ApplicationContext instance," | |
+ "you may need a custom ApplicationContextFactory", ex); | |
} |
};
复制代码
这个办法就是依据 SpringBootApplication 的 webApplicationType 属性的值,利用反射来创立不同类型的利用上下文(context)。而属性 webApplicationType 的值是在后面执行构造方法的时候由 WebApplicationType.deduceFromClasspath() 取得的。通过办法名很容易看进去,就是依据 classpath 中的类来推断以后的利用类型。
咱们这里是一个一般的 Web 利用,所以最终返回的类型为 SERVLET。所以会返回一个 AnnotationConfigServletWebServerApplicationContext 实例。
构建容器上下文
接着咱们来到 run() 办法编号 9 的 prepareContext() 办法。通过办法名,咱们也能猜到它是为 context 做上台前的筹备工作的。
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, | |
ApplicationArguments applicationArguments, Banner printedBanner) { | |
...... | |
// 加载资源 | |
load(context, sources.toArray(new Object[0])); | |
listeners.contextLoaded(context); |
}
复制代码
在这个办法中,会做一些筹备工作,包含初始化容器上下文、设置环境、加载资源等。
加载资源
下面的代码中,又调用了一个很要害的办法——load()。这个 load() 办法真正的作用是去调用 BeanDefinitionLoader 类的 load() 办法。源码如下:
class BeanDefinitionLoader {
...... | |
void load() {for (Object source : this.sources) {load(source); | |
} | |
} | |
private void load(Object source) {Assert.notNull(source, "Source must not be null"); | |
if (source instanceof Class<?>) {load((Class<?>) source); | |
return; | |
} | |
if (source instanceof Resource) {load((Resource) source); | |
return; | |
} | |
if (source instanceof Package) {load((Package) source); | |
return; | |
} | |
if (source instanceof CharSequence) {load((CharSequence) source); | |
return; | |
} | |
throw new IllegalArgumentException("Invalid source type" + source.getClass()); | |
} | |
...... |
}
复制代码
能够看到,load() 办法在加载 Spring 中各种资源。其中咱们最相熟的就是 load((Class<?>) source) 和 load((Package) source) 了。一个用来加载类,一个用来加载扫描的包。
load((Class<?>) source) 中会通过调用 isComponent() 办法来判断资源是否为 Spring 容器治理的组件。isComponent() 办法通过资源是否蕴含 @Component 注解(@Controller、@Service、@Repository 等都蕴含在内)来辨别是否为 Spring 容器治理的组件。
而 load((Package) source) 办法则是用来加载 @ComponentScan 注解定义的包门路。
刷新上下文
run() 办法编号 10 的 refreshContext() 办法是整个启动过程比拟外围的中央。像咱们相熟的 BeanFactory 就是在这个阶段构建的,所有非懒加载的 Spring Bean(@Controller、@Service 等)也是在这个阶段被创立的,还有 Spring Boot 内嵌的 Web 容器要是在这个时候启动的。
跟踪源码你会发现外部调用的是 ConfigurableApplicationContext.refresh(),ConfigurableApplicationContext 是一个接口,真正实现这个办法的有三个类:AbstractApplicationContext、ReactiveWebServerApplicationContext 和 ServletWebServerApplicationContext。
AbstractApplicationContext 为前面两个的父类,两个子类的实现比较简单,次要是调用父类实现,比方 ServletWebServerApplicationContext 中的实现是这样的:
public final void refresh() throws BeansException, IllegalStateException {
try {
super.refresh();
}
catch (RuntimeException ex) {
WebServer webServer = this.webServer; | |
if (webServer != null) {webServer.stop(); | |
} | |
throw ex; |
}
}
复制代码
次要的逻辑都在 AbstractApplicationContext 中:
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh"); | |
// 1 筹备将要刷新的上下文 | |
prepareRefresh(); | |
// 2(通知子类,如:ServletWebServerApplicationContext)刷新外部 bean 工厂 | |
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); | |
// 3 为上下文筹备 bean 工厂 | |
prepareBeanFactory(beanFactory); | |
try { | |
// 4 容许在子类中对 bean 工厂进行后处理 | |
postProcessBeanFactory(beanFactory); | |
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process"); | |
// 5 调用注册为 bean 的工厂处理器 | |
invokeBeanFactoryPostProcessors(beanFactory); | |
// 6 注册拦截器创立的 bean 处理器 | |
registerBeanPostProcessors(beanFactory); | |
beanPostProcess.end(); | |
// 7 初始化国际化相干资源 | |
initMessageSource(); | |
// 8 初始化事件播送器 | |
initApplicationEventMulticaster(); | |
// 9 为具体的上下文子类初始化特定的 bean | |
onRefresh(); | |
// 10 注册监听器 | |
registerListeners(); | |
// 11 实例化所有非懒加载的单例 bean | |
finishBeanFactoryInitialization(beanFactory); | |
// 12 实现刷新公布相应的事件(Tomcat 就是在这里启动的)finishRefresh();} | |
catch (BeansException ex) {if (logger.isWarnEnabled()) { | |
logger.warn("Exception encountered during context initialization -" + | |
"cancelling refresh attempt:" + ex); | |
} | |
// 遇到异样销毁曾经创立的单例 bean | |
destroyBeans(); | |
// 充值 active 标识 | |
cancelRefresh(ex); | |
// 将异样向上抛出 | |
throw ex; | |
} finally { | |
// 重置公共缓存,完结刷新 | |
resetCommonCaches(); | |
contextRefresh.end();} | |
} |
}
复制代码
简略说一下编号 9 处的 onRefresh() 办法,该办法父类未给出具体实现,须要子类本人实现,ServletWebServerApplicationContext 中的实现如下:
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
private void createWebServer() {
……
if (webServer == null && servletContext == null) {
...... | |
// 依据配置获取一个 web server(Tomcat、Jetty 或 Undertow)ServletWebServerFactory factory = getWebServerFactory(); | |
this.webServer = factory.getWebServer(getSelfInitializer()); | |
...... |
}
……
}
复制代码
factory.getWebServer(getSelfInitializer()) 会依据我的项目配置失去一个 Web Server 实例,这里跟下一篇将要谈到的主动配置有点关系