原文連接:SpringBoot 中內嵌 Tomcat 的實現原理解析java
對於一個 SpringBoot web 工程來講,一個主要的依賴標誌就是有 spring-boot-starter-web 這個 starter ,spring-boot-starter-web 模塊在 spring boot 中其實並無代碼存在,只是在 pom.xml 中攜帶了一些依賴,包括 web、webmvc、tomcat 等:web
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
</dependencies>
複製代碼
Spring Boot 默認的 web 服務容器是 tomcat ,若是想使用 Jetty 等來替換 Tomcat ,能夠自行參考官方文檔來解決。spring
web、webmvc、tomcat 等提供了 web 應用的運行環境,那 spring-boot-starter 則是讓這些運行環境工做的開關(由於 spring-boot-starter 中會間接引入 spring-boot-autoconfigure )。apache
在 spring-boot-autoconfigure 模塊中,有處理關於 WebServer 的自動配置類 ServletWebServerFactoryAutoConfiguration 。json
代碼片斷以下:api
@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration 複製代碼
兩個 Condition 表示當前運行環境是基於 servlet 標準規範的 web 服務:tomcat
@EnableConfigurationProperties(ServerProperties.class):ServerProperties 配置中包括了常見的 server.port 等配置屬性。springboot
經過 @Import 導入嵌入式容器相關的自動配置類,有 EmbeddedTomcat、EmbeddedJetty 和EmbeddedUndertow。服務器
綜合來看,ServletWebServerFactoryAutoConfiguration 自動配置類中主要作了如下幾件事情:網絡
下面就針對這幾個點,作下詳細的分析。
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 中的代碼:
public class WebServerFactoryCustomizerBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {
// 省略部分代碼
// 在 postProcessBeforeInitialization 方法中,若是當前 bean 是 WebServerFactory,則進行
// 一些後置處理
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebServerFactory) {
postProcessBeforeInitialization((WebServerFactory) bean);
}
return bean;
}
// 這段代碼就是拿到全部的 Customizers ,而後遍歷調用這些 Customizers 的 customize 方法
private void postProcessBeforeInitialization(WebServerFactory webServerFactory) {
LambdaSafe
.callbacks(WebServerFactoryCustomizer.class, getCustomizers(),
webServerFactory)
.withLogger(WebServerFactoryCustomizerBeanPostProcessor.class)
.invoke((customizer) -> customizer.customize(webServerFactory));
}
// 省略部分代碼
}
複製代碼
這兩個 Customizer 實際上就是去處理一些配置值,而後綁定到 各自的工廠類的。
將 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);
}
複製代碼
相比於上面那個,這個 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());
}
}
複製代碼
用於建立 WebServer 的工廠的標記接口。
上圖爲 WebServerFactory -> 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);
}
複製代碼
對於 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 的類,來更換系統自帶的默認實現。
在 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();
// 獲取 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) {
// 設置失敗不須要關心
}
// :與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");
}
複製代碼
在 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 實際上就是構建一個 StandardServer 對象實例,關於 Tomcat 中的 Server 能夠參考附件中的說明。
public Server getServer() {
// 若是已經存在的話就直接返回
if (server != null) {
return server;
}
// 設置系統屬性 catalina.useNaming
System.setProperty("catalina.useNaming", "false");
// 直接 new 一個 StandardServer
server = new StandardServer();
// 初始化 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 的 Fatjar 方式沒有提供共享 Tomcat 的實現邏輯,就是兩個 FATJAT 啓動能夠只實例化一個 Tomcat 實例(包括 Connector 和 Host ),從前面的分析知道,每一個 web 應用(一個 FATJAT 對應的應用)實例上就是映射到一個 Context ;而對於 war 方式,一個 Host 下面是能夠掛載多個 Context 的。
組件名稱 | 說明 |
---|---|
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 組件間能夠共享的線程池 |