图文结合教你SpringBoot中初始化Tomcat以及Tomcat运行原理

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&lt;&gt;();
		//设置系统属性『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。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
当前课程商城项目的实战源码是我发布在 GitHub 上的开源项目 newbee-mall (新蜂商城),目前已有 9900 多个 Star,本课程是一个 Spring Boot 技术栈的实战类课程,课程共分为 3 大部分,前面两个部分为基础环境准备和相关概念介绍,第三个部分是 Spring Boot 商城项目功能的讲解,让大家实际操作并实践上手一个大型的线上商城项目,并学习到一定的开发经验以及其的开发技巧。商城项目所涉及的功能结构图整理如下: 作者寄语本课程录制于2019年,距今已有一段时间。期间,Spring Boot技术栈也有一些版本升级,比如Spring Boot 2.7.x发版、Spring Boot 3.x版本正式版本。对于这些情况,笔者会在本课程实战项目的开源仓库创建不同的代码分支,保持实战项目的源码更新,保证读者朋友们不会学习过气的知识点。新蜂商城的优化和迭代工作不会停止,不仅仅是功能的优化,在技术栈上也会不断的增加,截止2023年,新蜂商城已经发布了 7 个重要的版本,版本记录及开发计划如下图所示。 课程特色 对新手开发者十分友好,无需复杂的操作步骤,仅需 2 秒就可以启动这个完整的商城项目最终的实战项目是一个企业级别的 Spring Boot 大型项目,对于各个阶段的 Java 开发者都是极佳的选择实践项目页面美观且实用,交互效果完美程详细开发程详细完整、文档资源齐全代码+讲解+演示网站全方位保证,向 Hello World 程说拜拜技术栈新颖且知识点丰富,学习后可以提升大家对于知识的理解和掌握,可以进一步提升你的市场竞争力 课程预览 以下为商城项目的页面和功能展示,分别为:商城首页 1商城首页 2购物车订单结算订单列表支付页面后台管理系统登录页商品管理商品编辑

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值