导读

作为一个开发,使用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());// 监听器方式添加自定义的HelloServletcontext.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 {@Overridepublic 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());}@Overrideprotected 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 {@Overridepublic 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());}@Overrideprotected 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

    后台日志:
[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

    后台日志:
[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, JspServletctx.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: truemin-response-size: 1MBport: 8080error: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 {@BeanTomcatServletWebServerFactory 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 {@Beanpublic 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 GenericWebApplicationContextimplements ConfigurableWebServerApplicationContext {... 省略其他方法 ...@Overrideprotected 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下,服务器一定是先启动的这种模式下这里不为nullServletContext servletContext = getServletContext();// spring boot jar 启动或者 main 方法启动方式,这里一定为nullif (webServer == null && servletContext == null) {// factory 就是之前创建的TomcatServletWebServerFactoryServletWebServerFactory 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,默认为trueif (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 threadrethrowDeferredStartupExceptions();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 shutdownstartDaemonAwaitThread();}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部分源码:

 @SafeVarargspublic ServletContextInitializerBeans(ListableBeanFactory beanFactory,Class<? extends ServletContextInitializer>... initializerTypes) {this.initializers = new LinkedMultiValueMap<>();// 目前只有一种类型: ServletContextInitializer.classthis.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)) {// 对接口进行分类标记,监听器还\过滤器\servletaddServletContextInitializerBean(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的形式:

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

SpringBoot启动Tomcat原理与嵌入式Tomcat实践相关推荐

  1. Springboot启动配置原理

    Springboot启动配置原理 启动配置原理 核心启动配置参数 配置在META-INF/spring.factories中 ApplicationContextInitializer SpringA ...

  2. 【Tomcat】Tomcat原理 第一部分 Tomcat基础

    网络通信的三要素: ①IP:电子设备(计算机)在网络中的唯一标识 ②端口:应用程序在计算机中的唯一标识 ③传输协议:规定了数据传输的规则,基础协议有:TCP(安全协议,"3次握手" ...

  3. SpringBoot嵌入Tomcat原理分析

    SpringBoot嵌入Tomcat原理 内嵌Tomcat启动原理 首先,来到启动SpringBoot项目的地方,也就是朱配置类. @SpringBootApplication public clas ...

  4. maven_结合使用嵌入式Tomcat和Maven tomcat插件

    maven 使用Eclipse WTP开发Java Web应用程序时,我们需要在计算机中安装tomcat才能执行该应用程序. 如果在项目上使用Maven,则可以使用tomcat插件运行嵌入式tomca ...

  5. 结合使用嵌入式Tomcat和Maven tomcat插件

    使用Eclipse WTP开发Java Web应用程序时,我们需要在计算机中安装tomcat才能执行该应用程序. 如果在项目上使用Maven,则可以使用tomcat插件运行嵌入式tomcat安装并测试 ...

  6. SpringBoot2 | SpringBoot启动流程源码分析(一)

    首页 博客 专栏·视频 下载 论坛 问答 代码 直播 能力认证 高校 会员中心 收藏 动态 消息 创作中心 SpringBoot2 | SpringBoot启动流程源码分析(一) 置顶 张书康 201 ...

  7. 总结:SpringBoot内嵌Tomcat原理

    一.介绍 一般我们启动web服务都需要单独的去安装tomcat,而Springboot自身却直接整合了Tomcat,什么原理呢? 二.原理 SpringBoot应用只需要引入spring-boot-s ...

  8. 如何修改嵌入式服务器的端口号,Ai聘网之如何修改Spring Boot应用启动的嵌入式Tomcat的默认端口8080...

    原标题:Ai聘网之如何修改Spring Boot应用启动的嵌入式Tomcat的默认端口8080 Spring Boot是深受广大Java开发人员喜爱的框架,尤其是需要用Java开发微服务的那些开发人员 ...

  9. springboot启动没反应_新特性:Tomcat和Jetty如何处理Spring Boot应用?

    为了方便开发和部署,Spring Boot 在内部启动了一个嵌入式的 Web 容器.我们知道 Tomcat 和 Jetty 是组件化的设计,要启动 Tomcat 或者 Jetty 其实就是启动这些组件 ...

最新文章

  1. 2019微生物组—宏基因组分析技术专题研讨会第四期
  2. PLSQL的DBMS_GETLINE
  3. 可以分屏吗_LED透明屏分屏是怎么一回事?
  4. 常见php面试题,常见的 PHP 面试题和答案分享
  5. 12012.memtester内存测试
  6. 抽屉之Tornado实战(9)--装饰器实现用户登录状态验证
  7. 单板剥皮机行业调研报告 - 市场现状分析与发展前景预测(2021-2027年)
  8. 【java】之常用四大线程池用法以及ThreadPoolExecutor详解
  9. MicroKMS 下载 与使用
  10. struct lnode{}Lnode后面的Lnode是什么意思
  11. JS中同时支持切割中英文符号,例如分号,冒号
  12. 给计算机e盘加密,win10系统给e盘加密的操作方法
  13. fiilt1左耳连不上_FIIL T1完美解决真无线耳机的痛点:更快更稳更自由
  14. 鸿图之下服务器维护10月25,更新公告丨《鸿图之下》11月25日维护更新预告
  15. windows adb usb 找不到设备的解决方法
  16. 研究区分onbeforeunload事件是刷新还是关闭
  17. 请停止搬运部署,尤雨溪官申Vue 3官方文档地址
  18. 神经网络训练精度一直为1,损失为0
  19. 单身汪送给小汪姐的礼物(笛卡尔之心函数)
  20. 西北工业大学计算机专业课考什么,2020西北工业大学计算机考研初试科目、参考书目、招生人数...

热门文章

  1. matplotlib图中显示指定点的坐标
  2. 操作不能完成 (错误 0x00000709) 共享打印机无法连接解决办法
  3. C++——寻找第k大的数
  4. IPV6地址的表示方法
  5. 利用函数“逆序字符串”的三种解法
  6. 全网最详细的自定义设置 iPhone 充电提示音教程!除了音频,视频也可以!
  7. 兄弟2225粉盒清零方法图示
  8. Maven 极简入门
  9. java清空json_java – 从JSONArray中删除JSON对象 – Jettison
  10. 手机浏览器哪个最好用?