SpringBoot原始碼解析-內嵌Tomcat容器的啟動

吾乃上將軍邢道榮發表於2019-04-01

tomcat使用簡單示範

簡單回顧下內嵌tomcat使用,新建一個maven專案,匯入如下依賴

<dependencies>
    <dependency>
      <groupId>javax.annotation</groupId>
      <artifactId>javax.annotation-api</artifactId>
      <version>1.3.2</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-core</artifactId>
      <version>9.0.12</version>
      <scope>compile</scope>
      <exclusions>
        <exclusion>
          <artifactId>tomcat-annotations-api</artifactId>
          <groupId>org.apache.tomcat</groupId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-el</artifactId>
      <version>9.0.12</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
複製程式碼

新建一個servlet類,實現對應的方法。

public class HomeServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("request scheme: " + req.getScheme());
        resp.getWriter().print("hello tomcat");
    }

}

複製程式碼

在main函式中新增如下程式碼

    public static void main(String[] args) throws Exception {
        Tomcat tomcat = new Tomcat();
        //設定路徑
        tomcat.setBaseDir("d:tomcat/dir");
        tomcat.getHost().setAutoDeploy(false);

        Connector connector = new Connector();
        //設定埠
        connector.setPort(10086);
        tomcat.getService().addConnector(connector);

        Context context = new StandardContext();
        //設定context路徑
        context.setPath("");
        context.addLifecycleListener(new Tomcat.FixContextListener());
        tomcat.getHost().addChild(context);

        //新增servlet
        tomcat.addServlet("", "homeServlet", new HomeServlet());
        //設定servlet路徑
        context.addServletMappingDecoded("/", "homeServlet");

        tomcat.start();
        tomcat.getServer().await();
    }
複製程式碼

這樣的話一個簡單的tomcat伺服器就啟動了,開啟瀏覽器輸入localhost:10086,就可以看到servlet中的返回值。

springboot中tomcat容器的啟動

還記得前兩節講到springboot自動化配置裡面的配置檔案麼,配置檔案中有一個類,org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration ,進入這個類。

@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
		ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
		ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
		ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {
複製程式碼

發現上面有一個import註解,進入import註解匯入的類

	@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();
		}

	}
複製程式碼

根據上一節學習的判斷條件可以知道,import註解向spring容器中注入了一個TomcatServletWebServerFactory類,這個類我們先標記著。

回到main函式中,順著SpringApplication.run(Application.class, args);方法進入AbstractApplicationContext的refresh方法

	// Initialize other special beans in specific context subclasses.
	onRefresh();
複製程式碼

在onRefresh方法上發現一行註釋,在子類方法中初始化特殊的bean。tomcat容器應該算是一個特殊的bean了,所以我們進入子類的onRefresh方法。在子類ServletWebServerApplicationContext發現了這樣的程式碼。

	@Override
	protected void onRefresh() {
		super.onRefresh();
		try {
			createWebServer();
		}
		catch (Throwable ex) {
			throw new ApplicationContextException("Unable to start web server", ex);
		}
	}
複製程式碼

猜也能猜到,createWebServer方法就是tomcat初始化的地方了。所以進入方法一探究竟。

private void createWebServer() {
		WebServer webServer = this.webServer;
		ServletContext servletContext = getServletContext();
		//初始化進來,webServer和servletContext兩個物件都是null,所以進入if
		if (webServer == null && servletContext == null) {
			ServletWebServerFactory factory = getWebServerFactory();
			this.webServer = factory.getWebServer(getSelfInitializer());
		}
		else if (servletContext != null) {
			...
		}
		initPropertySources();
	}
複製程式碼

首先看一下getWebServerFactory方法。

	protected ServletWebServerFactory getWebServerFactory() {
		// Use bean names so that we don't consider the hierarchy
		String[] beanNames = getBeanFactory()
				.getBeanNamesForType(ServletWebServerFactory.class);
		if (beanNames.length == 0) {
			...
		}
		if (beanNames.length > 1) {
			...
		}
		return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
	}
複製程式碼

方法邏輯比較簡單,獲取容器中ServletWebServerFactory型別的例項,並校驗其數量,多了或者少了都不行,必須是正好1個。這個時候看一下上面通過自動化配置那邊匯入spring容器的TomcatServletWebServerFactory類,這個類就是ServletWebServerFactory的子類。所以在沒有其他配置的情況下,getWebServerFactory方法,獲取到的就是TomcatServletWebServerFactory類。

獲取到factory例項後,就來看一下factory的getWebServer方法。

		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);
		}
		配置context
		prepareContext(tomcat.getHost(), initializers);
		return getTomcatWebServer(tomcat);
複製程式碼

雖然比我們一開始那個示範要複雜許多,但是大致的邏輯還是很清晰的,不難看懂。(這個地方如果不理解的話,你需要補充一下tomcat的知識)

進入getTomcatWebServer方法。

protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
		return new TomcatWebServer(tomcat, getPort() >= 0);
	}

	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 {
		synchronized (this.monitor) {
			try {
				addInstanceIdToEngineName();

				Context context = findContext();
				context.addLifecycleListener((event) -> {
					if (context.equals(event.getSource())
							&& Lifecycle.START_EVENT.equals(event.getType())) {
						removeServiceConnectors();
					}
				});
				this.tomcat.start();
				...
				startDaemonAwaitThread();
			}
			...
		}
	}
複製程式碼

在getTomcatWebServer方法中,發現了tomcat啟動相關的程式碼,所以這個地方就是tomcat容器啟動的地方啦。不過如果你用debug的話,你會發現這個地方即使tomcat啟動過後,依然無法訪問。因為在啟動前spring框架還做了一件事。

				Context context = findContext();
				context.addLifecycleListener((event) -> {
					if (context.equals(event.getSource())
							&& Lifecycle.START_EVENT.equals(event.getType())) {
						//移除tomcat容器的聯結器connector
						removeServiceConnectors();
					}
				});
複製程式碼

因為這個時候作為一個特殊的bean,tomcat容器需要優先初始化,但是此時其他bean還沒有初始化完成,連線進來後是無法處理的。所以spring框架在這個地方移除了聯結器。

那麼被移除的聯結器在那個地方啟動的呢?在AbstractApplicationContext的refresh方法中,onRefresh方法後面還有一個方法finishRefresh方法。進入子類的這個方法(進入這個方法之前,所有的非lazy屬性的bean已經全部完成了初始化)

	@Override
	protected void finishRefresh() {
		super.finishRefresh();
		WebServer webServer = startWebServer();
		if (webServer != null) {
			publishEvent(new ServletWebServerInitializedEvent(webServer, this));
		}
	}

	private WebServer startWebServer() {
		WebServer webServer = this.webServer;
		if (webServer != null) {
			webServer.start();
		}
		return webServer;
	}

	public void start() throws WebServerException {
		...
				addPreviouslyRemovedConnectors();
				Connector connector = this.tomcat.getConnector();
				if (connector != null && this.autoStart) {
					performDeferredLoadOnStartup();
				}
				...
		}
	}
複製程式碼

在這個方法中,我們找到了被移除的connector。spring框架將剛剛移除得到聯結器又放到tomcat容器中,並且啟用了他,這樣的話tomcat就可以被訪問到了。

tomcat的啟動到這兒我們已經瞭解了,不知道大家有沒有發現一個問題,就是我們並沒有看到類似示例中新增servlet和設定servlet路徑相關的程式碼。那這部分程式碼在哪裡呢?

回到剛剛factory的getWebServer方法。這個方法中傳入了一個引數getSelfInitializer()我們看一下這個引數是啥。

	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);
		}
	}
複製程式碼

這個lambda表示式應該還很好理解吧,返回了一個ServletContextInitializer例項,該例項的onStartup方法就是呼叫了這邊的selfInitialize方法。這個selfInitialize方法裡,最關鍵的就是getServletContextInitializerBeans方法了。但是我們從這邊分析程式碼的話,其實不太看得出來getServletContextInitializerBeans到底獲取到了那些類,所以可以取巧一下,使用IDEA的debug功能。藉助debug我們看到了這邊獲取到的幾個類,關鍵的是DispatcherServletRegistrationBean。也就是這個地方會呼叫DispatcherServletRegistrationBean的onStartup方法。

那麼他的onStartup到底幹了那些事呢?

	@Override
	public final void onStartup(ServletContext servletContext) throws ServletException {
		String description = getDescription();
		if (!isEnabled()) {
			logger.info(StringUtils.capitalize(description)
					+ " was not registered (disabled)");
			return;
		}
		register(description, servletContext);
	}

	@Override
	protected final void register(String description, ServletContext servletContext) {
		D registration = addRegistration(description, servletContext);
		if (registration == null) {
			logger.info(StringUtils.capitalize(description) + " was not registered "
					+ "(possibly already registered?)");
			return;
		}
		configure(registration);
	}

	@Override
	protected ServletRegistration.Dynamic addRegistration(String description,
			ServletContext servletContext) {
		String name = getServletName();
		//這個地方將servlet新增進了context
		return servletContext.addServlet(name, this.servlet);
	}

	@Override
	protected void configure(ServletRegistration.Dynamic registration) {
		super.configure(registration);
		String[] urlMapping = StringUtils.toStringArray(this.urlMappings);
		if (urlMapping.length == 0 && this.alwaysMapUrl) {
			urlMapping = DEFAULT_MAPPINGS;
		}
		if (!ObjectUtils.isEmpty(urlMapping)) {
		//這個方法則對servlet的路徑進行了配置
			registration.addMapping(urlMapping);
		}
		registration.setLoadOnStartup(this.loadOnStartup);
		if (this.multipartConfig != null) {
			registration.setMultipartConfig(this.multipartConfig);
		}
	}
複製程式碼

既然知道了ServletContextInitializer的作用,那麼我們就追蹤一下這個ServletContextInitializer被放置到了什麼地方,何時呼叫他的方法。

	@Override
	public WebServer getWebServer(ServletContextInitializer... initializers) {
		...
		prepareContext(tomcat.getHost(), initializers);
		return getTomcatWebServer(tomcat);
	}

	protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
		...
		ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
		...
		configureContext(context, initializersToUse);
		...
	}

	protected void configureContext(Context context,
			ServletContextInitializer[] initializers) {
		TomcatStarter starter = new TomcatStarter(initializers);
		if (context instanceof TomcatEmbeddedContext) {
			TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
			embeddedContext.setStarter(starter);
			embeddedContext.setFailCtxIfServletStartFails(true);
		}
		context.addServletContainerInitializer(starter, NO_CLASSES);
		...
	}
複製程式碼

可以看到ServletContextInitializer被包裝成了一個TomcatStarter放入了context中。在context的start方法裡,我們就可以看到initializers的啟動(這個地方涉及到tomcat容器的啟動,如果不熟悉的話可以回顧下)。

    @Override
    protected synchronized void startInternal() throws LifecycleException {
            ...
            // Call ServletContainerInitializers
            for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
                initializers.entrySet()) {
                try {
                    entry.getKey().onStartup(entry.getValue(),
                            getServletContext());
                } catch (ServletException e) {
                    log.error(sm.getString("standardContext.sciFail"), e);
                    ok = false;
                    break;
                }
            }
            ...
    }
複製程式碼

總結

經過這幾輪的分析,從SpringApplication的啟動,到自動化配置,再到今天的tomcat容器的啟動。我們已經窺探到了整個springboot框架的全貌。所以後面就需要對常用功能定點學習了。


返回目錄

相關文章