嵌入式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
定制化器 这种模式,只需自定义一个定制化器,就能修改某个东西的默认行为