在上文中我们介绍了Spring Boot
的一些基本概要以及我在这个系列中会讲到的一些东西,正如上文所述,这篇会讲解EmbeddedTomcat
的原理。
传统Tomcat
写过Java
的同学应该知道,在Spring Boot
之前,我们一般会使用Tomcat
等web容器
来管理我们的web
项目,类似于nginx
,但又有所区别。
虽然nginx
和tomcat
最终所做的事情大致一样,但是我们给nginx
的定义是web服务器
,而tomcat
是web容器
,tomcat
是实现了servlet
接口的开源项目,把我们需要自定义servlet
的基础架构给抽离出来。
内嵌Tomcat
那么内嵌Tomcat
有是什么呢?
内嵌Tomcat
可以让我们把Tomcat
内嵌于我们的项目中,让我们的项目更方便部署、方便调试。
实现原理
正如我在上文所说,我们会以「逆向」的方式来帮助大家快速了解其原理。在Spring Boot
中,给我们提供了配置诸如server.port
的能力。我们不妨先修改server.port
试试,看看最终tomcat
是否以另外一个端口执行:
server.port=8081
果然,执行端口更改了:
2019-04-01 21:08:57.644 INFO 28714 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path ''
我们不妨点开server.port
,发现跳转到了ServerProperties
:
public Integer getPort() {
return this.port;
}
public void setPort(Integer port) {
this.port = port;
}
既然我们是通过setPort
来设置端口,那么就意味着在调用的时候是从getPort
来获取端口。我们不妨点击查看getPort
这个方法被谁调用了:
ReactiveWebServerFactoryCustomizer
先不管,我们先看看ServletWebServerFactoryCustomizer
:
@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()::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);
}
我们可以看到某个地方调度了customize
来注入ConfigurableServletWebServerFactory
的一个实现,谁调用了customize
我们可以先放在一边,我们先看看ConfigurableServletWebServerFactory
有哪些实现:
我们看到了非常熟悉的几个类,因为我们已经通过console
得知了最终执行的是tomcat
的结论,那么我们现在需要证明的是为什么调用了TomcatServletWebServerFactory
。
我们可以看到,调用的地方很多,在查看之前,我们不妨先思考一下,假如你是设计者,有tomcat
、jetty
等需要根据用户的自定义来调度,那么你去设计这个类,是不是至少在名字上不应该跟tomcat
、jetty
有关?因此,我们不妨先去ServletWebServerFactoryConfiguration
去验证一下我们的想法是否正确。
@Configuration
class ServletWebServerFactoryConfiguration {
@Configuration
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedTomcat {
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
/**
* Nested configuration if Jetty is being used.
*/
@Configuration
@ConditionalOnClass({ Servlet.class, Server.class, Loader.class,
WebAppContext.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedJetty {
@Bean
public JettyServletWebServerFactory JettyServletWebServerFactory() {
return new JettyServletWebServerFactory();
}
}
/**
* Nested configuration if Undertow is being used.
*/
@Configuration
@ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedUndertow {
@Bean
public UndertowServletWebServerFactory undertowServletWebServerFactory() {
return new UndertowServletWebServerFactory();
}
}
}
我们发现其中有一个注解ConditionalOnClass
(原理在自动装配篇会讲),故名知意,就是说在存在这个类的时候才会执行,同时,我们可以看到最后会注册成一个bean
。
当注册TomcatServletWebServerFactory
之后,我们似乎没有了头绪,我们不妨可以思考一下,不管最终注册了哪个bean
,最终肯定是通过某个接口来完成这个bean
的调度,因此,一方面我们可以查看这个类实现了哪个接口,同时也可以看这个类里面会有接口方法的重写。
通过上述理论,我们不难发现TomcatServletWebServerFactory
实现了ServletWebServerFactory
,并重写了getWebServer
方法。
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
Tomcat tomcat = new Tomcat();
File baseDir = (this.baseDirectory != null) ? this.baseDirectory
: createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
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);
}
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
return new TomcatWebServer(tomcat, getPort() >= 0);
}
随之,我们不难查到ServletWebServerApplicationContext
调度了getWebServer
方法:
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
ServletWebServerFactory factory = getWebServerFactory();
this.webServer = factory.getWebServer(getSelfInitializer());
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context",
ex);
}
}
initPropertySources();
}
//通过ServletWebServerFactory获取bean,也就是TomcatServletWebServerFactory
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);
}
当我们获取TomcatServletWebServerFactory
之后,会调度getWebServer
方法,最后会返回一个TomcatWebServer
。
public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
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();
}
});
// Start the server to trigger initialization listeners
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();
throw new WebServerException("Unable to start embedded Tomcat", ex);
}
}
}
看到这里,我们就大概有一个比较清晰的思路了,我们看到TomcatWebServer
有一个initialize
方法,在构造的时候执行,输出的内容与我们在console
看到的一致:
然后我们通过ServletWebServerApplicationContext
的createWebServer
方法一层一层的往上追溯,发现是SpringApplication
的refresh
方法调度了ServletWebServerApplicationContext
的onRefresh
方法,而SpringApplication
的refresh
方法又是通过run
方法来调度的:
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);
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;
}
最后,就到了我们Spring Boot
的入口:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
那么问题就来了,为什么SpringApplication
能指定到ServletWebServerApplicationContext
这个上下文?
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, "
+ "please specify an ApplicationContextClass",
ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}
原因就在于创建的时候会指定我们的上下文。
至此,我们还有一个问题没有解决,也就是我们上面说到的,谁调度了ServletWebServerFactoryCustomizer
的customize
方法?
我们往上追溯,发现是WebServerFactoryCustomizerBeanPostProcessor
的postProcessBeforeInitialization
方法调度了,而这个类实现了BeanPostProcessor
,postProcessBeforeInitialization
方法就好比一个hook
,当你在注册bean
的时候会执行。
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof WebServerFactory) {
postProcessBeforeInitialization((WebServerFactory) bean);
}
return bean;
}
至此,我们的内嵌tomcat
的执行流程原理就完成了。
总结
我们来做一个小结,大致的运行过程就是SpringApplication
执行run
方法,他会根据指定的context
,然后会调度ServletWebServerApplicationContext
的refresh
方法,然后通过自己的父类调度onRefresh
方法去创建我们的webserver
,在创建的过程中,会执行getWebServerFactory
方法,来根据ServletWebServerFactory
返回我们自定义的bean
,比如默认的就是TomcatServletWebServerFactory
,最后通过这个类去返回一个TomcatWebServer
的类,来完成我们内嵌webserver
的调度。
本文由 nine 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Apr 1, 2019 at 11:56 pm