导读
作为一个开发,使用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, 那么应该是这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27<!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, 看起来简洁很多:
1
2
3
4
5
6
7
8
9public 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; }
完整代码,有详细的注释:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125package 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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57package 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56package 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的版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<!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都添加进去了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23一月 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
后台日志:
1
2
3
4
5
6
7
8[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
后台日志:
1
2
3
4
5
6[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配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32public 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时我们要配置端口资源路径等等这些全局配置,那么这些我们怎么配置呢?如下图:
1
2
3
4
5
6
7
8server: 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,简略代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28@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实例时回调:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25@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等等操作后, 然后进行刷新操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59@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服务器相关也从这里开始:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55public 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的方式类似,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46protected 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方法,回调下面会讲,这里先看源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33protected 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方法,开始真正启动:
1
2
3
4
5
6
7
8
9
10
11
12
13
14public 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服务:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50public 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方法:
1
2
3
4
5
6
7
8
9public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException { try { for (ServletContextInitializer initializer : this.initializers) { initializer.onStartup(servletContext); } } ... }
前边在创建TomcatStarter对象时,已经将ServletWebServerApplicationContext#selfInitialize传入,终于在这里有了作用,触发了方法调用,那么看ServletWebServerApplicationContext#selfInitialize做了什么:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16private 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部分源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53@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的接口后,进行循环回调:
1
2
3
4for (ServletContextInitializer beans : getServletContextInitializerBeans()) { beans.onStartup(servletContext); }
比如我的代码getServletContextInitializerBeans()里返回检索的对象有:
1
2
3
4
5
6
7
80 = {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实践导读嵌入式Tomcat使用SpringBoot使用Tomcat的全部内容,更多相关SpringBoot启动Tomcat原理与嵌入式Tomcat实践导读嵌入式Tomcat使用SpringBoot使用Tomcat内容请搜索靠谱客的其他文章。
发表评论 取消回复