SpringBoot 中內嵌 Tomcat 的實現原理解析,你瞭解嗎?

SpringBoot 中內嵌 Tomcat 的實現原理解析,你瞭解嗎?

引言

對於一個 SpringBoot web 工程來說,一個主要的依賴標誌就是有 spring-boot-starter-web 這個 starter ,spring-boot-starter-web 模塊在 spring boot 中其實並沒有代碼存在,只是在 pom.xml 中攜帶了一些依賴,包括 web、webmvc、tomcat 等:

<dependencies>
<dependency>
\t<groupid>org.springframework.boot/<groupid>
\t<artifactid>spring-boot-starter/<artifactid>
/<dependency>
<dependency>
\t<groupid>org.springframework.boot/<groupid>
\t<artifactid>spring-boot-starter-json/<artifactid>
/<dependency>
<dependency>
\t<groupid>org.springframework.boot/<groupid>
\t<artifactid>spring-boot-starter-tomcat/<artifactid>
/<dependency>
<dependency>
\t<groupid>org.hibernate.validator/<groupid>
\t<artifactid>hibernate-validator/<artifactid>
/<dependency>
<dependency>
\t<groupid>org.springframework/<groupid>
\t<artifactid>spring-web/<artifactid>
/<dependency>
<dependency>
\t<groupid>org.springframework/<groupid>
\t<artifactid>spring-webmvc/<artifactid>
/<dependency>
/<dependencies>
複製代碼

Spring Boot 默認的 web 服務容器是 tomcat ,如果想使用 Jetty 等來替換 Tomcat ,可以自行參考官方文檔來解決。

web、webmvc、tomcat 等提供了 web 應用的運行環境,那 spring-boot-starter 則是讓這些運行環境工作的開關(因為 spring-boot-starter 中會間接引入 spring-boot-autoconfigure )。

WebServer 自動配置

在 spring-boot-autoconfigure 模塊中,有處理關於 WebServer 的自動配置類 ServletWebServerFactoryAutoConfiguration 。

ServletWebServerFactoryAutoConfiguration

代碼片段如下:

@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
\t\tServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
\t\tServletWebServerFactoryConfiguration.EmbeddedJetty.class,
\t\tServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration
複製代碼

兩個 Condition 表示當前運行環境是基於 servlet 標準規範的 web 服務:

  • ConditionalOnClass(ServletRequest.class) : 表示當前必須有 servlet-api 依賴存在
  • ConditionalOnWebApplication(type = Type.SERVLET) :僅基於servlet的Web應用程序

@EnableConfigurationProperties(ServerProperties.class):ServerProperties 配置中包括了常見的 server.port 等配置屬性。

通過 @Import 導入嵌入式容器相關的自動配置類,有 EmbeddedTomcat、EmbeddedJetty 和EmbeddedUndertow。

綜合來看,ServletWebServerFactoryAutoConfiguration 自動配置類中主要做了以下幾件事情:

  • 導入了內部類 BeanPostProcessorsRegistrar,它實現了 ImportBeanDefinitionRegistrar,可以實現ImportBeanDefinitionRegistrar 來註冊額外的 BeanDefinition。
  • 導入了 ServletWebServerFactoryConfiguration.EmbeddedTomcat 等嵌入容器先關配置(我們主要關注tomcat 相關的配置)。
  • 註冊了ServletWebServerFactoryCustomizer、TomcatServletWebServerFactoryCustomizer 兩個WebServerFactoryCustomizer 類型的 bean。

下面就針對這幾個點,做下詳細的分析。

BeanPostProcessorsRegistrar

BeanPostProcessorsRegistrar 這個內部類的代碼如下(省略了部分代碼):

public static class BeanPostProcessorsRegistrar
implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
// 省略代碼
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
if (this.beanFactory == null) {
return;
}
// 註冊 WebServerFactoryCustomizerBeanPostProcessor
registerSyntheticBeanIfMissing(registry,
"webServerFactoryCustomizerBeanPostProcessor",
WebServerFactoryCustomizerBeanPostProcessor.class);
// 註冊 errorPageRegistrarBeanPostProcessor
registerSyntheticBeanIfMissing(registry,
"errorPageRegistrarBeanPostProcessor",
ErrorPageRegistrarBeanPostProcessor.class);
}
// 省略代碼
}
複製代碼

上面這段代碼中,註冊了兩個 bean,一個 WebServerFactoryCustomizerBeanPostProcessor,一個 errorPageRegistrarBeanPostProcessor;這兩個都實現類 BeanPostProcessor 接口,屬於 bean 的後置處理器,作用是在 bean 初始化前後加一些自己的邏輯處理。

  • WebServerFactoryCustomizerBeanPostProcessor:作用是在 WebServerFactory 初始化時調用上面自動配置類注入的那些 WebServerFactoryCustomizer ,然後調用 WebServerFactoryCustomizer 中的 customize 方法來 處理 WebServerFactory。
  • errorPageRegistrarBeanPostProcessor:和上面的作用差不多,不過這個是處理 ErrorPageRegistrar 的。

下面簡單看下 WebServerFactoryCustomizerBeanPostProcessor 中的代碼:

public class WebServerFactoryCustomizerBeanPostProcessor
\t\timplements BeanPostProcessor, BeanFactoryAware {
// 省略部分代碼

// 在 postProcessBeforeInitialization 方法中,如果當前 bean 是 WebServerFactory,則進行
// 一些後置處理
@Override
\tpublic Object postProcessBeforeInitialization(Object bean, String beanName)
\t\t\tthrows BeansException {
\t\tif (bean instanceof WebServerFactory) {
\t\t\tpostProcessBeforeInitialization((WebServerFactory) bean);
\t\t}
\t\treturn bean;
\t}
// 這段代碼就是拿到所有的 Customizers ,然後遍歷調用這些 Customizers 的 customize 方法
private void postProcessBeforeInitialization(WebServerFactory webServerFactory) {
\t\tLambdaSafe
\t\t\t\t.callbacks(WebServerFactoryCustomizer.class, getCustomizers(),
\t\t\t\t\t\twebServerFactory)
\t\t\t\t.withLogger(WebServerFactoryCustomizerBeanPostProcessor.class)
\t\t\t\t.invoke((customizer) -> customizer.customize(webServerFactory));
\t}

// 省略部分代碼
}
複製代碼

自動配置類中註冊的兩個 Customizer Bean

這兩個 Customizer 實際上就是去處理一些配置值,然後綁定到 各自的工廠類的。

WebServerFactoryCustomizer

將 serverProperties 配置值綁定給 ConfigurableServletWebServerFactory 對象實例上。

@Override
public void customize(ConfigurableServletWebServerFactory factory) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
// 端口
map.from(this.serverProperties::getPort).to(factory::setPort);
// address
map.from(this.serverProperties::getAddress).to(factory::setAddress);
// contextPath
map.from(this.serverProperties.getServlet()::getContextPath)
.to(factory::setContextPath);
// displayName
map.from(this.serverProperties.getServlet()::getApplicationDisplayName)
.to(factory::setDisplayName);
// session 配置
map.from(this.serverProperties.getServlet()::getSession).to(factory::setSession);
// ssl
map.from(this.serverProperties::getSsl).to(factory::setSsl);
// jsp
map.from(this.serverProperties.getServlet()::getJsp).to(factory::setJsp);
// 壓縮配置策略實現
map.from(this.serverProperties::getCompression).to(factory::setCompression);
// http2
map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
// serverHeader
map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader);
// contextParameters
map.from(this.serverProperties.getServlet()::getContextParameters)
.to(factory::setInitParameters);
}
複製代碼

TomcatServletWebServerFactoryCustomizer

相比於上面那個,這個 customizer 主要處理 Tomcat 相關的配置值

@Override
public void customize(TomcatServletWebServerFactory factory) {
// 拿到 tomcat 相關的配置
ServerProperties.Tomcat tomcatProperties = this.serverProperties.getTomcat();
// server.tomcat.additional-tld-skip-patterns
if (!ObjectUtils.isEmpty(tomcatProperties.getAdditionalTldSkipPatterns())) {
factory.getTldSkipPatterns()
.addAll(tomcatProperties.getAdditionalTldSkipPatterns());
}
// server.redirectContextRoot
if (tomcatProperties.getRedirectContextRoot() != null) {
customizeRedirectContextRoot(factory,
tomcatProperties.getRedirectContextRoot());
}
// server.useRelativeRedirects
if (tomcatProperties.getUseRelativeRedirects() != null) {
customizeUseRelativeRedirects(factory,
tomcatProperties.getUseRelativeRedirects());
}
}
複製代碼

WebServerFactory

用於創建 WebServer 的工廠的標記接口。

類體系結構

SpringBoot 中內嵌 Tomcat 的實現原理解析,你瞭解嗎?

上圖為 WebServerFactory -> TomcatServletWebServerFactory 的整個類結構關係。

TomcatServletWebServerFactory

TomcatServletWebServerFactory 是用於獲取 Tomcat 作為 WebServer 的工廠類實現,其中最核心的方法就是 getWebServer,獲取一個 WebServer 對象實例。

@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
// 創建一個 Tomcat 實例
Tomcat tomcat = new Tomcat();
// 創建一個 Tomcat 實例工作空間目錄
File baseDir = (this.baseDirectory != null) ? this.baseDirectory
: createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
// 創建連接對象
Connector connector = new Connector(this.protocol);
tomcat.getService().addConnector(connector);
// 1
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
// 配置 Engine,沒有什麼實質性的操作,可忽略
configureEngine(tomcat.getEngine());
// 一些附加鏈接,默認是 0 個
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
// 2
prepareContext(tomcat.getHost(), initializers);
// 返回 webServer
return getTomcatWebServer(tomcat);
}
複製代碼
  • 1、customizeConnector : 給 Connector 設置 port、protocolHandler、uriEncoding 等。Connector 構造的邏輯主要是在NIO和APR選擇中選擇一個協議,然後反射創建實例並強轉為 ProtocolHandler
  • 2、prepareContext 這裡並不是說準備當前 Tomcat 運行環境的上下文信息,而是準備一個 StandardContext ,也就是準備一個 web app。

準備 Web App Context 容器

對於 Tomcat 來說,每個 context 就是映射到 一個 web app 的,所以 prepareContext 做的事情就是將 web 應用映射到一個 TomcatEmbeddedContext ,然後加入到 Host 中。

protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
File documentRoot = getValidDocumentRoot();
// 創建一個 TomcatEmbeddedContext 對象
TomcatEmbeddedContext context = new TomcatEmbeddedContext();
if (documentRoot != null) {
context.setResources(new LoaderHidingResourceRoot(context));
}
// 設置描述此容器的名稱字符串。在屬於特定父項的子容器集內,容器名稱必須唯一。
context.setName(getContextPath());
// 設置此Web應用程序的顯示名稱。
context.setDisplayName(getDisplayName());
// 設置 webContextPath 默認是 /
context.setPath(getContextPath());
File docBase = (documentRoot != null) ? documentRoot
: createTempDir("tomcat-docbase");
context.setDocBase(docBase.getAbsolutePath());
// 註冊一個FixContextListener監聽,這個監聽用於設置context的配置狀態以及是否加入登錄驗證的邏輯
context.addLifecycleListener(new FixContextListener());
// 設置 父 ClassLoader
context.setParentClassLoader(
(this.resourceLoader != null) ? this.resourceLoader.getClassLoader()
: ClassUtils.getDefaultClassLoader());
// 覆蓋Tomcat的默認語言環境映射以與其他服務器對齊。

resetDefaultLocaleMapping(context);
// 添加區域設置編碼映射(請參閱Servlet規範2.4的5.4節)
addLocaleMappings(context);
// 設置是否使用相對地址重定向
context.setUseRelativeRedirects(false);
try {
context.setCreateUploadTargets(true);
}
catch (NoSuchMethodError ex) {
// Tomcat is < 8.5.39. Continue.
}
configureTldSkipPatterns(context);
// 設置 WebappLoader ,並且將 父 classLoader 作為構建參數
WebappLoader loader = new WebappLoader(context.getParentClassLoader());
// 設置 WebappLoader 的 loaderClass 值
loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName());
// 會將加載類向上委託
loader.setDelegate(true);
context.setLoader(loader);
if (isRegisterDefaultServlet()) {
addDefaultServlet(context);
}
// 是否註冊 jspServlet
if (shouldRegisterJspServlet()) {
addJspServlet(context);
addJasperInitializer(context);
}
context.addLifecycleListener(new StaticResourceConfigurer(context));
ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
// 在 host 中 加入一個 context 容器
// add時給context註冊了個內存洩漏跟蹤的監聽MemoryLeakTrackingListener,詳見 addChild 方法
host.addChild(context);
//對context做了些設置工作,包括TomcatStarter(實例化並set給context),
// LifecycleListener,contextValue,errorpage,Mime,session超時持久化等以及一些自定義工作
configureContext(context, initializersToUse);
// postProcessContext 方法是空的,留給子類重寫用的
postProcessContext(context);

}
複製代碼

從上面可以看下,WebappLoader 可以通過 setLoaderClass 和 getLoaderClass 這兩個方法可以更改loaderClass 的值。所以也就意味著,我們可以自己定義一個繼承 webappClassLoader 的類,來更換系統自帶的默認實現。

初始化 TomcatWebServer

在 getWebServer 方法的最後就是構建一個 TomcatWebServer。

// org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
// new 一個 TomcatWebServer
return new TomcatWebServer(tomcat, getPort() >= 0);
}
// org.springframework.boot.web.embedded.tomcat.TomcatWebServer
public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
// 初始化
initialize();
}
複製代碼

這裡主要是 initialize 這個方法,這個方法中將會啟動 tomcat 服務

private void initialize() throws WebServerException {
logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
synchronized (this.monitor) {
try {
// 對全局原子變量 containerCounter+1,由於初始值是-1,
// 所以 addInstanceIdToEngineName 方法內後續的獲取引擎並設置名字的邏輯不會執行
addInstanceIdToEngineName();

\t\t\t// 獲取 Context
Context context = findContext();
// 給 Context 對象實例生命週期監聽器
context.addLifecycleListener((event) -> {
if (context.equals(event.getSource())
&& Lifecycle.START_EVENT.equals(event.getType())) {
// 將上面new的connection以service(這裡是StandardService[Tomcat])做key保存到
// serviceConnectors中,並將 StandardService 中的connectors 與 service 解綁(connector.setService((Service)null);),
// 解綁後下面利用LifecycleBase啟動容器就不會啟動到Connector了
removeServiceConnectors();
}
});
// 啟動服務器以觸發初始化監聽器
this.tomcat.start();
// 這個方法檢查初始化過程中的異常,如果有直接在主線程拋出,
// 檢查方法是TomcatStarter中的 startUpException,這個值是在 Context 啟動過程中記錄的
rethrowDeferredStartupExceptions();
try {
// 綁定命名的上下文和classloader,
ContextBindings.bindClassLoader(context, context.getNamingToken(),
getClass().getClassLoader());
}
catch (NamingException ex) {
// 設置失敗不需要關心
}
\t\t\t// :與Jetty不同,Tomcat所有的線程都是守護線程,所以創建一個非守護線程
// (例:Thread[container-0,5,main])來避免服務到這就shutdown了
startDaemonAwaitThread();
}
catch (Exception ex) {
stopSilently();
throw new WebServerException("Unable to start embedded Tomcat", ex);
}

}
}
複製代碼

查找 Context ,實際上就是查找一個Tomcat 中的一個 web 應用,SpringBoot 中默認啟動一個 Tomcat ,並且一個 Tomcat 中只有一個 Web 應用(FATJAR 模式下,應用與 Tomcat 是 1:1 關係),所有在遍歷 Host 下的 Container 時,如果 Container 類型是 Context ,就直接返回了。

private Context findContext() {
for (Container child : this.tomcat.getHost().findChildren()) {
if (child instanceof Context) {
return (Context) child;
}
}
throw new IllegalStateException("The host does not contain a Context");
}
複製代碼

Tomcat 啟動過程

在 TomcatWebServer 的 initialize 方法中會執行 tomcat 的啟動。

// Start the server to trigger initialization listeners
this.tomcat.start();
複製代碼

org.apache.catalina.startup.Tomcat 的 start 方法:

public void start() throws LifecycleException {
// 初始化 server
getServer();
// 啟動 server
server.start();
}
複製代碼

初始化 Server

初始化 server 實際上就是構建一個 StandardServer 對象實例,關於 Tomcat 中的 Server 可以參考附件中的說明。

public Server getServer() {
\t// 如果已經存在的話就直接返回
if (server != null) {
return server;
}
\t// 設置系統屬性 catalina.useNaming
System.setProperty("catalina.useNaming", "false");
\t// 直接 new 一個 StandardServer
server = new StandardServer();
\t// 初始化 baseDir (catalina.base、catalina.home、 ~/tomcat.{port})
initBaseDir();
// Set configuration source
ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(new File(basedir), null));
server.setPort( -1 );
Service service = new StandardService();
service.setName("Tomcat");
server.addService(service);
return server;
}
複製代碼

總結一下

上面對 SpringBoot 中內嵌 Tomcat 的過程做了分析,這個過程實際上並不複雜,就是在刷新 Spring 上下文的過程中將 Tomcat 容器啟動起來,並且將當前應用綁定到一個 Context ,然後添加了 Host。下圖是程序的執行堆棧和執行內嵌 Tomcat 初始化和啟動的時機。

SpringBoot 中內嵌 Tomcat 的實現原理解析,你瞭解嗎?

下面總結下整個過程:

  • 通過自定配置註冊相關的 Bean ,包括一些 Factory 和 後置處理器等
  • 上下文刷新階段,執行創建 WebServer,這裡需要用到前一個階段所註冊的 Bean
  • 包括創建 ServletContext
  • 實例化 webServer
  • 創建 Tomcat 實例、創建 Connector 連接器
  • 綁定 應用到 ServletContext,並添加相關的生命週期範疇內的監聽器,然後將 Context 添加到 host 中
  • 實例化 webServer 並且啟動 Tomcat 服務

SpringBoot 的 Fatjar 方式沒有提供共享 Tomcat 的實現邏輯,就是兩個 FATJAT 啟動可以只實例化一個 Tomcat 實例(包括 Connector 和 Host ),從前面的分析知道,每個 web 應用(一個 FATJAT 對應的應用)實例上就是映射到一個 Context ;而對於 war 方式,一個 Host 下面是可以掛載多個 Context 的。

附:Tomcat 組件說明

組件名稱 說明 Server 表示整個Servlet 容器,因此 Tomcat 運行環境中只有唯一一個 Server 實例 Service Service 表示一個或者多個 Connector 的集合,這些 Connector 共享同一個 Container 來處理其請求。在同一個 Tomcat 實例內可以包含任意多個 Service 實例,他們彼此獨立。 Connector Tomcat 連接器,用於監聽和轉化 Socket 請求,同時將讀取的 Socket 請求交由 Container 處理,支持不同協議以及不同的 I/O 方式。 Container Container 表示能夠執行客戶端請求並返回響應的一類對象,在 Tomcat 中存在不同級別的容器:Engine、Host、Context、Wrapper Engine Engine 表示整個 Servlet 引擎。在 Tomcat 中,Engine 為最高層級的容器對象,雖然 Engine 不是直接處理請求的容器,確是獲取目標容器的入口 Host Host 作為一類容器,表示 Servlet 引擎(即Engine)中的虛擬機,與一個服務器的網絡名有關,如域名等。客戶端可以使用這個網絡名連接服務器,這個名稱必須要在 DNS 服務器上註冊 Context Context 作為一類容器,用於表示 ServletContext,在 Servlet 規範中,一個 ServletContext 即表示一個獨立的 web 應用 Wrapper Wrapper 作為一類容器,用於表示 Web 應用中定義的 Servlet Executor 表示 Tomcat 組件間可以共享的線程池

小結

私信小編髮送“架構”(免費獲取SpringBoo資料以及JAVA相關的面試架構資料喲)

最後,每一位讀到這裡的Java程序猿朋友們,感謝你們能耐心地看完。希望在成為一名更優秀的Java程序猿的道路上,我們可以一起學習、一起進步!都能贏取白富美,走向架構師的人生巔峰!


分享到:


相關文章: