SpringBoot中是如何初始化Tomcat 的
前言
SpringBoot的创造简化了我们创建项目和运行项目的工作。我们不必再像SpringMVC时代那样,将项目先打包成jar包,然后放入Tomcat中再启动。我们可以忽略打包的操作和tomat的配置问题,编写完代码后直接启动项目即可。这一切都得益于SpringBoot的内置容器的实现。那么,SpringBoot具体是如何为我们完成了初始化Tomcat 的这一系列的操作的呢?下面我将以第一人称的学习源码视角带领大家来一起快乐分析SpringBoot的源码来学习下…
源码入口
main方法
SpringBoot的项目启动都是从启动Application中的run方法开始,那么Tomcat 的初始化也必然在其中。
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
System.out.println("Demo Application is Running!");
}
我们点击run方法的源码进行查看:
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<springbootexceptionreporter> exceptionReporters = new ArrayList<>();
//设置系统属性『java.awt.headless』,为true则启用headless模式支持
configureHeadlessProperty();
//通过*SpringFactoriesLoader*检索*META-INF/spring.factories*,
//找到声明的所有SpringApplicationRunListener的实现类并将其实例化,
//之后逐个调用其started()方法,广播SpringBoot要开始执行了
SpringApplicationRunListeners listeners = getRunListeners(args);
//发布应用开始启动事件
listeners.starting();
try {
//初始化参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//创建并配置当前SpringBoot应用将要使用的Environment(包括配置要使用的PropertySource以及Profile),
//并遍历调用所有的SpringApplicationRunListener的environmentPrepared()方法,广播Environment准备完毕。
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
//打印banner
Banner printedBanner = printBanner(environment);
//创建应用上下文
context = createApplicationContext();
//通过*SpringFactoriesLoader*检索*META-INF/spring.factories*,获取并实例化异常分析器
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
//为ApplicationContext加载environment,之后逐个执行ApplicationContextInitializer的initialize()方法来进一步封装ApplicationContext,
//并调用所有的SpringApplicationRunListener的contextPrepared()方法,【EventPublishingRunListener只提供了一个空的contextPrepared()方法】,
//之后初始化IoC容器,并调用SpringApplicationRunListener的contextLoaded()方法,广播ApplicationContext的IoC加载完成,
//这里就包括通过**@EnableAutoConfiguration**导入的各种自动配置类。
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);
//遍历所有注册的ApplicationRunner和CommandLineRunner,并执行其run()方法。
//我们可以实现自己的ApplicationRunner或者CommandLineRunner,来对SpringBoot的启动过程进行扩展。
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;
}
这里,run方法的运行逻辑我们可以总结为几下几点:
1、配置系统属性
2、获取spring应用的监听器,发布应用的启动事件
3、初始化应用启动参数
4、配置应用启动的环境,输出banner
5、创建上下文(包含tomcat的初始化)
6、预处理上下文
7、刷新上下文(包含tomcat的初始化)
8、再刷新上下文,为了后续的扩展预留操作,也可以称之为空刷新。
9、发布应用已经启动的监听事件
10、发布应用启动完成的监听事件
createApplicationContext方法
其中,第5步和第7步为我们在研究tomcat如何初始化的重要的操作步骤。下面,我们分别来看下这两个方法的源码,到底做了什么事情:
/**
* Strategy method used to create the {@link ApplicationContext}. By default this
* method will respect any explicitly set application context or application context
* class before falling back to a suitable default.
* @return the application context (not yet refreshed)
* @see #setApplicationContextClass(Class)
*/
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
// 确定项目是应该创建那种类型的servlet
switch (this.webApplicationType) {
// web类型
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
// 非web类型
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);
}
在createApplicationContext这个方法中,我们可以大概的知道,这里主要是做对于应用的配置类型的界定。我们是创建web类型(SERVLET)?非web类型(REACTIVE)?默认类型?
既然我们是研究tomcat,那么必然我们是启动web类型的应用。所以,这里我们通过反射创建的必然是DEFAULT_SERVLET_WEB_CONTEXT_CLASS这个类。
AnnotationConfigServletWebServerApplicationContext类
上面,DEFAULT_SERVLET_WEB_CONTEXT_CLASS这个常量对应的类是:
/**
* The class name of application context that will be used by default for web
* environments.
*/
public static final String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot."
+ "web.servlet.context.AnnotationConfigServletWebServerApplicationContext";
那么,我们下来看下这个类:
public class AnnotationConfigServletWebServerApplicationContext extends ServletWebServerApplicationContext
implements AnnotationConfigRegistry {...}
在这个类中,别的我不想去管,主要是我看到了其继承的父类ServletWebServerApplicationContext这个类。
ServletWebServerApplicationContext类
从名字上,我们可以看到这个类的作用 – web类型的服务应用的上下文类。那么,下面我们一起看下这个类:
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.servlet.context;
import java.util.Collection;
import java.util.Collections;
import java.util.EventListener;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.boot.web.servlet.ServletContextInitializerBeans;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextException;
import org.springframework.core.io.Resource;
import org.springframework.util.StringUtils;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.context.support.ServletContextAwareProcessor;
import org.springframework.web.context.support.ServletContextResource;
import org.springframework.web.context.support.ServletContextScope;
import org.springframework.web.context.support.WebApplicationContextUtils;
/**
* A {@link WebApplicationContext} that can be used to bootstrap itself from a contained
* {@link ServletWebServerFactory} bean.
* <p>
* This context will create, initialize and run an {@link WebServer} by searching for a
* single {@link ServletWebServerFactory} bean within the {@link ApplicationContext}
* itself. The {@link ServletWebServerFactory} is free to use standard Spring concepts
* (such as dependency injection, lifecycle callbacks and property placeholder variables).
* <p>
* In addition, any {@link Servlet} or {@link Filter} beans defined in the context will be
* automatically registered with the web server. In the case of a single Servlet bean, the
* '/' mapping will be used. If multiple Servlet beans are found then the lowercase bean
* name will be used as a mapping prefix. Any Servlet named 'dispatcherServlet' will
* always be mapped to '/'. Filter beans will be mapped to all URLs ('/*').
* <p>
* For more advanced configuration, the context can instead define beans that implement
* the {@link ServletContextInitializer} interface (most often
* {@link ServletRegistrationBean}s and/or {@link FilterRegistrationBean}s). To prevent
* double registration, the use of {@link ServletContextInitializer} beans will disable
* automatic Servlet and Filter bean registration.
* <p>
* Although this context can be used directly, most developers should consider using the
* {@link AnnotationConfigServletWebServerApplicationContext} or
* {@link XmlServletWebServerApplicationContext} variants.
*
* @author Phillip Webb
* @author Dave Syer
* @author Scott Frederick
* @since 2.0.0
* @see AnnotationConfigServletWebServerApplicationContext
* @see XmlServletWebServerApplicationContext
* @see ServletWebServerFactory
*/
public class ServletWebServerApplicationContext extends GenericWebApplicationContext
implements ConfigurableWebServerApplicationContext {
private static final Log logger = LogFactory.getLog(ServletWebServerApplicationContext.class);
/**
* Constant value for the DispatcherServlet bean name. A Servlet bean with this name
* is deemed to be the "main" servlet and is automatically given a mapping of "/" by
* default. To change the default behavior you can use a
* {@link ServletRegistrationBean} or a different bean name.
*/
public static final String DISPATCHER_SERVLET_NAME = "dispatcherServlet";
private volatile WebServer webServer;
private ServletConfig servletConfig;
private String serverNamespace;
/**
* Create a new {@link ServletWebServerApplicationContext}.
*/
public ServletWebServerApplicationContext() {
}
/**
* Create a new {@link ServletWebServerApplicationContext} with the given
* {@code DefaultListableBeanFactory}.
* @param beanFactory the DefaultListableBeanFactory instance to use for this context
*/
public ServletWebServerApplicationContext(DefaultListableBeanFactory beanFactory) {
super(beanFactory);
}
/**
* Register ServletContextAwareProcessor.
* @see ServletContextAwareProcessor
*/
@Override
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
beanFactory.addBeanPostProcessor(new WebApplicationContextServletContextAwareProcessor(this));
beanFactory.ignoreDependencyInterface(ServletContextAware.class);
registerWebApplicationScopes();
}
@Override
public final void refresh() throws BeansException, IllegalStateException {
try {
super.refresh();
}
catch (RuntimeException ex) {
stopAndReleaseWebServer();
throw ex;
}
}
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
@Override
protected void finishRefresh() {
super.finishRefresh();
WebServer webServer = startWebServer();
if (webServer != null) {
publishEvent(new ServletWebServerInitializedEvent(webServer, this));
}
}
@Override
protected void onClose() {
super.onClose();
stopAndReleaseWebServer();
}
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();
}
/**
* Returns the {@link ServletWebServerFactory} that should be used to create the
* embedded {@link WebServer}. By default this method searches for a suitable bean in
* the context itself.
* @return a {@link ServletWebServerFactory} (never {@code null})
*/
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);
}
/**
* Returns the {@link ServletContextInitializer} that will be used to complete the
* setup of this {@link WebApplicationContext}.
* @return the self initializer
* @see #prepareWebApplicationContext(ServletContext)
*/
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);
}
}
private void registerApplicationScope(ServletContext servletContext) {
ServletContextScope appScope = new ServletContextScope(servletContext);
getBeanFactory().registerScope(WebApplicationContext.SCOPE_APPLICATION, appScope);
// Register as ServletContext attribute, for ContextCleanupListener to detect it.
servletContext.setAttribute(ServletContextScope.class.getName(), appScope);
}
private void registerWebApplicationScopes() {
ExistingWebApplicationScopes existingScopes = new ExistingWebApplicationScopes(getBeanFactory());
WebApplicationContextUtils.registerWebApplicationScopes(getBeanFactory());
existingScopes.restore();
}
/**
* Returns {@link ServletContextInitializer}s that should be used with the embedded
* web server. By default this method will first attempt to find
* {@link ServletContextInitializer}, {@link Servlet}, {@link Filter} and certain
* {@link EventListener} beans.
* @return the servlet initializer beans
*/
protected Collection<ServletContextInitializer> getServletContextInitializerBeans() {
return new ServletContextInitializerBeans(getBeanFactory());
}
/**
* Prepare the {@link WebApplicationContext} with the given fully loaded
* {@link ServletContext}. This method is usually called from
* {@link ServletContextInitializer#onStartup(ServletContext)} and is similar to the
* functionality usually provided by a {@link ContextLoaderListener}.
* @param servletContext the operational servlet context
*/
protected void prepareWebApplicationContext(ServletContext servletContext) {
Object rootContext = servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
if (rootContext != null) {
if (rootContext == this) {
throw new IllegalStateException(
"Cannot initialize context because there is already a root application context present - "
+ "check whether you have multiple ServletContextInitializers!");
}
return;
}
servletContext.log("Initializing Spring embedded WebApplicationContext");
try {
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this);
if (logger.isDebugEnabled()) {
logger.debug("Published root WebApplicationContext as ServletContext attribute with name ["
+ WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
}
setServletContext(servletContext);
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - getStartupDate();
logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
}
}
catch (RuntimeException | Error ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
}
private WebServer startWebServer() {
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.start();
}
return webServer;
}
private void stopAndReleaseWebServer() {
WebServer webServer = this.webServer;
if (webServer != null) {
try {
webServer.stop();
this.webServer = null;
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}
@Override
protected Resource getResourceByPath(String path) {
if (getServletContext() == null) {
return new ClassPathContextResource(path, getClassLoader());
}
return new ServletContextResource(getServletContext(), path);
}
@Override
public String getServerNamespace() {
return this.serverNamespace;
}
@Override
public void setServerNamespace(String serverNamespace) {
this.serverNamespace = serverNamespace;
}
@Override
public void setServletConfig(ServletConfig servletConfig) {
this.servletConfig = servletConfig;
}
@Override
public ServletConfig getServletConfig() {
return this.servletConfig;
}
/**
* Returns the {@link WebServer} that was created by the context or {@code null} if
* the server has not yet been created.
* @return the embedded web server
*/
@Override
public WebServer getWebServer() {
return this.webServer;
}
/**
* Utility class to store and restore any user defined scopes. This allow scopes to be
* registered in an ApplicationContextInitializer in the same way as they would in a
* classic non-embedded web application context.
*/
public static class ExistingWebApplicationScopes {
private static final Set<String> SCOPES;
static {
Set<String> scopes = new LinkedHashSet<>();
scopes.add(WebApplicationContext.SCOPE_REQUEST);
scopes.add(WebApplicationContext.SCOPE_SESSION);
SCOPES = Collections.unmodifiableSet(scopes);
}
private final ConfigurableListableBeanFactory beanFactory;
private final Map<String, Scope> scopes = new HashMap<>();
public ExistingWebApplicationScopes(ConfigurableListableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
for (String scopeName : SCOPES) {
Scope scope = beanFactory.getRegisteredScope(scopeName);
if (scope != null) {
this.scopes.put(scopeName, scope);
}
}
}
public void restore() {
this.scopes.forEach((key, value) -> {
if (logger.isInfoEnabled()) {
logger.info("Restoring user defined scope " + key);
}
this.beanFactory.registerScope(key, value);
});
}
}
}
在这个类中,无非就是对于整个web应用的生命周期操作的一些方法。但是,有一个方法显得很突出自我:那就是createWebServer()方法。
createWebServer方法
从名字上看,这个方法的作用就是创建一个web类型的应用。那么,真相到底是什么呢?我们一起来看下:
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
// 通过工厂模式,创建webServer的实例
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();
}
纵观整个方法,我们找到了创建web应用实例的地方。但是,还是没有找到在那里启动tomcat的,坑呀!!!但是,我们知道了其通过ServletWebServerFactory来创建了一个webServer的实例对象。再回首ServletWebServerApplicationContext这个类,既然我们有了应用的实例对象,那么下来是不是就该启动这个实例对象了?我们查找下ServletWebServerApplicationContext中是否有启动方法。
startWebServer方法
不出所料,还真有一个启动webServer的方法。我们看下这个方法在搞什么事情:
private WebServer startWebServer() {
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.start();
}
return webServer;
}
尴尬了,这里直接获取了上面创建的实例对象后,判断不为空后就直接调用了WebServer中的start()方法启动了。我们在这里打个断点调试下代码,看看运行到这里后是什么样子:
Connected to the target VM, address: '127.0.0.1:59767', transport: 'socket'
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.8.RELEASE)
2020-06-28 17:03:07.041 INFO 4687 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication on fanyuanhangdeMacBook-Pro.local with PID 4687 (/Users/fanyuanhang/Downloads/springboot/target/classes started by fanyuanhang in /Users/fanyuanhang/Downloads/springboot)
2020-06-28 17:03:07.044 INFO 4687 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to default profiles: default
2020-06-28 17:03:08.040 INFO 4687 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2020-06-28 17:03:08.049 INFO 4687 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-06-28 17:03:08.049 INFO 4687 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.36]
2020-06-28 17:03:08.120 INFO 4687 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-06-28 17:03:08.120 INFO 4687 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1025 ms
2020-06-28 17:03:08.302 INFO 4687 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-06-28 17:03:08.484 INFO 4687 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2020-06-28 17:03:28.064 INFO 4687 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 21.385 seconds (JVM running for 22.101)
我去!!!刚好运行完这个方法后tomcat启动了,还配置了默认端口8080…无语,看来配置tomcat这件事在启动webServer前就完成了…
那么,到底tomcat在那里初始化的?难道还有createTomcatxxx()这样的方法存在于ServletWebServerApplicationContext中吗?我们搜搜看:
GG!!!木有啊!!!看来初始化tomcat这件事情SpringBoot制作组藏得够深的啊…那么,我们冷静下来分析下:
1、启动前已经初始化好了tomcat
2、没有在ServletWebServerApplicationContext存在初始化方法
顺着这个思路我们想想自己是制作人会怎么做:按我写代码的习惯,创建一个实例的时候,我自己会将这个实例所需要的所有配置在创建实例的时候也一同配置好。难道?跟我想到一起去了?那么我们再次回顾下createWebServer这个方法。
再回首createWebServer方法
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();
}
我们再次细细品味下createWebServer方法。通过ServletWebServerFactory这个工厂接口的提供的getWebServer()方法获取了webServer实例对象…em,看来有必要看下这个接口的实现类具体是怎么实现的了。冥冥之中,我感觉我距离真相进了一步,哈哈。
ServletWebServerFactory类图结构
调出这个接口的类图结构来:
em,你品,你细品。ServletWebServerFactory接口的最终实现子类有三个,其中,分别对应了最开始createApplicationContext方法中根据类型来获取对应的应用类型的配置。
这里想都不用想,目标直指TomcatServletWebServerFactory这个类,我们来看下这里是怎么实现的getWebServer方法的。
TomcatServletWebServerFactory类
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
// 创建tomcat实例
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设置连接器connector
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
// 配置引擎
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
// 启动tomcat
return getTomcatWebServer(tomcat);
}
呦西,看看我们看到什么?哈哈,果然,在TomcatServletWebServerFactory实现的getWebServer方法中创建并初始化了tomcat。
到这里,我们终于找到了tomcat在那里创建的了!!!小兴奋一下。在Tomcat对象中,指定了一些默认属性的配置:
public class Tomcat {
private static final StringManager sm = StringManager.getManager(Tomcat.class);
// Some logging implementations use weak references for loggers so there is
// the possibility that logging configuration could be lost if GC runs just
// after Loggers are configured but before they are used. The purpose of
// this Map is to retain strong references to explicitly configured loggers
// so that configuration is not lost.
private final Map<String, Logger> pinnedLoggers = new HashMap<>();
protected Server server;
protected int port = 8080;
protected String hostname = "localhost";
protected String basedir;
好的,我们找到了Tomcat创建的地方。那么,创建了Tomcat,又是在那里启动的tomcat的呢?我们细品下TomcatServletWebServerFactory.getWebServer()方法。
我们继续在getWebServer()方法中加入断点,看看到底在哪个方法启动了tomcat:
/**
* Factory method called to create the {@link TomcatWebServer}. Subclasses can
* override this method to return a different {@link TomcatWebServer} or apply
* additional processing to the Tomcat server.
* @param tomcat the Tomcat server.
* @return a new {@link TomcatWebServer} instance
*/
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
return new TomcatWebServer(tomcat, getPort() >= 0);
}
根据调试结果显示,在执行 getTomcatWebServer(Tomcat tomcat)的时候启动了tomcat应用。而 getTomcatWebServer(Tomcat tomcat)又返回了一个TomcatWebServer的实例。那么在TomcatWebServer的构造函数中必然启动了tomcat,我们来看下。
TomcatWebServer中的初始化以及启动
/**
* Create a new {@link TomcatWebServer} instance.
* @param tomcat the underlying Tomcat server
* @param autoStart if the server should be started
*/
public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
initialize();
}
看下initialize方法:
TomcatWebServer.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();
destroySilently();
throw new WebServerException("Unable to start embedded Tomcat", ex);
}
}
}
至此,我们已经找到了关于SpringBoot是如何初始化并启动tomcat的所有新路历程,我也以我看源码的第一视角带着代价了解了源码。
思考与总结
如果只是想了解SpringBoot中tomcat的流程的那么,到这里就可以结束了。下面,我们再思考一个问题,我们在实际生产中也会将多service到一个tomcat下去同时启动。那么,tomcat是如何做到的呢?这就关乎到tomcat的运行原理了。所以,有兴趣的同学继续和我一起看下去。
Tomcat运行原理
导言
在上面, 我们在看TomcatServletWebServerFactory中的getWebServer时,不知道大家是否还记得这些代码:
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
// 1、创建tomcat实例
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);
// 2、将创建好的tomcat设置连接器connector
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
// 3、配置引擎
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
// 4、启动tomcat
return getTomcatWebServer(tomcat);
}
Connector连接器是什么鬼?为什么要放到Tomcat实例中?configureEngine()方法中,从Tomcat实例中获取到的Engine又是什么呢?
Engine
我们看到,在创建Tomcat实例的方法getWebServer中,在第4步时执行了configureEngine(tomcat.getEngine())方法获取了Engine,我们一起看下这个方法的源码了解下什么是Engine:
/**
* Access to the engine, for further customization.
* @return The engine
*/
public Engine getEngine() {
// 1、获取服务中的第一个service
Service service = getServer().findServices()[0];
// 2、如果获取到的service中已经在Engine容器中,则返回当Engine容器
if (service.getContainer() != null) {
return service.getContainer();
}
// 3、不存在,则创建一个容器并命名为tomcat后返回
Engine engine = new StandardEngine();
engine.setName( "Tomcat" );
engine.setDefaultHost(hostname);
engine.setRealm(createDefaultRealm());
service.setContainer(engine);
return engine;
}
根据源码的定义,看来Engine是一个服务类的容器了。那么,Engine这个容器的本质原理是什么呢?我们还是需要一起看下Engine的源码:
public interface Engine extends Container {
/**
* @return the default host name for this Engine.
*/
public String getDefaultHost();
/**
* Set the default hostname for this Engine.
*
* @param defaultHost The new default host
*/
public void setDefaultHost(String defaultHost);
/**
* @return the JvmRouteId for this engine.
*/
public String getJvmRoute();
/**
* Set the JvmRouteId for this engine.
*
* @param jvmRouteId the (new) JVM Route ID. Each Engine within a cluster
* must have a unique JVM Route ID.
*/
public void setJvmRoute(String jvmRouteId);
/**
* @return the <code>Service</code> with which we are associated (if any).
*/
public Service getService();
/**
* Set the <code>Service</code> with which we are associated (if any).
*
* @param service The service that owns this Engine
*/
public void setService(Service service);
}
我们可以看到,Engine接口中主要提供的方法基本上都是对于整个应用以及运行时JVM配置的一些方法。那么,我们再一起看下这个接口的描述信息:
/**
* An <b>Engine</b> is a Container that represents the entire Catalina servlet
* engine. It is useful in the following types of scenarios:
* <ul>
* <li>You wish to use Interceptors that see every single request processed
* by the entire engine.
* <li>You wish to run Catalina in with a standalone HTTP connector, but still
* want support for multiple virtual hosts.
* </ul>
* In general, you would not use an Engine when deploying Catalina connected
* to a web server (such as Apache), because the Connector will have
* utilized the web server's facilities to determine which Context (or
* perhaps even which Wrapper) should be utilized to process this request.
* <p>
* The child containers attached to an Engine are generally implementations
* of Host (representing a virtual host) or Context (representing individual
* an individual servlet context), depending upon the Engine implementation.
* <p>
* If used, an Engine is always the top level Container in a Catalina
* hierarchy. Therefore, the implementation's <code>setParent()</code> method
* should throw <code>IllegalArgumentException</code>.
*
* @author Craig R. McClanahan
*/
翻译一下,总结为以下几点:
1、Engine代表整个Catalina Servlet的容器引擎
2、运行Catalina至web应用时,一般不使用容器,当然也可以使用
3、如果使用容器引擎,那么,Engine必然是顶层容器
Container
那么, 既然Engine作为顶层容器,那么其父类Container为上级容器。我们再一起看下Container这个接口。
Container是容器的父接口,该容器的设计用的是典型的责任链的设计模式,它由四个自容器组件构成,分别是Engine、Host、Context、Wrapper。这四个组件是负责关系,存在包含关系。通常一个Servlet class对应一个Wrapper,如果有多个Servlet定义多个Wrapper,如果有多个Wrapper就要定义一个更高的Container,如Context。
Context 还可以定义在父容器 Host 中,Host 不是必须的,但是要运行 war 程序,就必须要 Host,因为 war 中必有 web.xml 文件,这个文件的解析就需要 Host 了,如果要有多个 Host 就要定义一个 top 容器 Engine 了。而 Engine 没有父容器了,一个 Engine 代表一个完整的 Servlet 引擎。
Tomcat 还有其它重要的组件,如安全组件 security、logger 日志组件、session、mbeans、naming 等其它组件。这些组件共同为 Connector 和 Container 提供必要的服务。
我们从Container的类图上可以看到,Container作为Engine的上级接口,和Engine平级的子接口还有Context、Host、Wapper。那么,四个接口的关系是什么呢?
Host、Context、Wapper、Engine的关系
//部分源码,其余部分省略。
public class Tomcat {
//设置连接器
public void setConnector(Connector connector) {
Service service = getService();
boolean found = false;
for (Connector serviceConnector : service.findConnectors()) {
if (connector == serviceConnector) {
found = true;
}
}
if (!found) {
service.addConnector(connector);
}
}
//获取service
public Service getService() {
return getServer().findServices()[0];
}
//设置Host容器
public void setHost(Host host) {
Engine engine = getEngine();
boolean found = false;
for (Container engineHost : engine.findChildren()) {
if (engineHost == host) {
found = true;
}
}
if (!found) {
engine.addChild(host);
}
}
//获取Engine容器
public Engine getEngine() {
Service service = getServer().findServices()[0];
if (service.getContainer() != null) {
return service.getContainer();
}
Engine engine = new StandardEngine();
engine.setName( "Tomcat" );
engine.setDefaultHost(hostname);
engine.setRealm(createDefaultRealm());
service.setContainer(engine);
return engine;
}
//获取server
public Server getServer() {
if (server != null) {
return server;
}
System.setProperty("catalina.useNaming", "false");
server = new StandardServer();
initBaseDir();
// Set configuration source
ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(new File(basedir), null));
server.setPort( -1 );
Service service = new StandardService();
service.setName("Tomcat");
server.addService(service);
return server;
}
//添加Context容器
public Context addContext(Host host, String contextPath, String contextName,
String dir) {
silence(host, contextName);
Context ctx = createContext(host, contextPath);
ctx.setName(contextName);
ctx.setPath(contextPath);
ctx.setDocBase(dir);
ctx.addLifecycleListener(new FixContextListener());
if (host == null) {
getHost().addChild(ctx);
} else {
host.addChild(ctx);
}
//添加Wrapper容器
public static Wrapper addServlet(Context ctx,
String servletName,
Servlet servlet) {
// will do class for name and set init params
Wrapper sw = new ExistingStandardWrapper(servlet);
sw.setName(servletName);
ctx.addChild(sw);
return sw;
}
}
通过Tomcat类的方法为突破口,我们可以知道如下的关系:
1、通过getServer()方法:一个Tomcat对应一个Server
2、通过getEngine()方法:一个Server下对应多个Service;一个Service代表我们的一个应用;一个Engine内只有一个Service
3、通过setHost()方法:一个Host内可以有多个Engine
4、通过addContext()方法:Context可以在Tomcat内拥有多个
5、通过addServlet()方法:Wrapper容器可以有多个
6、通过setConncetor()方法:connector是设置在service下的,一个service可以有多个connector
其中,Engine为特定的Service组件处理所有客户的请求,Host组件为特定的虚拟主机处理所有的客户请求。Context组件为特定的Web应用处理所有的客户请求。
连接器类元素代表了介于客户与服务之间的通信接口,负责将客户的请求发送给服务器,并将服务器的响应传递给客户。
运行原理图
1、用户点击网页内容,请求被发送到本机端口8080,被在那里监听的Coyote HTTP/1.1 Connector获得。
2、tomcat将此请求作为任务加入一个队列中,线程池中若干工作者线程从这个队列中获取任务,并把该请求交给它所在的Service的Engine来处理,并等待Engine的回应。
3、Engine获得请求localhost/test/index.jsp,匹配所有的虚拟主机Host。
4、Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机),名为localhost的Host获得请求/test/index.jsp,匹配它所拥有的所有的Context。Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为“ ”的Context去处理)。
5、path=“/test”的Context获得请求/index.jsp,在它的mapping table中寻找出对应的Servlet。Context匹配到URL PATTERN为*.jsp的Servlet,对应于JspServlet类。
6、构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet()或doPost().执行业务逻辑、数据存储等程序。
7、Context把执行完之后的HttpServletResponse对象返回给Host。
8、Host把HttpServletResponse对象返回给Engine。
9、Engine把HttpServletResponse对象返回Connector。
10、Connector把HttpServletResponse对象返回给客户Browser。