嵌入式 Servlet 容器
# 460.嵌入式 Servlet 容器
之前我们说过,SpringBoot 内置了 Tomcat,只需启动应用即可监听端口,本文就来讲讲其原理
ps:实用性不高,了解即可
# 原理
在官网文档,是这样说的:
The ServletWebServerApplicationContext
Under the hood, Spring Boot uses a different type of ApplicationContext for embedded servlet container support. The
ServletWebServerApplicationContext
is a special type of WebApplicationContext that bootstraps itself by searching for a singleServletWebServerFactory
bean. Usually aTomcatServletWebServerFactory
,JettyServletWebServerFactory
, orUndertowServletWebServerFactory
has been auto-configured.
大意:如果 SpringBoot 应用启动的时候,发现当前应用是 Web 应用,并且导入了 Tomcat,那么就会一个 Web 版本的 IoC 容器,名字是 ServletWebServerApplicationContext
;
该 IoC 容器在启动的时候会搜索 ServletWebServerFactory
,直译就是 Servlet 的 web 容器工厂,这个工厂生产的就是 Web 容器,例如 TomcatServletWebServerFactory
,JettyServletWebServerFactory
或 UndertowServletWebServerFactory
,对应生成的 Web 容器就是 Tomcat
,Jetty
和 Undertow
注意,
Undertow
不支持 JSP
这些工厂是 SpringBoot 底层默认有的,在配置类 ServletWebServerFactoryAutoConfiguration
中配置;直接由自动配置类帮我们自动配置好
@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {
2
3
4
5
6
7
8
9
10
可以看到 Import 了 ServletWebServerFactoryConfiguration
,而该类又注入了对应的容器工厂:
@Configuration(proxyBeanMethods = false)
class ServletWebServerFactoryConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
static class EmbeddedTomcat { //.....
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class })
static class EmbeddedJetty { //.....
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
static class EmbeddedUndertow { //.....
//.....
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
如果我们导入了 Tomcat,那么根据 @ConditionalOnClass(Tomcat.class)
,就会注入 Tomcat 的工厂类;
由于 web-starter 默认会导入 tomcat 包,因此容器中就有 TomcatServletWebServerFactory
,然后创建出 Tomcat 服务器并启动。
在 IoC 容器中,启动的时候会顺便启动服务器:
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
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();
}
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
我们可以在 createWebServer
方法上打一个断点,然后启动。首先会获取工厂:
ServletWebServerFactory factory = getWebServerFactory();
其源码如下:也就是在 IoC 容器中找,找不到则抛异常;如果数量大于 1 也抛异常
protected ServletWebServerFactory getWebServerFactory() {
// Use bean names so that we don't consider the hierarchy
String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
if (beanNames.length == 0) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing " + "ServletWebServerFactory bean.");
}
if (beanNames.length > 1) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple " + "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
}
return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
拿到工厂后,下一步就是创建服务器了:
ServletWebServerFactory factory = getWebServerFactory();
this.webServer = factory.getWebServer(getSelfInitializer());
2
我们步入 getWebServer
方法:
@Override
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);
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
return getTomcatWebServer(tomcat);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
其内部就会初始化服务器的信息,连接器,Host 和 Engine 等等(了解即可)。之前我们使用 Tomcat,都是双击 startup.bat 启动;而所谓内嵌服务器,就是在代码里完成这一步
# 切换其他服务器
如果想切换为其他服务器,例如 Undertow
,怎么做呢?
首先,Tomcat 是有 web 场景导入的:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2
3
4
我们点进该依赖的详情,就有 Tomcat:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
2
3
4
5
6
因此,我们得先排除掉 Tomcat 的依赖,再引入相关的依赖即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
再次启动,就能看到是 Undertow
了:
当然,建议还是用 Tomcat,因为 Tomcat 综合起来看还是不错的
# 定制服务器
如果我们想要修改服务器相关配置,也是很简单的,首先可以看文档:
可以看到要定制化,就是改配置文件,很多都是以 server 开头的。
从源码方面,很多配置都是取值 ServerProperties
类:
@EnableConfigurationProperties(ServerProperties.class)
public class ServletWebServerFactoryAutoConfiguration {
2
该类都是以 server
作为前缀的:
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
private Integer port;
private InetAddress address;
private final Servlet servlet = new Servlet();
private final Tomcat tomcat = new Tomcat();
private final Jetty jetty = new Jetty();
private final Netty netty = new Netty();
//......
2
3
4
5
6
7
8
9
10
11
12
13
可以看到,还可以针对不同的服务器做配置,然后 Servlet 做共有的配置。
- 例如使用
Tomcat
,端口是 9999,想要的日志路径是./logs2
- 使用
undertow
的话,端口是 9999,日志路径是./logs
server:
port: 9999
tomcat:
accesslog:
directory: ./logs2
undertow:
accesslog:
dir: ./logs
2
3
4
5
6
7
8
还可以自定义一个 Web 服务器工厂,在我们的自动配置类中,还注入了一个 Customizer
,定制化器:
public class ServletWebServerFactoryAutoConfiguration {
@Bean
public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties) {
return new ServletWebServerFactoryCustomizer(serverProperties);
}
2
3
4
5
6
该定制化器,就是在 web 服务器工厂准备好后,再修改一些属性,比如 customize
方法,就会拿到配置文件里的东西,然后绑定到工厂上:
public class ServletWebServerFactoryCustomizer
implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {
@Override
public void customize(ConfigurableServletWebServerFactory factory) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this.serverProperties::getPort).to(factory::setPort);
map.from(this.serverProperties::getAddress).to(factory::setAddress);
map.from(this.serverProperties.getServlet()::getContextPath).to(factory::setContextPath);
map.from(this.serverProperties.getServlet()::getApplicationDisplayName).to(factory::setDisplayName);
map.from(this.serverProperties.getServlet()::isRegisterDefaultServlet).to(factory::setRegisterDefaultServlet);
map.from(this.serverProperties.getServlet()::getSession).to(factory::setSession);
map.from(this.serverProperties::getSsl).to(factory::setSsl);
map.from(this.serverProperties.getServlet()::getJsp).to(factory::setJsp);
map.from(this.serverProperties::getCompression).to(factory::setCompression);
map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader);
map.from(this.serverProperties.getServlet()::getContextParameters).to(factory::setInitParameters);
map.from(this.serverProperties.getShutdown()).to(factory::setShutdown);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
这里扩展下,SpringBoot 中经常有 xxxxCustomizer
定制化器 这种模式,只需自定义一个定制化器,就能修改某个东西的默认行为