SpringBoot启动Tomcat原理与嵌入式Tomcat实践

导读

作为一个开发,使用Spring Boot 时,和传统的Tomcat 部署相比,我们只需要关注业务的开发,项目的启动和部署变的十分简单, 那么它背后是怎么实现的, 隐藏着什么? 本文先从一个嵌入式Tomcat的应用开发,再到Spring Boot的集成进行分解实践,由浅到深, 希望能你有所收获。 那么请系好安全带,打卡上车, 一起领略被忽略的风景。

嵌入式Tomcat使用

我们在看Spring Boot 之前先看下嵌入式Tomcat是怎么进行独立开发的。

目录结构

  • EmbedStarter 为启动类
  • HelloServlet 自定义的Servlet
  • TestServlet 自定义的Servlet
  • resources 资源目录, 分别放置了日志的配置和一个jsp页面

项目结构

EmbedStarter 启动类

注意这里没有webapp目录,也没有所谓的web.xml,当然我们可以这么做;这里没这么做的,在开发Spring Boot应用时我们也没有这么配置。
作为配置文件那么最终一定会被程序读取最终变为配置类,那么这里就是通过这样的方式来达成这个目的,参考EmbedStarter的addServlet方法,代码配置和xml配置是等同的。
如果要配置web.xml, 那么应该是这样:

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <servlet>
        <servlet-name>helloServlet</servlet-name>
        <servlet-class>com.cx.servlet.HelloServlet</servlet-class>
        <init-param>
            <param-name>name</param-name>
            <param-value>chengjz</param-value>
        </init-param>
        <init-param>
            <param-name>sex</param-name>
            <param-value>boy</param-value>
        </init-param>
        <init-param>
            <param-name>address</param-name>
            <param-value>shanghai</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>helloServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
</web-app>

通过代码配置来省略web.xml, 看起来简洁很多:

	public static Wrapper addServlet(Context ctx) {
		final Wrapper servlet = Tomcat.addServlet(ctx, "helloServlet", HelloServlet.class.getName());
		servlet.addInitParameter("name", "chengjz");
		servlet.addInitParameter("sex", "boy");
		servlet.addInitParameter("address", "shanghai");
		ctx.addServletMappingDecoded("/hello", "helloServlet");
		return servlet;
	}

完整代码,有详细的注释:

package com.cx;

import com.cx.servlet.HelloServlet;
import com.cx.servlet.TestServlet;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.*;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;

import javax.servlet.ServletRegistration.Dynamic;
import java.io.File;
import java.util.Collections;

/**
 * 嵌入式tomcat 启动类, 启动后访问:
 * <a href="http://127.0.0.1:8080/"> 首页 </a>
 * <a href="http://127.0.0.1:8080/hello"> hello servlet </a>
 * <a href="http://127.0.0.1:8080/test"> test servlet </a>
 *
 * @author chengjz
 * @version 1.0
 * @since 2021-01-04 16:33
 */
@Slf4j
public class EmbedStarter {
	public static void main(String[] args) throws Exception {
		// 项目目录
		// 获取当前类启动路径
		String projectDir = System.getProperty("user.dir") + File.separator + "embed-tomcat";
		// Tomcat 应用存放的目录,JSP编译会放在这个目录。
		String tomcatBaseDir = projectDir + File.separatorChar + "tomcat";
		// 项目部署目录,我们这里需要设置为 $userDir$/target/classes 目录,因为项目编译的文件都会存到改目录下。
		String webappDir = projectDir + File.separatorChar + "target" + File.separatorChar + "classes";

		Tomcat tomcat = new Tomcat();
		tomcat.setBaseDir(tomcatBaseDir);

		Connector connector = new Connector();
		// 端口号
		connector.setPort(8080);
		connector.setURIEncoding("UTF-8");
		// 创建服务
		final Service service = tomcat.getService();
		service.addConnector(connector);

		/**
		 * addDefaultWebXmlToWebapp 默认情况下就是true,
		 * {@link Tomcat#addWebapp(Host, String, String, LifecycleListener)} 会根据这个参数添加默认web.xml配置。
		 * 默认会配置default servlet 和  jsp servlet以及其他参数 {@link Tomcat#initWebappDefaults(Context)}
		 */
		tomcat.setAddDefaultWebXmlToWebapp(true);

		// addWebapp(getHost(), contextPath, docBase);  重载方法getHost()也是一个实现了生命周期接口的监听器
		// 注意 Context 这里添加了默认的servlet, 这里是通过DefaultWebXmlListener添加的
		final Context context = tomcat.addWebapp("/", webappDir);
		context.addLifecycleListener(event -> log.info("自定义监听器: {}", event.getType()));

		// servlet 3.0方式添加TestServlet, spring boot 使用的就是这种方式
		context.addServletContainerInitializer((c, ctx) -> {
			log.warn("servlet 3.0方式添加TestServlet");
			final Dynamic dynamic = ctx.addServlet("test", new TestServlet());
			dynamic.addMapping("/test");
			dynamic.setInitParameter("aaa", "aaa");
		}, Collections.emptySet());

		// 监听器方式添加自定义的HelloServlet
		context.addLifecycleListener(event -> {
			if (Lifecycle.BEFORE_START_EVENT.equals(event.getType())) {
				log.warn(" 监听器方式添加自定义的HelloServlet");
				addServlet((Context) event.getLifecycle());
			}
		});
		tomcat.start();
		tomcat.getServer().await();
	}

	/**
	 * 等同的web.xml里的配置
	 * <p>
	 * <pre>
	 *         {@code
	 *         <!DOCTYPE web-app PUBLIC
	 *         "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
	 *         "http://java.sun.com/dtd/web-app_2_3.dtd" >
	 *
	 * <web-app>
	 *     <servlet>
	 *         <servlet-name>helloServlet</servlet-name>
	 *         <servlet-class>com.cx.servlet.HelloServlet</servlet-class>
	 *         <init-param>
	 *             <param-name>name</param-name>
	 *             <param-value>chengjz</param-value>
	 *         </init-param>
	 *         <init-param>
	 *             <param-name>sex</param-name>
	 *             <param-value>boy</param-value>
	 *         </init-param>
	 *         <init-param>
	 *             <param-name>address</param-name>
	 *             <param-value>shanghai</param-value>
	 *         </init-param>
	 *     </servlet>
	 *     <servlet-mapping>
	 *         <servlet-name>helloServlet</servlet-name>
	 *         <url-pattern>/hello</url-pattern>
	 *     </servlet-mapping>
	 * </web-app>
	 *         }
	 *     </pre>
	 * </p>
	 *
	 * @param ctx 上下文
	 * @return servlet
	 */
	public static Wrapper addServlet(Context ctx) {
		final Wrapper servlet = Tomcat.addServlet(ctx, "helloServlet", HelloServlet.class.getName());
		servlet.addInitParameter("name", "chengjz");
		servlet.addInitParameter("sex", "boy");
		servlet.addInitParameter("address", "shanghai");
		ctx.addServletMappingDecoded("/hello", "helloServlet");
		return servlet;
	}
}

注意:
这里的tomcat.addWebapp(…)方法返回的Context中添加了一个默认的监听器 DefaultWebXmlListener,可以点击对应方法去查看, 这里添加了default 和 jsp 2个servlet, 因此我们可以处理 “/” 根路径和Jsp页面;同理,我们通过调用addLifecycleListener方法添加了2个监听器, 一个纯打印的监听器, 一个用来添加我们自己Servlet的监听器,和 DefaultWebXmlListener 很像。

自定义Servlet–HelloServlet

Servlet 只会初始化一次,会调用一次init方法, 我们自定义的只做了简单的参数打印和回写一段html代码块。
显示当前是HelloServlet,并且每次请求返回随机生成一个UUID。

package com.cx.servlet;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
import java.util.UUID;

/**
 * @author chengjz
 * @version 1.0
 * @since 2021-01-04 16:30
 */
@Slf4j
public class HelloServlet extends HttpServlet {

	@Override
	public void init(ServletConfig config) throws ServletException {
		final Enumeration<String> parameterNames = config.getInitParameterNames();
		log.info("{} 初始化开始 >>>", this.getClass().getSimpleName());
		StringBuilder sb = new StringBuilder("\n");
		while (parameterNames.hasMoreElements()) {
			final String element = parameterNames.nextElement();
			sb.append(String.format("%s \t %s %n", element, config.getInitParameter(element)));
		}
		log.info(sb.toString());
		log.info("{} 初始化结束 <<<", this.getClass().getSimpleName());
	}

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
		final ServletOutputStream out = resp.getOutputStream();
		resp.setContentType("text/html");
		String html = "<!DOCTYPE html>\n" +
				"<html>\n" +
				"    <head>\n" +
				"        <meta charset=\"UTF-8\">\n" +
				"        <title>HelloServlet</title>\n" +
				"    </head>\n" +
				"    <body>\n" +
				"        <p>\n" +
				"            <h1>Hello World!</h1> \nThis is HelloServlet[" + UUID.randomUUID() + "]. \n" +
				"        </p>\n" +
				"    </body>\n" +
				"</html>";
		out.write(html.getBytes());
	}
}


自定义Servlet–TestServlet

package com.cx.servlet;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
import java.util.UUID;

/**
 * @author chengjz
 * @version 1.0
 * @since 2021-01-04 16:30
 */
@Slf4j
public class TestServlet extends HttpServlet {

	@Override
	public void init(ServletConfig config) throws ServletException {
		final Enumeration<String> parameterNames = config.getInitParameterNames();
		log.info("{} 初始化开始 >>>", this.getClass().getSimpleName());
		StringBuilder sb = new StringBuilder("\n");
		while (parameterNames.hasMoreElements()) {
			final String element = parameterNames.nextElement();
			sb.append(String.format("%s \t %s %n", element, config.getInitParameter(element)));
		}
		log.info(sb.toString());
		log.info("{} 初始化结束 <<<", this.getClass().getSimpleName());
	}

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
		final ServletOutputStream out = resp.getOutputStream();
		resp.setContentType("text/html");
		String html = "<!DOCTYPE html>\n" +
				"<html>\n" +
				"    <head>\n" +
				"        <meta charset=\"UTF-8\">\n" +
				"        <title>TestServlet</title>\n" +
				"    </head>\n" +
				"    <body>\n" +
				"        <p>\n" +
				"            <h1>Hello World!</h1> \nThis is TestServlet[" + UUID.randomUUID() + "]. \n" +
				"        </p>\n" +
				"    </body>\n" +
				"</html>";
		out.write(html.getBytes());
	}
}

index.jsp

默认的首页,显示当前时间和Tomcat的版本

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>首页</title>
    </head>
    <body>
        <p>
            Hello World! Time is<%= new java.util.Date() %>
        </p>
        <p>
            We are running on<%= application.getServerInfo() %>!!!
        </p>
    </body>
</html>

启动

我们看到自定义监听器会不停打印事件名称,具体含义大家可以自行了解, 看到"监听器方式添加自定义的HelloServlet"和"servlet 3.0方式添加TestServlet"2句日志, 说明我们自己加的HelloServlet、TestServletd都添加进去了。

一月 05, 2021 5:33:18 下午 org.apache.catalina.core.StandardContext setPath
警告: A context path must either be an empty string or start with a '/' and do not end with a '/'. The path [/] does not meet these criteria and has been changed to []
一月 05, 2021 5:33:19 下午 org.apache.coyote.AbstractProtocol init
信息: Initializing ProtocolHandler ["http-nio-8080"]
一月 05, 2021 5:33:21 下午 org.apache.catalina.core.StandardService startInternal
信息: Starting service [Tomcat]
一月 05, 2021 5:33:21 下午 org.apache.catalina.core.StandardEngine startInternal
信息: Starting Servlet engine: [Apache Tomcat/9.0.37]
[INFO ] 2021-01-05T17:33:21,968 [main] EmbedStarter - 自定义监听器: before_init
[INFO ] 2021-01-05T17:33:21,993 [main] EmbedStarter - 自定义监听器: after_init
[INFO ] 2021-01-05T17:33:22,012 [main] EmbedStarter - 自定义监听器: before_start
[WARN ] 2021-01-05T17:33:22,013 [main] EmbedStarter -  监听器方式添加自定义的HelloServlet
一月 05, 2021 5:33:22 下午 org.apache.catalina.startup.ContextConfig getDefaultWebXmlFragment
信息: No global web.xml found
[INFO ] 2021-01-05T17:33:25,201 [main] EmbedStarter - 自定义监听器: configure_start
[WARN ] 2021-01-05T17:33:25,219 [main] EmbedStarter - servlet 3.0方式添加TestServlet
一月 05, 2021 5:33:25 下午 org.apache.jasper.servlet.TldScanner scanJars
信息: At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.
[INFO ] 2021-01-05T17:33:25,428 [main] EmbedStarter - 自定义监听器: start
[INFO ] 2021-01-05T17:33:25,429 [main] EmbedStarter - 自定义监听器: after_start
一月 05, 2021 5:33:25 下午 org.apache.coyote.AbstractProtocol start
信息: Starting ProtocolHandler ["http-nio-8080"]

访问

  • 访问首页: http://127.0.0.1:8080/
    首页
  • 访问HelloServlet: http://127.0.0.1:8080/hello
    HelloServlet
    后台日志:
[INFO ] 2021-01-05T17:33:46,812 [http-nio-8080-exec-2] HelloServlet - HelloServlet 初始化开始 >>>
[INFO ] 2021-01-05T17:33:46,814 [http-nio-8080-exec-2] HelloServlet - 
address 	 shanghai 
sex 	 boy 
name 	 chengjz 

[INFO ] 2021-01-05T17:33:46,815 [http-nio-8080-exec-2] HelloServlet - HelloServlet 初始化结束 <<<
  • 访问TestServlet: http://127.0.0.1:8080/test
    TestServlet
    后台日志:
[INFO ] 2021-01-05T17:33:50,225 [http-nio-8080-exec-3] TestServlet - TestServlet 初始化开始 >>>
[INFO ] 2021-01-05T17:33:50,225 [http-nio-8080-exec-3] TestServlet - 
aaa 	 aaa 

[INFO ] 2021-01-05T17:33:50,225 [http-nio-8080-exec-3] TestServlet - TestServlet 初始化结束 <<<

说明我们的程序运行均正常。

总结

使用嵌入式Tomcat时,基本配置比如端口,直接配置即可,Servlet可以按3.0回调的形式配置(spring boot的使用方式),也可以以监听器的形式进行回调来配置,下图是默认情况下Tomcat为我们做的,那推测Spring Boot应该也是这样做的,我们下章节进入Spring Boot分析, 下图是默认的Context配置:

public Context addWebapp(Host host, String contextPath, String docBase,
            LifecycleListener config) {

        silence(host, contextPath);

        Context ctx = createContext(host, contextPath);
        ctx.setPath(contextPath);
        ctx.setDocBase(docBase);

        if (addDefaultWebXmlToWebapp) {
	        // 配置DefaultServlet, JspServlet
            ctx.addLifecycleListener(getDefaultWebXmlListener());
        }
		// 查找并配置其他配置文件
        ctx.setConfigFile(getWebappConfigFile(docBase, contextPath));

        ctx.addLifecycleListener(config);

        if (addDefaultWebXmlToWebapp && (config instanceof ContextConfig)) {
            // prevent it from looking ( if it finds one - it'll have dup error )
            ((ContextConfig) config).setDefaultWebXml(noDefaultWebXmlPath());
        }

        if (host == null) {
            getHost().addChild(ctx);
        } else {
            host.addChild(ctx);
        }

        return ctx;
    }

SpringBoot使用Tomcat

配置基础参数

使用嵌入式Tomcat时我们要配置端口资源路径等等这些全局配置,那么这些我们怎么配置呢?如下图:

server:
  compression:
    enabled: true
    min-response-size: 1MB
  port: 8080
  error:
    path: /error

创建Tomcat 参数自定义属性配置Bean

yml里的配置参数,最终会被Spring Boot读取,ServletWebServerFactoryConfiguration 来确定使用什么服务器,EmbeddedWebServerFactoryCustomizerAutoConfiguration来决定怎样去配置Web服务器,默认情况下Spring Boot引入的是Tomcat,因此会创建TomcatServletWebServerFactory 和TomcatWebServerFactoryCustomizer这2个Bean,简略代码如下:

@Configuration(proxyBeanMethods = false)
class ServletWebServerFactoryConfiguration {
	
	// 默认情况下使用Tomcat 服务器,这里传入了一些其他的Customizer,允许开发人员进行一些定制
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
	@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
	static class EmbeddedTomcat {

		@Bean
		TomcatServletWebServerFactory tomcatServletWebServerFactory(
				ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
				ObjectProvider<TomcatContextCustomizer> contextCustomizers,
				ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
			TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
			factory.getTomcatConnectorCustomizers()
					.addAll(connectorCustomizers.orderedStream().collect(Collectors.toList()));
			factory.getTomcatContextCustomizers()
					.addAll(contextCustomizers.orderedStream().collect(Collectors.toList()));
			factory.getTomcatProtocolHandlerCustomizers()
					.addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
			return factory;
		}

	}
 	... 省略其他 ...
}

自定义属性配置器,会在创建Tomcat实例时回调:

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication
@EnableConfigurationProperties(ServerProperties.class)
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {

	/**
	 * 默认情况下Spring Boot引入的是Tomcat,会满足此条件,然后创建TomcatWebServerFactoryCustomizer
	 */
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })
	public static class TomcatWebServerFactoryCustomizerConfiguration {

		@Bean
		public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment,
				ServerProperties serverProperties) {
			return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
		}

	}

	 ... 省略其他 ...

}

应用启动

我们已经知道了Spring 会配置这样TomcatServletWebServerFactory 和TomcatWebServerFactoryCustomizer2个Bean,那一起跟踪下看下启动流程。
我们看下Spring Boot 应用时怎么启动的,这里不是分析全流程源码,因此我们只关心和Tomcat相关的,这可能对新手不是很友好,真的很抱歉。
SpringApplication.run(…)方法会创建AnnotationConfigServletWebServerApplicationContext这个上下文,然后进行环境初始化,自动化配置,创建单例Bean等等操作后, 然后进行刷新操作:

@SpringBootApplication
public class WebMvcApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebMvcApplication.class, args);
    }

}


public class SpringApplication {
    ... 省略其他方法 ...

public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			// Web 情况下使用的是AnnotationConfigServletWebServerApplicationContext这个应用上下文
			context = createApplicationContext();
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			// 刷新操作
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
			listeners.started(context);
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}
	
   ... 省略其他方法 ...
}

refreshContext我们已经知道是AnnotationConfigServletWebServerApplicationContext 调用其refresh,我们看下类图,核心的已经标记出来了:
类图
AnnotationConfigServletWebServerApplicationContext 继承自AbstractApplicationContext,所以这里的refresh其实就是调用父类AbstractApplicationContext的refresh模板方法。
简易时序图:
时序图

创建Tomcat服务

已经知道ServletWebServerApplicationContext#onRefresh方法会被执行,和Web服务器相关也从这里开始:

public class ServletWebServerApplicationContext extends GenericWebApplicationContext
		implements ConfigurableWebServerApplicationContext {
	... 省略其他方法 ...
	@Override
	protected void onRefresh() {
		super.onRefresh();
		try {
			// 创建Web服务器
			createWebServer();
		}
		catch (Throwable ex) {
			throw new ApplicationContextException("Unable to start web server", ex);
		}
	}
	private void createWebServer() {
		WebServer webServer = this.webServer;
		// war包放在tomcat下,服务器一定是先启动的这种模式下这里不为null 
		ServletContext servletContext = getServletContext();
		// spring boot jar 启动或者 main 方法启动方式,这里一定为null
		if (webServer == null && servletContext == null) {
		    // factory 就是之前创建的TomcatServletWebServerFactory
			ServletWebServerFactory factory = getWebServerFactory();
			// 这里在回调的时候传入了一个初始化的回调
			this.webServer = factory.getWebServer(getSelfInitializer());
			getBeanFactory().registerSingleton("webServerGracefulShutdown",
					new WebServerGracefulShutdownLifecycle(this.webServer));
			getBeanFactory().registerSingleton("webServerStartStop",
					new WebServerStartStopLifecycle(this, this.webServer));
		}
		else if (servletContext != null) {
			try {
				getSelfInitializer().onStartup(servletContext);
			}
			catch (ServletException ex) {
				throw new ApplicationContextException("Cannot initialize servlet context", ex);
			}
		}
		initPropertySources();
	}

	private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
		return this::selfInitialize;
	}

	private void selfInitialize(ServletContext servletContext) throws ServletException {
		prepareWebApplicationContext(servletContext);
		registerApplicationScope(servletContext);
		WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
		for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
			beans.onStartup(servletContext);
		}
	}
	... 省略其他方法 ...
}

这里的回调其实就是为了给spring 其他的Servlet 功能提供一个钩子,在Tomcat 启动过程中进行一些其他的配置。它可以理解为一个桥梁,打通了spring context 和 web server context。

factory既然是TomcatServletWebServerFactory,那么继续跟踪factory.getWebServer(getSelfInitializer())方法,代码和第一章单独使用嵌入式Tomcat的方式类似,如下:

	public WebServer getWebServer(ServletContextInitializer... initializers) {
		if (this.disableMBeanRegistry) {
			Registry.disableRegistry();
		}
		Tomcat tomcat = new Tomcat();
		// 
		File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
		tomcat.setBaseDir(baseDir.getAbsolutePath());
		Connector connector = new Connector(this.protocol);
		connector.setThrowOnFailure(true);
		tomcat.getService().addConnector(connector);
		// 触发定制customize回调
		customizeConnector(connector);
		tomcat.setConnector(connector);
		tomcat.getHost().setAutoDeploy(false);
		configureEngine(tomcat.getEngine());
		for (Connector additionalConnector : this.additionalTomcatConnectors) {
			tomcat.getService().addConnector(additionalConnector);
		}
		// 准备webContext, 注意将初始化的回调已经传入
		prepareContext(tomcat.getHost(), initializers);
		// 启动容器
		return getTomcatWebServer(tomcat);
	}

注意这里的TomcatEmbeddedContext 和单独使用Tomcat里的StandardContext进行区分,第一章是通过Tomcat#addWebapp(…)创建,返回的是StandardContext,会默认添加DefaultServlet和JspServlet。然而,这里是Spring boot创建了一个自己的TomcatEmbeddedContext,它继承自StandardContext:

protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
		File documentRoot = getValidDocumentRoot();
		// 干净的context ,继承了StandardContext,没有任何监听器
		TomcatEmbeddedContext context = new TomcatEmbeddedContext();
		if (documentRoot != null) {
			context.setResources(new LoaderHidingResourceRoot(context));
		}
		context.setName(getContextPath());
		context.setDisplayName(getDisplayName());
		context.setPath(getContextPath());
		File docBase = (documentRoot != null) ? documentRoot : createTempDir("tomcat-docbase");
		context.setDocBase(docBase.getAbsolutePath());
		context.addLifecycleListener(new FixContextListener());
		context.setParentClassLoader((this.resourceLoader != null) ? this.resourceLoader.getClassLoader()
				: ClassUtils.getDefaultClassLoader());
		resetDefaultLocaleMapping(context);
		addLocaleMappings(context);
		try {
			context.setCreateUploadTargets(true);
		}
		catch (NoSuchMethodError ex) {
			// Tomcat is < 8.5.39. Continue.
		}
		configureTldSkipPatterns(context);
		WebappLoader loader = new WebappLoader();
		loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName());
		loader.setDelegate(true);
		context.setLoader(loader);
		// 单独添加默认的servlet,默认为true
		if (isRegisterDefaultServlet()) {
			addDefaultServlet(context);
		}
		// 是否添加JspServlet取决于是否存在org.apache.jasper.servlet.JspServlet, spring boot 默认引得是tomcat-embed-core,所以这里不会添加JspServlet的支持
		if (shouldRegisterJspServlet()) {
			addJspServlet(context);
			addJasperInitializer(context);
		}
		context.addLifecycleListener(new StaticResourceConfigurer(context));
		// 注意这里将ServletWebServerApplicationContext的实例化方法进行了封装
		ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
		host.addChild(context);
		// 配置tomcat 上下文
		configureContext(context, initializersToUse);
		postProcessContext(context);
	}

configureContext这个方法创建了一个TomcatStarter类,它实现ServletContainerInitializer接口,servlet3.0的新实现,同时将initializers配置在了TomcatStarter里。
注意:initializers包含有ServletWebServerApplicationContext#selfInitialize方法回调
servlet容器启动后会调用其onStartup方法,回调下面会讲,这里先看源码:

protected void configureContext(Context context, ServletContextInitializer[] initializers) {
		TomcatStarter starter = new TomcatStarter(initializers);
		if (context instanceof TomcatEmbeddedContext) {
			TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
			embeddedContext.setStarter(starter);
			embeddedContext.setFailCtxIfServletStartFails(true);
		}
		context.addServletContainerInitializer(starter, NO_CLASSES);
		for (LifecycleListener lifecycleListener : this.contextLifecycleListeners) {
			context.addLifecycleListener(lifecycleListener);
		}
		for (Valve valve : this.contextValves) {
			context.getPipeline().addValve(valve);
		}
		// 配置错误页面
		for (ErrorPage errorPage : getErrorPages()) {
			org.apache.tomcat.util.descriptor.web.ErrorPage tomcatErrorPage = new org.apache.tomcat.util.descriptor.web.ErrorPage();
			tomcatErrorPage.setLocation(errorPage.getPath());
			tomcatErrorPage.setErrorCode(errorPage.getStatusCode());
			tomcatErrorPage.setExceptionType(errorPage.getExceptionName());
			context.addErrorPage(tomcatErrorPage);
		}
		for (MimeMappings.Mapping mapping : getMimeMappings()) {
			context.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
		}
		// session相关
		configureSession(context);
		new DisableReferenceClearingContextCustomizer().customize(context);
		for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) {
			customizer.customize(context);
		}
	}

一切准备就绪,返回到getWebServer方法,开始真正启动:

	public WebServer getWebServer(ServletContextInitializer... initializers) {
		if (this.disableMBeanRegistry) {
			Registry.disableRegistry();
		}
		Tomcat tomcat = new Tomcat();
		...省略其他代码 ...
		prepareContext(tomcat.getHost(), initializers);
		// 启动
		return getTomcatWebServer(tomcat);
	}
	protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
		return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown());
	}

启动Tomcat服务:

public class TomcatWebServer implements WebServer {

	public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
		Assert.notNull(tomcat, "Tomcat Server must not be null");
		this.tomcat = tomcat;
		this.autoStart = autoStart;
		this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
		initialize();
	}
private void initialize() throws WebServerException {
		logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
		synchronized (this.monitor) {
			try {
				addInstanceIdToEngineName();

				Context context = findContext();
				context.addLifecycleListener((event) -> {
					if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
						// Remove service connectors so that protocol binding doesn't
						// happen when the service is started.
						removeServiceConnectors();
					}
				});

				// 启动服务,触发监听器
				this.tomcat.start();

				// We can re-throw failure exception directly in the main thread
				rethrowDeferredStartupExceptions();

				try {
					ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
				}
				catch (NamingException ex) {
					// Naming is not enabled. Continue
				}

				// Unlike Jetty, all Tomcat threads are daemon threads. We create a
				// blocking non-daemon to stop immediate shutdown
				startDaemonAwaitThread();
			}
			catch (Exception ex) {
				stopSilently();
				destroySilently();
				throw new WebServerException("Unable to start embedded Tomcat", ex);
			}
		}
	}
}

servlet容器启动后,会触发ServletContainerInitializer#onStartup回调,TomcatStarter实现了该接口,触发onStartup方法:

	public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
		try {
			for (ServletContextInitializer initializer : this.initializers) {
				initializer.onStartup(servletContext);
			}
		}
		...
	}

前边在创建TomcatStarter对象时,已经将ServletWebServerApplicationContext#selfInitialize传入,终于在这里有了作用,触发了方法调用,那么看ServletWebServerApplicationContext#selfInitialize做了什么:

	private void selfInitialize(ServletContext servletContext) throws ServletException {
		// 准备 spring web 上下文参数, 从这一步开始servletContext就不在为null了
		prepareWebApplicationContext(servletContext);
		// web 情况下独有的scope绑定
		registerApplicationScope(servletContext);
		// 添加环境变量
		WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
		// 初始化servlet下的bean, 和ServletContainerInitializer功能类似,只是这里是spring 自己的接口
		for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
			beans.onStartup(servletContext);
		}
	}
	protected Collection<ServletContextInitializer> getServletContextInitializerBeans() {
		return new ServletContextInitializerBeans(getBeanFactory());
	}

getServletContextInitializerBeans 创建了ServletContextInitializerBeans对象,目的是检索实现了ServletContextInitializer接口的对象,ServletContextInitializer.class是spring自己的接口,类似servlet 3.0 ServletContainerInitializer接口,都有onStartup方法但参数不一样,另一一个小细节集成自AbstractCollection接口,因此可以进行集合操作,ServletContextInitializerBeans部分源码:

	@SafeVarargs
	public ServletContextInitializerBeans(ListableBeanFactory beanFactory,
			Class<? extends ServletContextInitializer>... initializerTypes) {
		this.initializers = new LinkedMultiValueMap<>();
		// 目前只有一种类型: ServletContextInitializer.class
		this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes)
				: Collections.singletonList(ServletContextInitializer.class);
		// 在spring 上下文中检索ServletContextInitializer的实现类,并进行回调
		// spring mvc DispatchServlet将会在这里添加
		addServletContextInitializerBeans(beanFactory);
		
		addAdaptableBeans(beanFactory);
		// 排序
		List<ServletContextInitializer> sortedInitializers = this.initializers.values().stream()
				.flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE))
				.collect(Collectors.toList());
		this.sortedList = Collections.unmodifiableList(sortedInitializers);
		logMappings(this.initializers);
	}
	
	private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) {
		for (Class<? extends ServletContextInitializer> initializerType : this.initializerTypes) {
			for (Entry<String, ? extends ServletContextInitializer> initializerBean : getOrderedBeansOfType(beanFactory,
					initializerType)) {
					// 对接口进行分类标记,监听器还\过滤器\servlet
				addServletContextInitializerBean(initializerBean.getKey(), initializerBean.getValue(), beanFactory);
			}
		}
	}
	private void addServletContextInitializerBean(String beanName, ServletContextInitializer initializer,
			ListableBeanFactory beanFactory) {
		if (initializer instanceof ServletRegistrationBean) {
			Servlet source = ((ServletRegistrationBean<?>) initializer).getServlet();
			addServletContextInitializerBean(Servlet.class, beanName, initializer, beanFactory, source);
		}
		else if (initializer instanceof FilterRegistrationBean) {
			Filter source = ((FilterRegistrationBean<?>) initializer).getFilter();
			addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source);
		}
		else if (initializer instanceof DelegatingFilterProxyRegistrationBean) {
			String source = ((DelegatingFilterProxyRegistrationBean) initializer).getTargetBeanName();
			addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source);
		}
		else if (initializer instanceof ServletListenerRegistrationBean) {
			EventListener source = ((ServletListenerRegistrationBean<?>) initializer).getListener();
			addServletContextInitializerBean(EventListener.class, beanName, initializer, beanFactory, source);
		}
		else {
			addServletContextInitializerBean(ServletContextInitializer.class, beanName, initializer, beanFactory,
					initializer);
		}
	}

ServletContextInitializerBeans检索到所有ServletContextInitializer的接口后,进行循环回调:

for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
			beans.onStartup(servletContext);
		}

比如我的代码getServletContextInitializerBeans()里返回检索的对象有:

0 = {FilterRegistrationBean@8232} "characterEncodingFilter urls=[/*] order=-2147483648"
1 = {FilterRegistrationBean@7903} "filterRegistrationBean urls=[/*] order=-2147483647"
2 = {FilterRegistrationBean@8233} "formContentFilter urls=[/*] order=-9900"
3 = {FilterRegistrationBean@8234} "requestContextFilter urls=[/*] order=-105"
4 = {DelegatingFilterProxyRegistrationBean@8031} "springSecurityFilterChain urls=[/*] order=-100"
5 = {DispatcherServletRegistrationBean@8050} "dispatcherServlet urls=[/]"
6 = {ServletEndpointRegistrar@8052} 

我们看到一个DispatcherServletRegistrationBean,它就是负责将DispatcherServlet添加到servlet 容器里的。熟悉spring mvc的应该都知道这个是它的核心也是唯一的servlet,负责web所有的请
而DispatcherServletRegistrationBean这个bean是由DispatcherServletRegistrationConfiguration进行注册的。
DispatcherServletRegistrationBean则是通过直接添加servlet的形式:
DispatcherServletRegistrationBean时序图

至此,tomcat 也已经启动完毕,后续spring 会做一些其他的动作,不在本文的范畴。文章整理略显匆忙,有不对之处请大家多指教。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值