之前在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 = new connector(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<string, object> 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<servletcontextinitializer> mergedinitializers = new arraylist<servletcontextinitializer>(); 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<service, connector[]> 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<class<?>> 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所有的線程都是守護線程,所以創建一個非守護線程(例:thread[container-0,5,main])來避免服務到這就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(); }
這個await每10秒檢查一次是否關閉了:
try { awaitthread = thread.currentthread(); while(!stopawait) { try { thread.sleep( 10000 ); } catch( interruptedexception ex ) { // continue and check the flag } } } finally { awaitthread = null; } return;
回到embeddedwebapplicationcontext,initpropertysources方法,用初始化好的servletcontext完善環境變量:
/** * {@inheritdoc} * <p>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
原文鏈接:http://www.cnblogs.com/saaav/p/6323350.html