在上文中我们介绍了 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:49 pm