SpringBootApplication是如何啟動Tomcat的? | 破解SpringBoot Tomcat啟動之謎 !

Moshow鄭鍇發表於2020-04-07

前言

我們都知道,SpringBoot內建了容器Tomcat,可以直接啟動WebServletServer,那麼SpringBoot是如何啟動Tomcat的?
本文從Main方法入手,從SpringApplication.run跟到ServletWebServerApplicationContext 再到TomcatServletWebServerFactory,破解SpringBoot Tomcat啟動之謎 !!!

  • springboot版本 : 2.0.5.RELEASE
  • 追蹤工具 : IDEA

Main方法

@SpringBootApplication
public class DemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(DemoApplication .class,args);
	}
}

Run方法

	/**
	 * Run the Spring application, creating and refreshing a new
	 * {@link ApplicationContext}.
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return a running {@link ApplicationContext}
	 * 解釋 by zhengkai.blog.csdn.net
	 */
	public ConfigurableApplicationContext run(String... args) {
	    //zhengkai.blog.csdn.net
		//定義個StopWatch,Wathch是表,定義個表來監控一下執行的效能,用了多少ms
		StopWatch stopWatch = new StopWatch();
		//開始計時
		stopWatch.start();
		//定義一個可配置的上下文,ConfigurableApplicationContext介面extends了ApplicationContext+Lifecycle+Closeable,可以被大多數的應用上下文實現,為配置應用上下文提供便利.
		ConfigurableApplicationContext context = null;
		//ExceptionReporter明顯是一個錯誤報告,for SpringApplication的starup error,基本只是啟動的報錯(不包括啟動後的報錯)
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		//RunListeners,執行監聽器SpringApplicationRunListeners=List<SpringApplicationRunListener>,是個集合來的
		//包括starting()environmentPrepared()contextPrepared()contextLoaded()started()running()failed()事件的監聽
		SpringApplicationRunListeners listeners = getRunListeners(args);
		//進入監聽器的staring階段
		listeners.starting();
		try {
		    //獲取SpringBootApplication的執行引數,如果你是java -jar xxx.jar --server.context-path=/mypath,則可以動態獲取
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(
					args);
			//開始準備環境,傳入監聽器和執行引數,獲得環境變數,可以getSystemEnvironment和getSystemProperties,可以setActiveProfiles設定啟用的配置
			ConfigurableEnvironment environment = prepareEnvironment(listeners,
					applicationArguments);
		    //從spring.beaninfo.ignore獲取需要忽然的bean列表
			configureIgnoreBeanInfo(environment);
			//列印banner,控制檯輸出,Mode有三種:OFF/console/log file
			Banner printedBanner = printBanner(environment);
			//***開始建立上下文,這個比較重點,下面列出來單獨說
			//根據webApplicationType,獲取對應的CONTEXT_CLASS,分SERVLET(Web)/REACTIVE(webflux)/default(spring).開始建立上下文
			context = createApplicationContext();
			//裡面實際上建立了SpringFactoriesInstances
			exceptionReporters = getSpringFactoriesInstances(
					SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			//這裡才是真正建立上下文的地方:
			//context.setEnvironment設定環境變數.setResourceLoader設定資源載入器,用context來初始化所有initializer
			//開始logStartupInfo輸出日誌,registerArguments註冊引數,registerBanner註冊控制檯輸出
			//createBeanDefinitionLoader建立bean定義載入器,最後load到所有監聽器listener上
			prepareContext(context, environment, listeners, applicationArguments,
					printedBanner);
			//這裡為context註冊一個ShutdownHooK,也就是關掉應用的時候要做什麼
			refreshContext(context);
			//這是一個空方法,可供改造???
			afterRefresh(context, applicationArguments);
			//好了,計時停止,看下啟動用了多少ms
			stopWatch.stop();
			if (this.logStartupInfo) {
				//輸出SpringBoot那些啟動資訊,日誌,用了多少ms等,你平常看到的那堆
				new StartupInfoLogger(this.mainApplicationClass)
						.logStarted(getApplicationLog(), stopWatch);
			}
			//進入監聽器的啟動完成事件
			listeners.started(context);
			//分ApplicationRunner和CommandLineRunner,callback run
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
		    //萬一啟動過程中有報錯,就handleExitCode然後reportFailure,然後重新丟擲RuntimeException
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
		    //進入監聽器的running執行中階段
			listeners.running(context);
		}
		catch (Throwable ex) {
		    //萬一有報錯怎麼辦,跟上面一樣......
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		//搞定,,,返回上下文!
		return context;
	}

抓住createApplicationContext

大致過了一遍之後,我覺得我們應該關注這個方法,createApplicationContext()

	/**
	 * 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 {
				switch (this.webApplicationType) {
				case SERVLET:
					//DEFAULT_WEB_CONTEXT_CLASS=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext
					contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS);
					break;
				case REACTIVE:
					//org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext
					contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
					break;
				default:
					//org.springframework.context.annotation.AnnotationConfigApplicationContext
					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);
	}

CONTEXT_CLASS分析

DEFAULT_WEB_CONTEXT_CLASS=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext

DEFAULT_WEB_CONTEXT_CLASS一路跟蹤到extends的類,直到發現新大陸:

//沒什麼可以看
public class AnnotationConfigServletWebServerApplicationContext
		extends ServletWebServerApplicationContext implements AnnotationConfigRegistry{
}		
//重點: 整理by zhengkai.blog.csdn.net
public class ServletWebServerApplicationContext extends GenericWebApplicationContext
		implements ConfigurableWebServerApplicationContext{
		//放眼一望,這個裡面也相當豐富,很多內容
		//發現WebServer
		private volatile WebServer webServer;
		//發現ServletConfig
		private ServletConfig servletConfig;
		//發現建立WebServer的方法
		private void createWebServer() {
			WebServer webServer = this.webServer;
			//Servlet上下文
			ServletContext servletContext = getServletContext();
			//有server和上下文,則不需要建立,直接從ServletWebServer工廠裡面拿
			if (webServer == null && servletContext == null) {
				//ServletWebServerFactory工廠,應該有很多東西,提到下面來分析
				ServletWebServerFactory factory = getWebServerFactory();
				this.webServer = factory.getWebServer(getSelfInitializer());
			}
			else if (servletContext != null) {
				try {
				   //沒有話,從ServletContextInitializer開始一個吧
					getSelfInitializer().onStartup(servletContext);
				}
				catch (ServletException ex) {
					throw new ApplicationContextException("Cannot initialize servlet context",
							ex);
				}
		}
		initPropertySources();
	}
}	

//沒啥子
public class GenericWebApplicationContext extends GenericApplicationContext
		implements ConfigurableWebApplicationContext, ThemeSource {
}	

//沒啥子
public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
}

//AbstractApplicationContext 這個類也還是挺大的,雖然是抽象類,但是很多方法,超級豐富,值得關注
public abstract class AbstractApplicationContext extends DefaultResourceLoader
		implements ConfigurableApplicationContext {   
}

ServletWebServerFactory工廠

IDEA下ctrl+alt+B,直接檢視ServletWebServerFactory工廠的實現,發現:
在這裡插入圖片描述
Tomcat啟動之謎

看到上面幾個工廠類的時候,這個謎題已經破解了。同時Jetty,Undertow的啟動之謎也揭曉了。

	/**
	 * 建立並獲取TomcatWebServer,TomcatServletWebServerFactory,整理 by zhengkai.blog.csdn.net
	 */
	@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);
		//getTomcatWebServer=return new TomcatWebServer(tomcat, getPort() >= 0);
		return getTomcatWebServer(tomcat);
	}
	/**
	 * 建立並獲取JettyWebServer,JettyServletWebServerFactory,整理 by zhengkai.blog.csdn.net
	 */
	@Override
	public WebServer getWebServer(ServletContextInitializer... initializers) {
		JettyEmbeddedWebAppContext context = new JettyEmbeddedWebAppContext();
		int port = (getPort() >= 0) ? getPort() : 0;
		InetSocketAddress address = new InetSocketAddress(getAddress(), port);
		Server server = createServer(address);
		configureWebAppContext(context, initializers);
		server.setHandler(addHandlerWrappers(context));
		this.logger.info("Server initialized with port: " + port);
		if (getSsl() != null && getSsl().isEnabled()) {
			customizeSsl(server, address);
		}
		for (JettyServerCustomizer customizer : getServerCustomizers()) {
			customizer.customize(server);
		}
		if (this.useForwardHeaders) {
			new ForwardHeadersCustomizer().customize(server);
		}
		//getJettyWebServer=return new JettyWebServer(server, getPort() >= 0);
		return getJettyWebServer(server);
	}
	/**
	 * 建立並獲取UndertowWebServer,UndertowServletWebServerFactory,整理 by zhengkai.blog.csdn.net
	 */
	@Override
	public WebServer getWebServer(ServletContextInitializer... initializers) {
		DeploymentManager manager = createDeploymentManager(initializers);
		int port = getPort();
		Builder builder = createBuilder(port);
		//getUndertowWebServer=return new UndertowServletWebServer(builder, manager, getContextPath(),isUseForwardHeaders(), port >= 0, getCompression(), getServerHeader());
		return getUndertowWebServer(builder, manager, port);
	}
	//Undertow做多了一層Builder的封裝
	private Builder createBuilder(int port) {
		Builder builder = Undertow.builder();
		if (this.bufferSize != null) {
			builder.setBufferSize(this.bufferSize);
		}
		if (this.ioThreads != null) {
			builder.setIoThreads(this.ioThreads);
		}
		if (this.workerThreads != null) {
			builder.setWorkerThreads(this.workerThreads);
		}
		if (this.directBuffers != null) {
			builder.setDirectBuffers(this.directBuffers);
		}
		if (getSsl() != null && getSsl().isEnabled()) {
			customizeSsl(builder);
		}
		else {
			builder.addHttpListener(port, getListenAddress());
		}
		for (UndertowBuilderCustomizer customizer : this.builderCustomizers) {
			customizer.customize(builder);
		}
		return builder;
	}

Tomcat vs Jetty vs Undertow

每個Web容器都有自己的設計和元件:

  • Tomcat是Connector
  • Jetty是Handler
  • Undertow是Builder

單純比較 Tomcat 與 Jetty 的效能意義不是很大,只能說在某種使用場景下,它表現的各有差異。因為它們面向的使用場景不盡相同。從架構上來看 Tomcat 在處理少數非常繁忙的連線上更有優勢,也就是說連線的生命週期如果短的話,Tomcat 的總體效能更高。

而 Jetty 剛好相反,Jetty 可以同時處理大量連線而且可以長時間保持這些連線。例如像一些 web 聊天應用非常適合用 Jetty 做伺服器,像淘寶的 web 旺旺就是用 Jetty 作為 Servlet 引擎。

另外 Jetty 預設使用的是 NIO 技術,在處理 I/O 請求上更佔優勢,Tomcat 預設使用的是 BIO,在處理靜態資源時,Tomcat 的效能不如 Jetty。

=。=Undertow可能大家陌生有點,是一個Java開發的靈活的高效能Web伺服器,提供包括阻塞和基於NIO的非阻塞機制。Undertow是紅帽公司的開源產品,是Wildfly預設的Web伺服器。SpringBoot2中可以將Web伺服器切換到Undertow來提高應用效能。

Undertow認為它的運用場景是在IO密集型的系統應用中,簡單點的講,就是結合了Tomcat和Jetty的優點,類似Netty的強大

相關文章