天天看點

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 的工廠的标記接口。

類體系結構

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();
			// 擷取 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 初始化和啟動的時機。

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 元件間可以共享的線程池

▐ 文章來源:磊叔,轉載請事先告知,謝謝!