在工作中,雖然我們都在基於springboot框架開發項目,還是發現很多小夥伴熟悉業務開發,但是對框架的一些底層機制原理不夠了解。因此有了這篇文章的分享。
本文我將從springboot啓動入口開始,逐步跟蹤源碼,逐步分析,力求讓大家通過本文能夠了解springboot的整個啓動流程,主要有以下幾個關注點
-
springboot框架提供了哪些核心能力
-
springboot框架啓動關鍵流程
-
內嵌支持哪些web容器
-
如何實現web容器的啓動
-
如何在不同的web容器之間實現切換
-
前方高能預警,需要翻閱大量源代碼,請做好心理準備!
1.springboot框架核心能力
大體上來看,springboot框架提供了四方面核心能力
-
統一依賴管理
-
自動裝配
-
健康檢查
-
cli
在日常開發中,我們直接受益的有統一依賴管理、自動裝配、健康檢查。想象一下
-
平常開發,我們只需要引入spring-boot-starter-xxx,即可正常使用xxx提供的能力,不用再因爲依賴包之間的兼容性,而花費大量的時間去調試兼容性問題了。這就是springboot統一依賴管理後,帶來的直接收益!
-
相信使用spring框架,從xml配置時代過來的小夥伴,在使用springboot框架後,神清氣爽了很多!畢竟每每想到大量的xml配置文件內容,心有餘悸!這就是springboot自動裝配後,帶來的直接收益!
-
原來我們做應用監控,怎麼做呢?需要在業務層面去開發一些監控接口,反正是要寫代碼!現在springboot直接提供了actuator,業務健康檢查,是那麼so easy的事情!甚至還可以擴展HealthIndicator接口,輕鬆就能實現新的指標監控檢查。這就是springboot提供的actuator,帶來的直接收益!
相比統一依賴管理、自動裝配、健康檢查,cli相對業界用的比較少。
2.springboot框架啓動流程
2.1.內嵌支持的web容器
使用springboot框架以後,最直觀的感受是,哪怕開發web應用,只需要
-
引入spring-boot-starter-web依賴
-
在application.yml文件中,增加配置與web容器相關的一些內容,比如端口、contextpath、靜態資源等
-
開發springmvc相關的controller,開放端點
-
打成一個jar包
-
通過java -jar xxx.jar啓動應用,然後即可以通過瀏覽器訪問應用
就是這麼簡單,稍後我們在啓動流程中詳細分析,web容器是如何啓動的,先看一看,springboot都內嵌了哪些web容器。爲了支持web容器,springboot提供了一個接口
public interface WebServer {
// 啓動容器
void start() throws WebServerException;
// 停止容器
void stop() throws WebServerException;
int getPort();
default void shutDownGracefully(GracefulShutdownCallback callback) {
callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
}
}
併爲WebServer接口,提供瞭如下實現
2.2.web容器啓動流程
默認情況下,即直接引入spring-boot-starter-web依賴,springboot啓動應用的是tomcat容器
接下來,我們就以springboot啓動tomcat容器爲例,來嘗試啓動流程源碼分析。
啓動入口:FollowMeSpringbootActuatorApplication
@SpringBootApplication
public class FollowMeSpringbootActuatorApplication {
public static void main(String[] args) {
//springboot應用啓動入口, SpringApplication.run()方法 SpringApplication.run(FollowMeSpringbootActuatorApplication.class, args);
}
}
源碼解析
-
對於springboot應用,分析它的源碼,我們只需要從啓動類開始即可
-
進入SpringApplication內部,便可一窺究竟!源碼之下無祕密!
進入方法:SpringApplication.run
// 1.從啓動類main方法,進入run方法
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
// 調用了重載的run方法
return run(new Class[]{primarySource}, args);
}
// 2.重載的run方法
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
// new 了一個SpringApplication實例,並再次調用重載run方法
return (new SpringApplication(primarySources)).run(args);
}
// 3.真正啓動spring應用的地方,返回ioc容器:ConfigurableApplicationContext
public ConfigurableApplicationContext run(String... args) {
......省略非關鍵代碼......
try {
......省略非關鍵代碼......
// 關鍵代碼:創建spring應用ioc容器
context = this.createApplicationContext();
......省略非關鍵代碼......
// 關鍵代碼:刷新ioc容器
this.refreshContext(context);
......省略非關鍵代碼......
} catch (Throwable var10) {
......省略非關鍵代碼......
}
......省略非關鍵代碼......
}
源碼解析
-
在SpringApplication內部,有多個重載的run方法
-
我們需要一直跟蹤到方法:run(String... args)
-
在該方法內部,有兩行關鍵代碼需要我們關注
-
context = this.createApplicationContext():創建spring ioc容器
-
this.refreshContext(context):刷新ioc容器,啓動web容器具體內容的入口就是這裏
-
進入方法:createApplicationContext
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
// 重點關注代碼:根據webApplicationType應用類型
// 決定創建ioc容器的具體實現,取值有三類
// SERVLET:創建web容器AnnotationConfigServletWebServerApplicationContext
// REACTIVE:創建響應式web容器AnnotationConfigReactiveWebServerApplicationContext
// 默認:創建普通jar應用容器AnnotationConfigApplicationContext
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具體實現
-
有三種情況,根據webApplicationType取值
-
SERVLET:AnnotationConfigServletWebServerApplicationContext
-
REACTIVE:AnnotationConfigReactiveWebServerApplicationContext
-
默認:AnnotationConfigApplicationContext
-
-
第三個我們很熟悉,在還沒有springboot框架以前,通過註解方式配置ioc的ApplicationContext就是它
-
我們重點關注的應該是:AnnotationConfigServletWebServerApplicationContext,注意類名稱中,有ServletWebServer關鍵字。有點意思了,跟web容器掛上鉤了!
-
但是最關鍵的地方是,webApplicationType的取值,從根據什麼來的呢?這纔是重點!
枚舉類:WebApplicationType
public enum WebApplicationType {
NONE,
SERVLET,
REACTIVE;
// 相關ioc容器ApplicationContext標識類常量定義,用於決定啓動時,加載具體的ApplicationContext實現
private static final String[] SERVLET_INDICATOR_CLASSES = new String[]{"javax.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext"};
private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";
private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";
private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";
private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";
......省略非關鍵代碼......
// 關鍵方法:從classpath下,決定加載ApplicationContext具體實現
static WebApplicationType deduceFromClasspath() {
// 響應式【暫時不關注它,不關注這個if判斷】
if (ClassUtils.isPresent("org.springframework.web.reactive.DispatcherHandler", (ClassLoader)null) && !ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", (ClassLoader)null) && !ClassUtils.isPresent("org.glassfish.jersey.servlet.ServletContainer", (ClassLoader)null)) {
return REACTIVE;
} else {
// 重點關注這裏:
// 如果類路徑classpath下,存在Servlet、存在ConfigurableWebApplicationContext,那麼說明該應用是web應用
// 返回 SERVLET
String[] var0 = SERVLET_INDICATOR_CLASSES;
int var1 = var0.length;
for(int var2 = 0; var2 < var1; ++var2) {
String className = var0[var2];
if (!ClassUtils.isPresent(className, (ClassLoader)null)) {
return NONE;
}
}
return SERVLET;
}
}
......省略非關鍵代碼......
}
源碼解析
-
WebApplicationType枚舉類,用於從classpath下,是否存在Servlet、存在ConfigurableWebApplicationContext
-
來決定啓動普通的spring應用,還是啓動web應用
-
根據引入spring-boot-starter-web依賴以後
-
會自動引入內嵌的tomcat,即存在Servlet
-
會自動引入spring-web,即存在ConfigurableWebApplicationContext
-
因此最終返回 SERVLET
-
類:AnnotationConfigServletWebServerApplicationContext
源碼解析
-
如上圖所示,它的類層次結構說明一切!
小結:到此拿到容器(AnnotationConfigServletWebServerApplicationContext),它明確告訴我們了,這是一個web應用上下文容器。繼續往下,看是如何加載創建webServer
此時,我們需要關注另外一行關鍵代碼了,你還記得它嗎?
// 關鍵代碼:刷新ioc容器
this.refreshContext(context);
進入方法:SpringApplication.refreshContext/refresh
private void refreshContext(ConfigurableApplicationContext context) {
// 進入resresh方法
this.refresh((ApplicationContext)context);
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
} catch (AccessControlException var3) {
;
}
}
/** @deprecated */
@Deprecated
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(ConfigurableApplicationContext.class, applicationContext);
// 繼續進入refresh
this.refresh((ConfigurableApplicationContext)applicationContext);
}
protected void refresh(ConfigurableApplicationContext applicationContext) {
// 關鍵代碼:最終調用了ApplicationContext的refresh方法
applicationContext.refresh();
}
源碼解析
-
通過一路源碼跟蹤,最終發現代碼 applicationContext.refresh();
-
即實現了ApplicationContext的刷新操作,而我們從上面知道,此時的applicationContext,它是ServletWebServerApplicationContext
進入方法:ServletWebServerApplicationContext.refresh
public final void refresh() throws BeansException, IllegalStateException {
try {
// 繼續調用父類refresh方法
super.refresh();
} catch (RuntimeException var3) {
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.stop();
}
throw var3;
}
}
//抽閒父類:AbstractApplicationContext#refresh
public void refresh() throws BeansException, IllegalStateException {
......省略非關鍵代碼......
try {
......省略非關鍵代碼......
// 關鍵代碼:調用onRefresh方法
this.onRefresh();
......省略非關鍵代碼......
} catch (BeansException var9) {
......省略非關鍵代碼......
throw var9;
} finally {
this.resetCommonCaches();
}
}
}
// onRefresh方法,在父類中是一個空方法,即鉤子方法
// 該方法的具體實現,留給子類實現
// 此時的具體實現子類是:ServletWebServerApplicationContext
protected void onRefresh() throws BeansException {
}
源碼解析
-
一路追蹤refresh方法,最終跟到了onRefresh方法
-
而onRefresh方法,在父類中是空實現,具體實現在子類
-
此時的子類是:ServletWebServerApplicationContext
進入方法:ServletWebServerApplicationContext.onRefresh
protected void onRefresh() {
super.onRefresh();
try {
// 關鍵代碼:創建WebServer,高興!快要柳暗花明了!
this.createWebServer();
} catch (Throwable var2) {
throw new ApplicationContextException("Unable to start web server", var2);
}
}
進入方法:ServletWebServerApplicationContext.createWebServer
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = this.getServletContext();
if (webServer == null && servletContext == null) {
// 關鍵代碼:獲取ServletWebServer工廠,該工廠用於創建WebServer
ServletWebServerFactory factory = this.getWebServerFactory();
this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
......省略非關鍵代碼......
} else if (servletContext != null) {
......省略非關鍵代碼......
}
......省略非關鍵代碼......
}
protected ServletWebServerFactory getWebServerFactory() {
// 關鍵代碼:從classpath中,檢查加載具體的webServer:tomcat/jetty/netty/undertow
String[] beanNames = this.getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
if (beanNames.length == 0) {
......省略非關鍵代碼......
} else if (beanNames.length > 1) {
......省略非關鍵代碼......
} else {
return (ServletWebServerFactory)this.getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}
}
源碼解析
-
到此,我們看到最終需要獲取ServletWebServerFactory工廠,該工廠用於創建具體的ServletWebServer實例
-
即具體創建的web容器是:tomcat、或者jetty、或者netty、還是undertow,需要檢查classpath類路徑下的依賴來決定
-
即具體的web容器依賴來決定,存在誰,那麼加載的就是誰
小結:到此,我們得到了這麼幾個信息
-
首先,已經明確這是一個web應用,獲取到的ApplicationContext是ServletWebServerApplicationContext
-
其次,啓動web應用,需要創建一個WebServer,該WebServer由具體的工廠來創建,該工廠是ServletWebServerFactory
-
最終,在ServletWebServerFactory工廠中,需要創建哪個具體的web容器(tomcat/jetty/netty/undertow),由類路徑classpath依賴決定
最後的謎底,我們需要回到最開始介紹springboot提供的核心能力的知識點了,其中有一條是說自動裝配。
最後來看一下,最終獲取的ServletWebServerFactory,到底是誰?
找到spring-boot-autoconfigure依賴,並展開它,一直找到包
org.springframework.boot.autoconfigure.web.embedded
並找到類
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnWebApplication
@EnableConfigurationProperties({ServerProperties.class})
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {
public EmbeddedWebServerFactoryCustomizerAutoConfiguration() {
}
@Configuration(
proxyBeanMethods = false
)
// 如果classpath下存在HttpServer類,那麼啓用NettyWebServer
@ConditionalOnClass({HttpServer.class})
public static class NettyWebServerFactoryCustomizerConfiguration {
public NettyWebServerFactoryCustomizerConfiguration() {
}
@Bean
public NettyWebServerFactoryCustomizer nettyWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
return new NettyWebServerFactoryCustomizer(environment, serverProperties);
}
}
@Configuration(
proxyBeanMethods = false
)
// 如果classpath下存在Undertow,那麼啓用UndertowWebServer
@ConditionalOnClass({Undertow.class, SslClientAuthMode.class})
public static class UndertowWebServerFactoryCustomizerConfiguration {
public UndertowWebServerFactoryCustomizerConfiguration() {
}
@Bean
public UndertowWebServerFactoryCustomizer undertowWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
return new UndertowWebServerFactoryCustomizer(environment, serverProperties);
}
}
@Configuration(
proxyBeanMethods = false
)
// 如果classpath下,存在Server類,那麼啓用JettyWebServer
@ConditionalOnClass({Server.class, Loader.class, WebAppContext.class})
public static class JettyWebServerFactoryCustomizerConfiguration {
public JettyWebServerFactoryCustomizerConfiguration() {
}
@Bean
public JettyWebServerFactoryCustomizer jettyWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
return new JettyWebServerFactoryCustomizer(environment, serverProperties);
}
}
@Configuration(
proxyBeanMethods = false
)
// 如果classpath下存在Tomcat類,那麼啓用TomcatWebServer
@ConditionalOnClass({Tomcat.class, UpgradeProtocol.class})
public static class TomcatWebServerFactoryCustomizerConfiguration {
public TomcatWebServerFactoryCustomizerConfiguration() {
}
@Bean
public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
}
}
}
源碼解析
-
EmbeddedWebServerFactoryCustomizerAutoConfiguration,是一個自動裝配類
-
它根據classpath下,是否存在HttpServer、Undertow、Server、Tomcat類,來決定啓用哪個web容器
-
HttpServer--->Netty
-
Undertow--->Undertow
-
Server--->Jetty
-
Tomcat--->Tomcat
-
-
在我們當前應用中,classpath下存在的是Tomcat,因此啓動的是Tomcat web容器
相信到這裏,你可以理解springboot應用中,整個web應用的啓動流程了。源碼有點多,如有不適,請多看幾遍!
2.3.不同web容器之間切換
從前面的內容,我們理清了springboot是如何啓動web應用的,它啓用不同的web容器關鍵點是,看classpath下依賴了誰?因此要想在不同的web容器之間切換,實現就非常簡單,分兩個步驟
-
首先在pom.xml文件中,排除tomcat依賴
-
加入其它web容器的依賴即可