SpringBoot 中內嵌 Tomcat 的實現原理解析

原文連接: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

WebServer 自動配置

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

ServletWebServerFactoryAutoConfiguration

代碼片斷以下: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

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

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

經過 @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);
}
複製代碼
  • 一、customizeConnector : 給 Connector 設置 port、protocolHandler、uriEncoding 等。Connector 構造的邏輯主要是在NIO和APR選擇中選擇一個協議,而後反射建立實例並強轉爲 ProtocolHandler
  • 二、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 組件間能夠共享的線程池
相關文章
相關標籤/搜索