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());// 监听器方式添加自定义的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实践相关推荐
- Springboot启动配置原理
Springboot启动配置原理 启动配置原理 核心启动配置参数 配置在META-INF/spring.factories中 ApplicationContextInitializer SpringA ...
- 【Tomcat】Tomcat原理 第一部分 Tomcat基础
网络通信的三要素: ①IP:电子设备(计算机)在网络中的唯一标识 ②端口:应用程序在计算机中的唯一标识 ③传输协议:规定了数据传输的规则,基础协议有:TCP(安全协议,"3次握手" ...
- SpringBoot嵌入Tomcat原理分析
SpringBoot嵌入Tomcat原理 内嵌Tomcat启动原理 首先,来到启动SpringBoot项目的地方,也就是朱配置类. @SpringBootApplication public clas ...
- maven_结合使用嵌入式Tomcat和Maven tomcat插件
maven 使用Eclipse WTP开发Java Web应用程序时,我们需要在计算机中安装tomcat才能执行该应用程序. 如果在项目上使用Maven,则可以使用tomcat插件运行嵌入式tomca ...
- 结合使用嵌入式Tomcat和Maven tomcat插件
使用Eclipse WTP开发Java Web应用程序时,我们需要在计算机中安装tomcat才能执行该应用程序. 如果在项目上使用Maven,则可以使用tomcat插件运行嵌入式tomcat安装并测试 ...
- SpringBoot2 | SpringBoot启动流程源码分析(一)
首页 博客 专栏·视频 下载 论坛 问答 代码 直播 能力认证 高校 会员中心 收藏 动态 消息 创作中心 SpringBoot2 | SpringBoot启动流程源码分析(一) 置顶 张书康 201 ...
- 总结:SpringBoot内嵌Tomcat原理
一.介绍 一般我们启动web服务都需要单独的去安装tomcat,而Springboot自身却直接整合了Tomcat,什么原理呢? 二.原理 SpringBoot应用只需要引入spring-boot-s ...
- 如何修改嵌入式服务器的端口号,Ai聘网之如何修改Spring Boot应用启动的嵌入式Tomcat的默认端口8080...
原标题:Ai聘网之如何修改Spring Boot应用启动的嵌入式Tomcat的默认端口8080 Spring Boot是深受广大Java开发人员喜爱的框架,尤其是需要用Java开发微服务的那些开发人员 ...
- springboot启动没反应_新特性:Tomcat和Jetty如何处理Spring Boot应用?
为了方便开发和部署,Spring Boot 在内部启动了一个嵌入式的 Web 容器.我们知道 Tomcat 和 Jetty 是组件化的设计,要启动 Tomcat 或者 Jetty 其实就是启动这些组件 ...
最新文章
- 2019微生物组—宏基因组分析技术专题研讨会第四期
- PLSQL的DBMS_GETLINE
- 可以分屏吗_LED透明屏分屏是怎么一回事?
- 常见php面试题,常见的 PHP 面试题和答案分享
- 12012.memtester内存测试
- 抽屉之Tornado实战(9)--装饰器实现用户登录状态验证
- 单板剥皮机行业调研报告 - 市场现状分析与发展前景预测(2021-2027年)
- 【java】之常用四大线程池用法以及ThreadPoolExecutor详解
- MicroKMS 下载 与使用
- struct lnode{}Lnode后面的Lnode是什么意思
- JS中同时支持切割中英文符号,例如分号,冒号
- 给计算机e盘加密,win10系统给e盘加密的操作方法
- fiilt1左耳连不上_FIIL T1完美解决真无线耳机的痛点:更快更稳更自由
- 鸿图之下服务器维护10月25,更新公告丨《鸿图之下》11月25日维护更新预告
- windows adb usb 找不到设备的解决方法
- 研究区分onbeforeunload事件是刷新还是关闭
- 请停止搬运部署,尤雨溪官申Vue 3官方文档地址
- 神经网络训练精度一直为1,损失为0
- 单身汪送给小汪姐的礼物(笛卡尔之心函数)
- 西北工业大学计算机专业课考什么,2020西北工业大学计算机考研初试科目、参考书目、招生人数...