您的位置:首頁>正文

Spring Boot啟動過程(四):Spring Boot內嵌Tomcat啟動

之前在Spring Boot啟動過程(二)提到過createEmbeddedServletContainer創建了內嵌的Servlet容器, 我用的是默認的Tomcat。

private void createEmbeddedServletContainer { EmbeddedServletContainer localContainer = this.embeddedServletContainer; ServletContext localServletContext = getServletContext; if (localContainer == null && localServletContext == null) { EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory; this.embeddedServletContainer = containerFactory .getEmbeddedServletContainer(getSelfInitializer); } else if (localServletContext != null) { try { getSelfInitializer.onStartup(localServletContext); } catch (ServletException ex) { throw new ApplicationContextException("Cannot initialize servlet context", ex); } } initPropertySources; }

getEmbeddedServletContainerFactory方法中調用了ServerProperties, 從ServerProperties的實例方法customize可以看出Springboot支援三種內嵌容器的定制化配置:Tomcat、Jetty、Undertow。

這裡直接說TomcatEmbeddedServletContainerFactory的getEmbeddedServletContainer方法了, 原因在前面那篇裡說過了。 不過首先是getSelfInitializer方法先執行的:

private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer { return new ServletContextInitializer { @Override public void onStartup(ServletContext servletContext) throws ServletException { selfInitialize(servletContext); } }; }

將初始化的ServletContextInitializer傳給了getEmbeddedServletContainer方法。 進入了getEmbeddedServletContainer方法直接就是產生實體了一個Tomcat:

Tomcat tomcat = new Tomcat;

然後生成一個臨時目錄, 並tomcat.setBaseDir, setBaseDir方法的注釋說Tomcat需要一個目錄用於暫存檔案並且它應該是第一個被調用的方法;如果方法沒有被調用會使用默認的幾個位置system properties - catalina.base, catalina.home - $PWD/tomcat.$PORT, 另外/tmp從安全角度來說不建議。

接著:

Connector connector = r(this.protocol);

創建Connector過程中, 靜態代碼塊:單獨抽出來寫了。

RECYCLE_FACADES屬性可以通過啟動參數JAVA_OPTS來配置: -Dorg.apache.catalina.connector.RECYCLE_FACADES=, 預設是false, 配置成true可以提高安全性但同時性能會有些損耗, 參考:http://tomcat.apache.org/tomcat-7.0-doc/config/systemprops.html和http://bztax.gov.cn/docs/security-howto.html。 其他屬性不細說了, Connector構造的邏輯主要是在NIO和APR選擇中選擇一個協議, 我的是org.apache.coyote.http11.Http11NioProtocol, 然後反射創建實例並強轉為ProtocolHandler。 關於apr, 似乎是更native, 性能據說更好, 但我沒測, 相關文檔可參考:http://tomcat.apache.org/tomcat-8.5-doc/apr.html。 這裡簡單提一下coyote, 它的主要作用是將socket接受的資訊封裝為request和response並提供給上Servlet容器, 進行上下層之間的溝通, 文檔我沒找到比較新的:http://tomcat.apache.org/tomcat-4.1-doc/config/coyote.html。 STRICT_SERVLET_COMPLIANCE也是啟動參數控制, 預設是false, 配置項是org.apache.catalina.STRICT_SERVLET_COMPLIANCE, 預設情況下會設置URIEncoding = "UTF-8"和URIEncodingLower = URIEncoding.toLowerCase(Locale.ENGLISH), 相關詳細介紹可參考:http://tomcat.apache.org/tomcat-7.0-doc/config/systemprops.html。 Connector的創建過程比較關鍵, 容我單獨寫一篇吧。

Connector實例創建好了之後tomcat.getService.addConnector(connector), getService的getServer中new了一個StandardServer, StandardServer的初始化主要是創建了globalNamingResources(globalNamingResources主要用於管理明明上下文和JDNI上下文),

並根據catalina.useNaming判斷是否註冊NamingContextListener監聽器給lifecycleListeners。 創建Server之後initBaseDir, 先讀取catalina.home配置System.getProperty(Globals.CATALINA_BASE_PROP), 如果沒取到則使用之前生成的臨時目錄, 這段直接看代碼吧:

protected void initBaseDir { String catalinaHome = System.getProperty(Globals.CATALINA_HOME_PROP); if (basedir == null) { basedir = System.getProperty(Globals.CATALINA_BASE_PROP); } if (basedir == null) { basedir = catalinaHome; } if (basedir == null) { // Create a temp dir. basedir = System.getProperty("user.dir") + "/tomcat." + port; } File baseFile = new File(basedir); baseFile.mkdirs; try { baseFile = baseFile.getCanonicalFile; } catch (IOException e) { baseFile = baseFile.getAbsoluteFile; } server.setCatalinaBase(baseFile); System.setProperty(Globals.CATALINA_BASE_PROP, baseFile.getPath); basedir = baseFile.getPath; if (catalinaHome == null) { server.setCatalinaHome(baseFile); } else { File homeFile = new File(catalinaHome); homeFile.mkdirs; try { homeFile = homeFile.getCanonicalFile; } catch (IOException e) { homeFile = homeFile.getAbsoluteFile; } server.setCatalinaHome(homeFile); } System.setProperty(Globals.CATALINA_HOME_PROP, server.getCatalinaHome.getPath); }

然後又產生實體了個StandardService, 代碼並沒有什麼特別的:

service = new StandardService; service.setName("Tomcat"); server.addService( service )

server.addService( service )這裡除了發佈了一個PropertyChangeEvent事件, 也沒做什麼特別的, 最後返回這個server。 addConnector的邏輯和上面addService沒什麼區別。 然後是customizeConnector, 這裡設置了Connector的埠、編碼等資訊, 並將“bindOnInit”和對應值false寫入了最開頭說的靜態代碼塊中的replacements集合, IntrospectionUtils.setProperty(protocolHandler, repl, value)通過反射的方法將protocolHandler實現物件的setBindOnInit存在的情況下(拼字串拼出來的)set為前面的false, 這個方法裡有大量的判斷比如參數類型及setter的參數類型, 比如返回數值型別以及沒找到還會try a setProperty("name", "value")等, setProperty可以處理比如AbstractEndpoint中有個HashMap attributes的屬性時會attributes.put(name, value)。 如果是ssl還會執行customizeSsl方法, 設置一些SSL用的屬性比如協定比如秘鑰還有可以用上秘鑰倉庫等。

如果配置了壓縮, 這裡還會給協議的相關setter設置值。 tomcat.setConnector(connector)不解釋。 tomcat.getHost.setAutoDeploy(false), getHost方法中創建了StandardHost並設置host名(例如localhost), 並getEngine.addChild( host );然後設置host的自動部署。 configureEngine(tomcat.getEngine), getEngine中如果engine為null就初始化標準引擎, 設置名字為Tomcat,設置Realm和service.setContainer(engine), 不過這裡engine已經在getHost初始化過了所以直接返回;configureEngine方法先設置引擎的後臺進程延遲, 並將引擎的Value物件註冊給引擎的pipeline, 此時尚無value物件實例。 這裡簡單說明一下:value物件在Tomcat的各級容器中都有標準類型, 並且各級容器都有一個pipeline, 在請求處理過程中會從各級的第一個value物件開始依次執行一遍, value用於加入到對應的各級容器的邏輯, 預設有一個標注value實現, 名字類似StandardHostValue。

prepareContext(tomcat.getHost, initializers), initializers這裡是AnnotationConfigEmbeddedWebApplicationContext, Context級的根;準備Context的過程主要設置Base目錄, new一個TomcatEmbeddedContext並在構造中判斷了下loadOnStartup方法是否被重寫;註冊一個FixContextListener監聽,

這個監聽用於設置context的配置狀態以及是否加入登錄驗證的邏輯;context.setParentClassLoader;設置各種語言的編碼映射, 我這裡是en和fr設置為UTF-8, 此處可以使用設定檔org/apache/catalina/util/CharsetMapperDefault .properties;設置是否使用相對位址重定向useRelativeRedirects=false, 此屬性應該是Tomcat 8.0.30版本加上的;接著就是初始化webapploader,這裡和完整版的Tomcat有點不一樣, 它用的是虛擬機器的方式, 會將載入類向上委託loader.setDelegate(true), context.setLoader(loader);之後就開始創建Wapper了, 至此engine, host, context及wrapper四個層次的容器都創建完了:

private void addDefaultServlet(Context context) { Wrapper defaultServlet = context.createWrapper; defaultServlet.setName("default"); defaultServlet.setServletClass("org.apache.catalina.servlets.DefaultServlet"); defaultServlet.addInitParameter("debug", "0"); defaultServlet.addInitParameter("listings", "false"); defaultServlet.setLoadOnStartup(1); // Otherwise the default location of a Spring DispatcherServlet cannot be set defaultServlet.setOverridable(true); context.addChild(defaultServlet); addServletMapping(context, "/", "default"); }

connector從socket接收的資料, 解析成HttpServletRequest後就會經過這幾層容器, 有容器各自的Value物件鏈依次處理。

接著是是否註冊jspServlet,jasperInitializer和StoreMergedWebXmlListener我這裡是都沒有的。 接著的mergeInitializers方法:

protected final ServletContextInitializer mergeInitializers( ServletContextInitializer... initializers) { List mergedInitializers = new ArrayList; mergedInitializers.addAll(Arrays.asList(initializers)); mergedInitializers.addAll(this.initializers); return mergedInitializers .toArray(new ServletContextInitializer[mergedInitializers.size()]); }

configureContext(context, initializersToUse)對context做了些設置工作, 包括TomcatStarter(產生實體並set給context),LifecycleListener,contextValue,errorpage,Mime,session超時持久化等以及一些自訂工作:

TomcatStarter starter = new TomcatStarter(initializers); if (context instanceof TomcatEmbeddedContext) { // Should be true ((TomcatEmbeddedContext) context).setStarter(starter); } context.addServletContainerInitializer(starter, NO_CLASSES); for (LifecycleListener lifecycleListener : this.contextLifecycleListeners) { context.addLifecycleListener(lifecycleListener); } for (Valve valve : this.contextValves) { context.getPipeline.addValve(valve); } for (ErrorPage errorPage : getErrorPages) { new TomcatErrorPage(errorPage).addToContext(context); } for (MimeMappings.Mapping mapping : getMimeMappings) { context.addMimeMapping(mapping.getExtension, mapping.getMimeType); }

Session如果不需要持久化會註冊一個DisablePersistSessionListener。 其他定制化操作是通過TomcatContextCustomizer的實現類實現的:

context配置完了作為child add給host,add時給context註冊了個記憶體洩漏跟蹤的監聽MemoryLeakTrackingListener。 postProcessContext(context)方法是空的, 留給子類重寫用的。

getEmbeddedServletContainer方法的最後一行:return getTomcatEmbeddedServletContainer(tomcat)。

protected TomcatEmbeddedServletContainer getTomcatEmbeddedServletContainer( Tomcat tomcat) { return new TomcatEmbeddedServletContainer(tomcat, getPort >= 0); }

TomcatEmbeddedServletContainer的構造函數:

public TomcatEmbeddedServletContainer(Tomcat tomcat, boolean autoStart) { Assert.notNull(tomcat, "Tomcat Server must not be null"); this.tomcat = tomcat; this.autoStart = autoStart; initialize; }

initialize的第一個方法addInstanceIdToEngineName對全域原子變數containerCounter+1, 由於初始值是-1, 所以addInstanceIdToEngineName方法內後續的獲取引擎並設置名字的邏輯沒有執行:

private void addInstanceIdToEngineName { int instanceId = containerCounter.incrementAndGet; if (instanceId > 0) { Engine engine = this.tomcat.getEngine; engine.setName(engine.getName + "-" + instanceId); } }

initialize的第二個方法removeServiceConnectors, 將上面new的connection以service(這裡是StandardService[Tomcat])做key保存到private final Map serviceConnectors中,並將StandardService中的protected Connector connectors與service解綁(connector.setService((Service)null);),解綁後下面利用LifecycleBase啟動容器就不會啟動到Connector了。

之後是this.tomcat.start,這段比較複雜,我單獨總結一篇吧。

TomcatEmbeddedServletContainer的初始化,接下來是rethrowDeferredStartupExceptions,這個方法檢查初始化過程中的異常,如果有直接在主執行緒拋出,檢查方法是TomcatStarter中的private volatile Exception startUpException,這個值是在Context啟動過程中記錄的:

@Override public void onStartup(Set> classes, ServletContext servletContext) throws ServletException { try { for (ServletContextInitializer initializer : this.initializers) { initializer.onStartup(servletContext); } } catch (Exception ex) { this.startUpException = ex; // Prevent Tomcat from logging and re-throwing when we know we can // deal with it in the main thread, but log for information here. if (logger.isErrorEnabled) { logger.error("Error starting Tomcat context. Exception: " + ex.getClass.getName + ". Message: " + ex.getMessage); } } }

Context context = findContext:

private Context findContext { for (Container child : this.tomcat.getHost.findChildren) { if (child instanceof Context) { return (Context) child; } } throw new IllegalStateException("The host does not contain a Context"); }

綁定命名的上下文和classloader,不成功也無所謂:

try { ContextBindings.bindClassLoader(context, getNamingToken(context), getClass.getClassLoader); } catch (NamingException ex) { // Naming is not enabled. Continue }

startDaemonAwaitThread方法的注釋是:與Jetty不同,Tomcat所有的執行緒都是守護執行緒,所以創建一個非守護執行緒來避免服務到這就shutdown了:

private void startDaemonAwaitThread { Thread awaitThread = new Thread("container-" + (containerCounter.get)) { @Override public void run { TomcatEmbeddedServletContainer.this.tomcat.getServer.await; } }; awaitThread.setContextClassLoader(getClass.getClassLoader); awaitThread.setDaemon(false); awaitThread.start; }

回到EmbeddedWebApplicationContext,initPropertySources方法,用初始化好的servletContext完善環境變數:

/** * {@inheritDoc} *

Replace {@code Servlet}-related property sources. */ @Override protected void initPropertySources { ConfigurableEnvironment env = getEnvironment; if (env instanceof ConfigurableWebEnvironment) { ((ConfigurableWebEnvironment) env).initPropertySources(this.servletContext, null); } }

createEmbeddedServletContainer就結束了,內嵌容器的啟動過程至此結束。

==========================================================

咱最近用的github:https://github.com/saaavsaaa

將上面new的connection以service(這裡是StandardService[Tomcat])做key保存到private final Map serviceConnectors中,並將StandardService中的protected Connector connectors與service解綁(connector.setService((Service)null);),解綁後下面利用LifecycleBase啟動容器就不會啟動到Connector了。

之後是this.tomcat.start,這段比較複雜,我單獨總結一篇吧。

TomcatEmbeddedServletContainer的初始化,接下來是rethrowDeferredStartupExceptions,這個方法檢查初始化過程中的異常,如果有直接在主執行緒拋出,檢查方法是TomcatStarter中的private volatile Exception startUpException,這個值是在Context啟動過程中記錄的:

@Override public void onStartup(Set> classes, ServletContext servletContext) throws ServletException { try { for (ServletContextInitializer initializer : this.initializers) { initializer.onStartup(servletContext); } } catch (Exception ex) { this.startUpException = ex; // Prevent Tomcat from logging and re-throwing when we know we can // deal with it in the main thread, but log for information here. if (logger.isErrorEnabled) { logger.error("Error starting Tomcat context. Exception: " + ex.getClass.getName + ". Message: " + ex.getMessage); } } }

Context context = findContext:

private Context findContext { for (Container child : this.tomcat.getHost.findChildren) { if (child instanceof Context) { return (Context) child; } } throw new IllegalStateException("The host does not contain a Context"); }

綁定命名的上下文和classloader,不成功也無所謂:

try { ContextBindings.bindClassLoader(context, getNamingToken(context), getClass.getClassLoader); } catch (NamingException ex) { // Naming is not enabled. Continue }

startDaemonAwaitThread方法的注釋是:與Jetty不同,Tomcat所有的執行緒都是守護執行緒,所以創建一個非守護執行緒來避免服務到這就shutdown了:

private void startDaemonAwaitThread { Thread awaitThread = new Thread("container-" + (containerCounter.get)) { @Override public void run { TomcatEmbeddedServletContainer.this.tomcat.getServer.await; } }; awaitThread.setContextClassLoader(getClass.getClassLoader); awaitThread.setDaemon(false); awaitThread.start; }

回到EmbeddedWebApplicationContext,initPropertySources方法,用初始化好的servletContext完善環境變數:

/** * {@inheritDoc} *

Replace {@code Servlet}-related property sources. */ @Override protected void initPropertySources { ConfigurableEnvironment env = getEnvironment; if (env instanceof ConfigurableWebEnvironment) { ((ConfigurableWebEnvironment) env).initPropertySources(this.servletContext, null); } }

createEmbeddedServletContainer就結束了,內嵌容器的啟動過程至此結束。

==========================================================

咱最近用的github:https://github.com/saaavsaaa

同類文章
Next Article
喜欢就按个赞吧!!!
点击关闭提示