导读
作为一个开发,使用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
后台日志:
[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, 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的形式:
至此,tomcat 也已经启动完毕,后续spring 会做一些其他的动作,不在本文的范畴。文章整理略显匆忙,有不对之处请大家多指教。