1. 程式人生 > 程式設計 >SpringBoot 系列-內嵌 Tomcat 的實現原理解析

SpringBoot 系列-內嵌 Tomcat 的實現原理解析

原文連結:SpringBoot 中內嵌 Tomcat 的實現原理解析

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

<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 ,可以自行參考官方檔案來解決。

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,ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,ServletWebServerFactoryConfiguration.EmbeddedJetty.class,ServletWebServerFactoryConfiguration.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
		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 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 的工廠的標記介面。

類體系結構

上圖為 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();
			// 獲取 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");
}
複製程式碼

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() {
	// 如果已經存在的話就直接返回
    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 初始化和啟動的時機。

下面總結下整個過程:

  • 通過自定配置註冊相關的 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 元件間可以共享的執行緒池