Spring Boot中Tomcat是怎麼啟動的

程式設計師自由之路發表於2020-07-14

Spring Boot一個非常突出的優點就是不需要我們額外再部署Servlet容器,它內建了多種容器的支援。我們可以通過配置來指定我們需要的容器。

本文以我們平時最常使用的容器Tomcat為列來介紹以下兩個知識點:

  • Spring Boot是怎麼整合啟動Tomcat容器的;
  • 在Spring Boot中,怎麼進行Tomcat的深度配置。

Spring Boot整合啟動Tomcat的流程

對於看原始碼,每個人都有自己的方法。我自己在看原始碼的時候喜歡結合IDEA的Debug功能一起看。比如說現在我們要研究Spring Boot是在哪個環節點啟動Tomcat的,
我的思路是:Tomcat在啟動時會呼叫各個元件的init方法和start方法,那麼我只需要在這些方法上打上端點,然後就能在呼叫棧上看出Spring Boot是在哪個環節點啟用
Tomcat的了。

按照這個思路,我在Tomcat的Connector元件的init方法上打了端點,通過呼叫棧能很清楚的看出Spring Boot是在容器的onRefresh方法中呼叫Tomcat的。

protected void onRefresh() {
    super.onRefresh();
    try {
        this.createWebServer();
    } catch (Throwable var2) {
        throw new ApplicationContextException("Unable to start web server", var2);
    }
}

找到了呼叫點,那麼一切都好辦了。從上面的方法中可以看出,重點內容就在this.createWebServer()這個方法中。

在Spring Boot中使用的容器類是ServletWebServerApplicationContext系列的容器,這個系列的容器可以內嵌Web容器。這個我們
可以從這個容器的屬性和方法中可以看出來。

public class ServletWebServerApplicationContext extends GenericWebApplicationContext implements ConfigurableWebServerApplicationContext {
    //...省略部分程式碼
    public static final String DISPATCHER_SERVLET_NAME = "dispatcherServlet";
    //內嵌容器
    private volatile WebServer webServer;
    private ServletConfig servletConfig;
   
    //...省略部分程式碼
    //建立Web容器
    private void createWebServer() {
        WebServer webServer = this.webServer;
        ServletContext servletContext = this.getServletContext();
        //webServer和servletContext都是null,表示還沒建立容器,進入建立容器的邏輯
        if (webServer == null && servletContext == null) {
            //獲取建立容器的工廠,可以通過WebServerFactoryCustomizer介面對這個工廠進行自定義設定
            ServletWebServerFactory factory = this.getWebServerFactory();
            //具體的建立容器的方法,我們進去具體看下
            this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
        } else if (servletContext != null) {
            try {
                this.getSelfInitializer().onStartup(servletContext);
            } catch (ServletException var4) {
                throw new ApplicationContextException("Cannot initialize servlet context", var4);
            }
        }
        this.initPropertySources();
    }
}

下面是TomcatServletWebServerFactory的getWebServer方法。

public class TomcatServletWebServerFactory的getWebServer{
//...

@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
    //建立Tomcat容器
    Tomcat tomcat = new Tomcat();
    File baseDir = (this.baseDirectory != null ? this.baseDirectory
            : createTempDir("tomcat"));
    tomcat.setBaseDir(baseDir.getAbsolutePath());
    //建立聯結器,預設NIO模式,可以通過WebServerFactoryCustomizer改變具體模式
    Connector connector = new Connector(this.protocol);
    tomcat.getService().addConnector(connector);
    //自定義聯結器
    customizeConnector(connector);
    tomcat.setConnector(connector);
    tomcat.getHost().setAutoDeploy(false);
    configureEngine(tomcat.getEngine());
    //可以通過WebServerFactoryCustomizer新增額外的聯結器,這邊將這些聯結器繫結到Tomcat
    for (Connector additionalConnector : this.additionalTomcatConnectors) {
        tomcat.getService().addConnector(additionalConnector);
    }
    //組測Listener、Filter和Servlet,自定義Context等操作
    //這個方法可以重點看下
    prepareContext(tomcat.getHost(), initializers);
    //建立TomcatWebServer,並呼叫start方法
    return getTomcatWebServer(tomcat);
}

//內嵌的Tomcat容器
public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
		Assert.notNull(tomcat, "Tomcat Server must not be null");
		this.tomcat = tomcat;
		this.autoStart = autoStart;
        //這邊觸發Tomcat的啟動流程,是Tomcat啟動的入口點
		initialize();
}
//...省略部分程式碼
}

至此Spring Boot內嵌的Tomcat已將順序啟動了。那麼Spring Boot是在什麼時候註冊DispatchServlet的呢?

配置Listener、Filter和Servlet

Spring Boot配置Listener、Filter和Servlet可以參考我之前寫的文章Spring Boot使用嵌入式容器,那怎麼配置自定義Filter呢

推薦使用ServletListenerRegistrationBean、FilterRegistrationBean和ServletRegistrationBean的方式註冊Listener、Filter和Servlet。

Spring Boot註冊DispatcherServlet

在傳統的Spring MVC專案中,我們都會在web.xml中註冊DispatcherServlet這個入口類,那麼在Spring Boot中是在哪裡註冊的呢?

大家如果看Spring Boot的原始碼,這邊有個小技巧大家可以參考下。就是Spring Boot把之前傳統專案中的配置項都通過AutoConfig的形式
做配置了。所以這邊在尋找DispatcherServlet是在哪裡配置的也可以順著這個思路去尋找。

在IDEA的類查詢功能中輸入DispatcherServlet關鍵字,我們能看到一個DispatcherServletAutoConfiguration類。從名字上就能看出這個
類是DispatcherServlet的自動配置類,我們點進去看下是否是在這個類內部註冊的DispatcherServlet?

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
@EnableConfigurationProperties(ServerProperties.class)
public class DispatcherServletAutoConfiguration {

	/*
	 * The bean name for a DispatcherServlet that will be mapped to the root URL "/"
	 */
	public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";

	/*
	 * The bean name for a ServletRegistrationBean for the DispatcherServlet "/"
	 */
	public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";

	@Configuration
	@Conditional(DefaultDispatcherServletCondition.class)
	@ConditionalOnClass(ServletRegistration.class)
	@EnableConfigurationProperties(WebMvcProperties.class)
	protected static class DispatcherServletConfiguration {

		private final WebMvcProperties webMvcProperties;

		public DispatcherServletConfiguration(WebMvcProperties webMvcProperties) {
			this.webMvcProperties = webMvcProperties;
		}

		@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
		public DispatcherServlet dispatcherServlet() {
			DispatcherServlet dispatcherServlet = new DispatcherServlet();
			dispatcherServlet.setDispatchOptionsRequest(
					this.webMvcProperties.isDispatchOptionsRequest());
			dispatcherServlet.setDispatchTraceRequest(
					this.webMvcProperties.isDispatchTraceRequest());
			dispatcherServlet.setThrowExceptionIfNoHandlerFound(
					this.webMvcProperties.isThrowExceptionIfNoHandlerFound());
			return dispatcherServlet;
		}

		@Bean
		@ConditionalOnBean(MultipartResolver.class)
		@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
		public MultipartResolver multipartResolver(MultipartResolver resolver) {
			// Detect if the user has created a MultipartResolver but named it incorrectly
			return resolver;
		}

	}

	@Configuration
	@Conditional(DispatcherServletRegistrationCondition.class)
	@ConditionalOnClass(ServletRegistration.class)
	@EnableConfigurationProperties(WebMvcProperties.class)
	@Import(DispatcherServletConfiguration.class)
	protected static class DispatcherServletRegistrationConfiguration {

		private final ServerProperties serverProperties;

		private final WebMvcProperties webMvcProperties;

		private final MultipartConfigElement multipartConfig;

		public DispatcherServletRegistrationConfiguration(
				ServerProperties serverProperties, WebMvcProperties webMvcProperties,
				ObjectProvider<MultipartConfigElement> multipartConfigProvider) {
			this.serverProperties = serverProperties;
			this.webMvcProperties = webMvcProperties;
			this.multipartConfig = multipartConfigProvider.getIfAvailable();
		}

        //很熟悉的程式碼有沒有,ServletRegistrationBean就是我們上一節中介紹的註冊Servlet的方式
        //只不過這邊註冊的是DispatcherServlet這個特殊的Servlet
		@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
		@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
		public ServletRegistrationBean<DispatcherServlet> dispatcherServletRegistration(
				DispatcherServlet dispatcherServlet) {
			ServletRegistrationBean<DispatcherServlet> registration = new ServletRegistrationBean<>(
					dispatcherServlet,
					this.serverProperties.getServlet().getServletMapping());
			registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
			registration.setLoadOnStartup(
					this.webMvcProperties.getServlet().getLoadOnStartup());
			if (this.multipartConfig != null) {
				registration.setMultipartConfig(this.multipartConfig);
			}
			return registration;
		}

	}

//...省略部分程式碼
}

好了通過這邊的介紹,我們直到DispatcherServlet是通過DispatcherServletAutoConfiguration這個自動配置類註冊的。

Spring Boot中關於Tomcat的一些其他配置

server.tomcat.accept-count 100.0 Maximum queue length for incoming connection requests when all possible request processing threads are in use.(backlog的長度)
server.tomcat.accesslog.buffered true Whether to buffer output such that it is flushed only periodically.
server.tomcat.accesslog.check-exists false Whether to check for log file existence so it can be recreated it if an external process has renamed it.
server.tomcat.accesslog.condition-if Whether logging of the request will only be enabled if "ServletRequest.getAttribute(conditionIf)" does not yield null.
server.tomcat.accesslog.condition-unless Whether logging of the request will only be enabled if "ServletRequest.getAttribute(conditionUnless)" yield null.
server.tomcat.accesslog.directory logs Directory in which log files are created. Can be absolute or relative to the Tomcat base dir.
server.tomcat.accesslog.enabled false Enable access log.
server.tomcat.accesslog.encoding Character set used by the log file. Default to the system default character set.
server.tomcat.accesslog.file-date-format .yyyy-MM-dd Date format to place in the log file name.
server.tomcat.accesslog.ipv6-canonical false Whether to use IPv6 canonical representation format as defined by RFC 5952.
server.tomcat.accesslog.locale Locale used to format timestamps in log entries and in log file name suffix. Default to the default locale of the Java process.
server.tomcat.accesslog.max-days -1.0 Number of days to retain the access log files before they are removed.
server.tomcat.accesslog.pattern common Format pattern for access logs.
server.tomcat.accesslog.prefix access_log Log file name prefix.
server.tomcat.accesslog.rename-on-rotate false Whether to defer inclusion of the date stamp in the file name until rotate time.
server.tomcat.accesslog.request-attributes-enabled false Set request attributes for the IP address, Hostname, protocol, and port used for the request.
server.tomcat.accesslog.rotate true Whether to enable access log rotation.
server.tomcat.accesslog.suffix .log Log file name suffix.
server.tomcat.additional-tld-skip-patterns Comma-separated list of additional patterns that match jars to ignore for TLD scanning. The special '?' and '*' characters can be used in the pattern to match one and only one character and zero or more characters respectively.
server.tomcat.background-processor-delay 10s Delay between the invocation of backgroundProcess methods. If a duration suffix is not specified, seconds will be used.
server.tomcat.basedir Tomcat base directory. If not specified, a temporary directory is used.
server.tomcat.connection-timeout Amount of time the connector will wait, after accepting a connection, for the request URI line to be presented.
server.tomcat.max-connections 8192.0 Maximum number of connections that the server accepts and processes at any given time. Once the limit has been reached, the operating system may still accept connections based on the "acceptCount" property.
server.tomcat.max-http-form-post-size 2MB Maximum size of the form content in any HTTP post request.
server.tomcat.max-swallow-size 2MB Maximum amount of request body to swallow.
server.tomcat.mbeanregistry.enabled false Whether Tomcat's MBean Registry should be enabled.
server.tomcat.processor-cache 200.0 Maximum number of idle processors that will be retained in the cache and reused with a subsequent request. When set to -1 the cache will be unlimited with a theoretical maximum size equal to the maximum number of connections.
server.tomcat.redirect-context-root true Whether requests to the context root should be redirected by appending a / to the path.
server.tomcat.relaxed-path-chars Comma-separated list of additional unencoded characters that should be allowed in URI paths. Only "< > [ \ ] ^ ` { | }" are allowed.
server.tomcat.relaxed-query-chars Comma-separated list of additional unencoded characters that should be allowed in URI query strings. Only "< > [ \ ] ^ ` { | }" are allowed.
server.tomcat.remoteip.host-header X-Forwarded-Host Name of the HTTP header from which the remote host is extracted.
server.tomcat.remoteip.internal-proxies Regular expression that matches proxies that are to be trusted.
server.tomcat.remoteip.port-header X-Forwarded-Port Name of the HTTP header used to override the original port value.
server.tomcat.remoteip.protocol-header Header that holds the incoming protocol, usually named "X-Forwarded-Proto".
server.tomcat.remoteip.protocol-header-https-value https Value of the protocol header indicating whether the incoming request uses SSL.
server.tomcat.remoteip.remote-ip-header Name of the HTTP header from which the remote IP is extracted. For instance, X-FORWARDED-FOR.
server.tomcat.resource.allow-caching true Whether static resource caching is permitted for this web application.
server.tomcat.resource.cache-ttl Time-to-live of the static resource cache.
server.tomcat.threads.max 200.0 Maximum amount of worker threads.
server.tomcat.threads.min-spare 10.0 Minimum amount of worker threads.
server.tomcat.uri-encoding UTF-8 Character encoding to use to decode the URI.
server.tomcat.use-relative-redirects false Whether HTTP 1.1 and later location headers generated by a call to sendRedirect will use relative or absolute redirects.

這邊給出一個配置的列子

server:
  port: ${port:9999}
  tomcat:
    accept-count: 200
    #最好進行這段配置,預設會在tmp目錄下建立,Linux有時會有定時任務刪除tmp目錄下的內容
    basedir: my-tomcat
    accesslog:
      enabled: true
      pattern: '%t %a "%r" %s %S (%b M) (%D ms)'
      max-http-post-size: 2MB
      max-swallow-size: 2M
    uri-encoding: GBK
    threads:
      max: 100
      min-spare: 10

具體使用時可以參考Spring Boo官閘道器於Tomcat的配置

一些其他類

Spring Boot還提供了很多自定義類,讓使用者對Tomcat的元件做自定義配置。這個符合Spring的設計哲學:只提供選擇,而不是強制使用者使用某項技術。

關於Tomcat的自定義配置類還有以下幾個,大家可以按需使用。

  • WebServerFactoryCustomizer介面:自定義Web容易工廠
  • WebServerFactoryCustomizerBeanPostProcessor處理類:WebServerFactoryCustomizer類通過WebServerFactoryCustomizerBeanPostProcessor類生效
  • TomcatConnectorCustomizer:聯結器自定義處理類
  • TomcatContextCustomizer:Context自定義介面

相關文章