天天看點

深入Jetty源碼之Servlet架構及實作(ServletContext)

servlet是server applet的縮寫,即在伺服器端運作的小程式,而servlet架構則是對http伺服器(servlet container)和使用者小程式中間層的标準化和抽象。這一層抽象隔離了http伺服器的實作細節,而servlet規範定義了各個類的行為,進而保證了這些“伺服器端運作的小程式”對伺服器實作的無關性(即提升了其可移植性)。

在servlet規範有以下幾個核心類(接口):

servletcontext:定義了一些可以和servlet container互動的方法。

registration:實作filter和servlet的動态注冊。

servletrequest(httpservletrequest):對http請求消息的封裝。

servletresponse(httpservletresponse):對http響應消息的封裝。

requestdispatcher:将目前請求分發給另一個url,甚至servletcontext以實作進一步的處理。

servlet(httpservlet):所有“伺服器小程式”要實作了接口,這些“伺服器小程式”重寫doget、dopost、doput、dohead、dodelete、dooption、dotrace等方法(httpservlet)以實作響應請求的相關邏輯。

filter(filterchain):在進入servlet前以及出servlet以後添加一些使用者自定義的邏輯,以實作一些橫切面相關的功能,如使用者驗證、日志列印等功能。

asynccontext:實作異步請求處理。

context在這裡是指一個web application的上下文(web application是一個server子url下的servlet和資源的集合),即它包含了這個web application級别的資訊,如目前web application對應的根路徑、使用的servlet版本、使用的classloader等,在一個jvm中的一個web application隻能有一個context(一個jvm可以包含多個web application,它們包含不同的根路徑,即不同的context路徑,context路徑可以是空("/")即這個jvm隻能包含一個web application)。servletcontext則是對這個context的抽象,它還定義了一些和servlet container互動的方法,如擷取檔案的mine type、dispatch請求到另一個url或context、将日志寫入檔案、根據提供的路徑擷取resource執行個體、向這個servletcontext注冊并擷取servlet或filter、向這個servletcontext注冊并擷取attribute或初始參數、向這個servletcontext注冊或擷取相關listener等。對distributed的web application來說,每個jvm下的web application都有一個獨立的servletcontext,因而servletcontext不可以作為全局資訊存儲的地方,因而它并沒有分布式資訊同步的功能,即它隻是本地的servletcontext。在servlet中,使用servletconfig執行個體可以擷取servletcontext執行個體。

類圖如下:

深入Jetty源碼之Servlet架構及實作(ServletContext)

servletcontext的接口定義如下:

public interface servletcontext {

    // servlet container為目前web application設定臨時目錄,并将該臨時目錄的值存儲到目前servletcontext的屬性中使用的屬性名。

    // jetty使用webinfconfiguration(在preconfig ure()方法中)來設定該值,設定temp目錄的規則:

    // 1. 如果存在web-inf/work目錄,則temp目錄的值為:web-inf/work/jetty_<host>_<port>__<resourcebase>_<context>_<virtualhost+base36_hashcode_of_whole_string>

    // 2. 如果"javax.servlet.context.tempdir"已經在外部被設定,并且該目錄存在且可寫,則temp目錄直接設定為該目錄執行個體。

    // 3. 如果系統變量中"jetty.home"目錄下存在"work"目錄且可寫,則設定temp目錄的值為:${jetty.home}/work/jetty_<host>_<port>__<resourcebase>_<context>_<virtualhost+base36_hashcode_of_whole_string>

    // 4. 如果存在"org.eclipse.jetty.webapp.basetempdir"的屬性,且該目錄存在并可寫,設定temp目錄為:${org.eclipse.jetty.webapp.basetempdir}/jetty_<host>_<port>__<resourcebase>_<context>_<virtualhost+base36_hashcode_of_whole_string>

    // 5. 如果以上條件都不成立,則設定temp目錄為:${java.io.tmpdir}/jetty_<host>_<port>__<resourcebase>_<context>_<virtualhost+base36_hashcode_of_whole_string>,且删除已存在臨時目錄。

    // 注:對temp目錄的父目錄不是work,會注冊在jvm退出時删除該temp目錄,并在temp目錄下建立.active目錄。

    public static final string tempdir = "javax.servlet.context.tempdir";

    // servlet 3.0中新引入的特性,即可以在web-inf/lib下的jar包中定義/meta-inf/web-fragment配置響應的servlet等。

    // 如果在web.xml檔案中定義了absolute-ordering,或者在jar包中存在/meta-inf/web-fragment.xml檔案,且定義了ordering,

    // 則該屬性的值即為根據規範中定義的規則計算出來的讀取jar包中web-fragment.xml檔案的jar包名順序,它時一個list<string>類型,包含jar包名字。

    // 在jetty中使用ordering來表示這種順序,它有兩個實作類:absoluteordering和relativeordering用來分别表示在web.xml和web-fragment.xml中定義的absolute-ordering和ordering定義,

    // 并且将最終的解析結果彙總到metadata類中,并根據規範中定義的規則以及metadata-complete的定義來計算實際的解析順序,

    // 而對這兩種配置檔案的解析由webdescriptor和fragmentdescriptor來實作,它們都包含了metadata-complete解析,而真正的解析入口在webxmlconfiguration和fragmentconfiguration中。

    // 該規範的定義參考:https://blogs.oracle.com/swchan/entry/servlet_3_0_web_fragment

    public static final string ordered_libs = "javax.servlet.context.orderedlibs";

    // 傳回目前web application的context path,即目前web application的根路徑,servlet container根據該路徑以及一個request url的比對情況來選擇一個request應該交給那個web application處理該請求。

    // context path以"/"開始,但是不能以"/"結尾,對預設的根context,它傳回"",而不是"/"。該值在配置jetty的contexthandler中設定。

    // 有些servlet container支援多個context path指向同一個context,此時可以使用httpservletrequest中的getcontextpath()來擷取該request實際對應的context path,此時這兩個context path的值可能會不同,但是servletcontext中傳回的context path值是主要的值。另外jetty也不支援這種特性。

    public string getcontextpath();

    // 通過給定一個context path以在目前servlet container中找到其對應的servletcontext執行個體。

    // 可以通過該方法擷取servlet container中定義的另一個web application的servletcontext執行個體,并獲得其requestdispatcher,并将目前請求dispatch到那個web application中做進一步的處理。這裡的uripath必須以"/"開頭,且其路徑相對于目前server的根路徑。出于安全考慮,該方法可能會傳回null。

    // 在jetty的實作中,這裡uripath可以是一個具體的路徑,并且支援查找最準确的比對。如:對uripath為/foo/goo/abc.html,在server中由以下context path定義:"/", "/foo", "/foo/goo",則最終查找到的servletcontext為"/foo/goo"作為context path對應的servletcontext執行個體。

    public servletcontext getcontext(string uripath);

    //傳回servlet規範的主版本,如3

    public int getmajorversion();

    // 傳回servlet規範的次版本,如0

    public int getminorversion();

    // 傳回目前web application基于的servlet規範的主版本,如在web.xml檔案中定義的version(<web-app version="..." ...>...</web-app>,即jetty中的實作)

    public int geteffectivemajorversion();

    // 傳回目前web application基于的servlet規範的次版本,如在web.xml檔案中定義的version(<web-app version="..." ...>...</web-app>,即jetty中的實作)

    public int geteffectiveminorversion();

    // 傳回給定file的mime type,傳回null如果無法計算出其mime type。這個映射關系由servlet container定義或在web.xml檔案中定義(mime-mapping, extension, mine-type)。

    // 常見的mime type有:text/html, image/gif等。jetty使用mimetypes類來封裝所有和mime type相關的操作,mimetypes類中定義了所有預設支援的mime type以及編碼類型,

    // 并且預設從org/eclipse/jetty/http/mime.properties檔案中加載預設的映射,如css=text/css, doc=application/msword等,使用addmimemapping()方法向該類注冊web.xml中定義的檔案名擴充名到mime type的映射。

    // 而從org/eclipse/jetty/http/encoding.properties檔案中加載mime type的預設編碼類型,如text/xml=utf-8等。

    // 在使用檔案名查找mime type時,根據檔案名的擴充名查找已注冊或預設注冊的mime type。使用者自定義的映射優先。使用者定義的mime type映射支援extension為"*",表示任意擴充名。

    public string getmimetype(string file);

    // 對于給定目錄,傳回該目錄下所有的資源以及目錄。path必須以"/"開頭,如果它不是指向一個目錄,則傳回空的set。所有傳回的路徑都相對目前web application的根目錄,

    // 或對于/web-inf/lib中jar包中的/meta-inf/resources/目錄,如一個web application包含以下資源:/catalog/offers/music.html, /web-inf/web.xml, 

    // 以及/web-inf/lib/catalog.jar!/meta-inf/resources/catalog/moreoffers/books.html,則getresourcepaths("/catalog")傳回{"/catalog/offers/", /catalog/moreoffers/"}

    // jetty的實作中,在metainfconfiguration中,它會掃描web-inf/lib目錄下所有的jar包,如果發現在某個jar包中存在meta-inf/resources/目錄,

    // 就會将該目錄資源作為baseresource在webinfconfiguration中注冊到contexthandler(webappcontext)中。進而實作jar包中的meta-inf/resources/目錄作為根目錄的查找。  

    public set<string> getresourcepaths(string path);

    // 傳回給定path的url,path必須以"/"開頭,它相對目前web application的根目錄或相對/web-inf/lib中jar包中的/meta-inf/resources/目錄。

    // 其中查找順序前者優先于後者,但是在/web-inf/lib/目錄下的jar包的查找順序未定義。該方法不同于class.getresource()在于它不使用classloader,如果沒有找到給定資源,傳回null。

    // 在webappcontext實作中,它還支援alias查找,并且如果其extractwar的變量為false,給定的資源在war包中,則該url傳回war包中的url。    

    public url getresource(string path) throws malformedurlexception;

    // 參考getresource(path)的查找實作,如果其傳回的url為null,則該方法傳回null,否則傳回url對應的inputstream。

    public inputstream getresourceasstream(string path);

    // 建立一個requestdispatcher用于将一個request、response分發到path對應的url中,這裡path必須以"/"開頭,且它相對于目前context path。如果無法建立requestdispatcher,傳回null。

    // path可以包含query資料用于傳遞參數:uriincontext?param1=abc&param2=123....該方法可以和getcontext(uripath)一起使用,以将目前請求分發到另一個web application中。

    // 該方法的另一種用法是先有一個servlet或filter處理基本的邏輯,然後使用這個requestdispatcher将目前請求forward到另一個url中或include一個jsp檔案生成響應頁面,如果在處理過程中出錯,則将其目前請求分發到錯誤處理的流程中。

    // requestdispatcher支援兩種類型的分發:forward和include,唯一的差別是include隻可以改變response的内容,不可以改變其header資訊,forward則沒有這種限制。

    public requestdispatcher getrequestdispatcher(string path);

    // 建立一個requestdispatcher用于将一個request、response分發到name對應的servlet(jsp)中。如果沒能找到響應的servlet,傳回null。

    public requestdispatcher getnameddispatcher(string name);    

    // 将msg列印到servlet對應的log檔案中,在jetty中,使用info級别列印,logger名稱為web.xml定義的display-name,或者context path。

    // jetty預設使用slf4j作為日志列印架構,可以使用"org.eclipse.jetty.util.log.class"系統屬性改變其日志列印架構。

    public void log(string msg);   

    // 列印message和throwable到servlet對應的log檔案中,在jetty中使用warn級别列印該資訊。

    public void log(string message, throwable throwable);

    // 傳回給定path在目前機器上作業系統對應的位置。對/meta-inf/resources下的資源,除非他們已經解壓到本地目錄,否則對那些資源該方法傳回null。

    // 在jetty實作中,使用getresource()方法相同的實作擷取resource執行個體,如果其getfile()傳回不為null,則傳回該file的canonical路徑。

    public string getrealpath(string path);

    // 傳回servlet container的名稱和版本号,其格式為:<servername>/<versionnumber>,如:jetty/8.1.9.v20130131。

    public string getserverinfo();

    // servletcontext級别的初始參數操作,可以在web.xml中使用context-param定義,也可以手動設定。在get中如果找不到對應的項,傳回null,在set時,如果已存在name,則傳回false,并且不更新相應的值。

    public string getinitparameter(string name);

    public enumeration<string> getinitparameternames();

    public boolean setinitparameter(string name, string value);

    // servletcontext級别的屬性操作,其中屬性名遵循包命名規則。在set中,如果object為null表示移除該屬性,如果name以存在,則會替換原有的值,如果注冊了servletcontextattributelistener,則會出發相應的attributeremoved、attributereplaced、attributeadded事件。在remove中,如果name存在且被移除了,則會觸發attributeremoved事件。

    // 在jetty中使用contexthandler中的context内部類實作servletcontext,在contexthandler中定義了兩個相關字段:_attributes以及_contextattributes,其中_attributes表示在jetty内部通過contexthandler設定的屬性,而_contextattributes表示使用者設定的屬性,但是在擷取屬性值時,兩個字段的屬性都會考慮進去,在移除屬性時,如果是移除_attributes字段中的值,則不會觸發attributeremoved事件。

    public object getattribute(string name);

    public enumeration<string> getattributenames();

    public void setattribute(string name, object object);

    public void removeattribute(string name);

    // 傳回目前web application在web.xml中的display-name定義的servletcontext名字,在jetty實作中,如果該值為null,則傳回context path。

    public string getservletcontextname();

    // 該部分具體的使用可以參考:http://www.blogjava.net/yongboy/archive/2010/12/30/346209.html 

    // 動态的向servletcontext中注冊servlet,注冊的servlet還可以通過傳回的servletregistration繼續配置,如addmapping、setinitparameter等。

    // 在使用classname執行個體話servlet時,使用目前servletcontext相關聯的classloader。在建立servlet執行個體時,會根據該類中定義的以下annotation做相應的配置:

    // javax.servlet.annotation.servletsecurity、javax.servlet.annotation.multipartconfig、javax.annotation.security.runas、javax.annotation.security.declareroles

    public servletregistration.dynamic addservlet(string servletname, string classname);

    public servletregistration.dynamic addservlet(string servletname, servlet servlet);

    public servletregistration.dynamic addservlet(string servletname, class <? extends servlet> servletclass);

    // 建立給定servlet類的servlet執行個體,并且會根據該類中定義的以下annotation做相應的配置:

    // 在建立servlet執行個體後,一般還要調用addservlet()方法将其注冊到servletcontext中。

    public <t extends servlet> t createservlet(class<t> clazz) throws servletexception;

    // 根據servletname擷取servletregistration執行個體

    public servletregistration getservletregistration(string servletname);

    // 擷取所有在servletcontext中注冊的servletname到servletregistration映射的map。所有動态注冊和使用配置注冊的映射。

    public map<string, ? extends servletregistration> getservletregistrations();

    // 動态的向servletcontext中注冊filter,注冊的filter可以通過傳回的filterregistration進一步配置,如addmappingforurlpatterns、setinitparameter等

    public filterregistration.dynamic addfilter(string filtername, string classname);

    public filterregistration.dynamic addfilter(string filtername, filter filter);

    public filterregistration.dynamic addfilter(string filtername, class <? extends filter> filterclass);

    // 建立給定filter類執行個體的filter執行個體,一般都會後繼調用addfilter将該執行個體注冊到servletcontext中。

    public <t extends filter> t createfilter(class<t> clazz) throws servletexception;

    // 根據filtername擷取filterregistration執行個體。

    public filterregistration getfilterregistration(string filtername);

    // 傳回所有filtername到filterregistration映射的map,包括所有動态注冊和使用配置注冊的映射。

    public map<string, ? extends filterregistration> getfilterregistrations();

    // 傳回sessioncookieconfig執行個體,用于session tracking的cookie屬性,多次調用該方法傳回相同的執行個體。

    public sessioncookieconfig getsessioncookieconfig();

    // 設定session tracking模式,可以是url、cookie、ssl。jetty8隻支援url和cookie。

    public void setsessiontrackingmodes(set<sessiontrackingmode> sessiontrackingmodes);

    // 傳回目前servletcontext預設支援的session tracking模式。

    public set<sessiontrackingmode> getdefaultsessiontrackingmodes();

    // 傳回目前servletcontext目前使用的session tracking模式。

    public set<sessiontrackingmode> geteffectivesessiontrackingmodes();

    // 向servletcontext動态的注冊listener,該listener類或執行個體必須實作以下的一個或多個接口:

    // servletcontextattributelistener、servletrequestlistener、servletrequestattributelistener、httpsessionlistener、httpsessionattributelistener}</tt>

    // 如果這個servletcontext傳入servletcontainerinitializer的onstartup方法,那麼這個listener類或執行個體也可以實作servletcontextlistener接口。

    // 注:動态注冊的servletcontextlistener中的contextinitialized方法中不可以調用servlet 3.0中定義的這些動态注冊servlet、filter、listener等方法,不然會抛出unsupportedoperationexception,看起來像是出于一緻性、安全性或是相容性的考慮,但是具體是什麼原因一直想不出來。而且在jetty實作中,它在注冊eventlistener執行個體是确取消了這種限制,而對注冊eventlistener類執行個體和類名确有這種限制,不知道這是jetty的bug還是其他什麼原因。。。。。

    // 對于調用順序按定義順序來的eventlistener(如servletrequestlistener、servletcontextlistener、httpsessionlistener),那這個新的eventlistener會添加到相應清單末尾。

    public void addlistener(string classname);

    public <t extends eventlistener> void addlistener(t t);

    public void addlistener(class <? extends eventlistener> listenerclass);

    // 建立clazz對應的eventlistener執行個體,一般這個新建立的eventlistener會之後注冊到servletcontext中。

    public <t extends eventlistener> t createlistener(class<t> clazz) throws servletexception; 

    // 傳回web.xml和web-fragment.xml配置檔案中定義<jsp-config>的彙總,或傳回null如果沒有相關配置。看起來像jetty并沒有實作該方法。

    public jspconfigdescriptor getjspconfigdescriptor();

    // 傳回目前web application對應的classloader執行個體。

    public classloader getclassloader();

    // 定義角色名。角色名在servletregistration.dynamic.setservletsecurity()和servletregistration.dynamic.setrunasrole()中預設定義,因而不需要重新使用這個方法定義。

    public void declareroles(string

深入Jetty源碼之Servlet架構及實作(ServletContext)

 rolenames);

}

servletcontext的初始化從contexthandler的dostart()方法開始,在其startcontext()方法快結束時,會調用注冊的servletcontextlistener中的contextinitialized()方法,因而這裡是使用者對servletcontext初始化時做一些自定義邏輯的擴充點。

在servlet 3.0中還引入了servletcontainerinitializer接口,它定義了onstartup()方法,該方法會在webappcontext中的startcontext方法中的configure方法中通過containerinitializerconfiguration.configure()中被調用,該方法的調用要早于所有servletcontextlistener .contextinitialized()事件的觸發。

public interface servletcontainerinitializer {

    // 當servletcontext對應的web application初始化時,該方法會被調用。其中c參數是所有繼承、實作在servletcontainerinitializer實作類定義的handlestypes注解中定義的類,

    // 如果該注解定義的類數組中有注解,那麼c參數還包含所有存在這個注解的類。如果實作servletcontainerinitializer接口的類在web-inf/lib的jar包中綁定,

    // 那麼該方法隻會在對應的web application初始化被調用一次,如果實作servletcontainerinitializer接口的類在web-inf/lib以外定義,但是還可以被servlet container找到,

    // 那麼意味這這個jar包是多個web application共享的,因而該方法會在每個web application初始化時被調用。如果servletcontainerinitializer實作類沒有定義handlestypes注解,

    // 那麼c參數為null。servletcontainerinitializer實作類的查找使用運作時service provider機制,然而對于定義在fragment jar包中的servletcontainerinitializer實作類,

    // 但是該jar在absolute ordering中被exclude了,那麼該jar包會被忽略,即在web.xml中的absolute-ordering中沒有包含相應的fragment名。

    // 在查找滿足handlestypes注解中定義的類數組的類執行個體集合時,由于有些jar包是可選的,因而在加載class時有時會遇到問題,此時servlet container可以選擇忽略該錯誤,

    // 但是需要提供配置已讓servlet container決定是否要将這些錯誤列印到日志檔案中。 

    public void onstartup(set<class<?>> c, servletcontext ctx) throws servletexception; 

要自定義servletcontainerinitializer邏輯,首先需要meta-inf/services/目錄下建立一個javax.servlet.servletcontainerinitializer檔案,在該檔案内部寫入使用者在定義的servletcontainerinitializer類,如:x.y.z.myservletcontainerinitializer,該myservletcontainerinitializer類必須實作servletcontainerinitializer接口,如果它有一些特定感興趣的類,可以向其定義handlestypes注解,該注解的值可以是類執行個體、接口執行個體、注解執行個體,它表示所有繼承自注解中定義的類、實作注解中定義的接口、有注解中定義的注解注解的類(注解在類、字段、方法中)都會收內建一個set<class<?>>類型,并傳入onstartup()方法中,如果沒有handlestypes注解,其onstartup()中set<class<?>>參數為null。

在jetty實作中,annotationconfiguration會查找到所有jar包中在meta-inf/services/javax.servlet.servletcontainerinitializer中定義的servletcontainerinitializer(排除那些被absolute-ordering排除在外的fragment jar包中的定義),使用這些查找到的servletcontainerinitializer建立containerinitializer執行個體,使用handlestypes注解中定義的class數組初始化interestedtypes字段,如果這個class數組中有注解類型,則将所有存在這個注解類型的類(這個注解可以注解在該類的類、字段、方法中)添加到containerinitializer執行個體中的_annotatedtypenames集合中(使用containerinitializerannotationhandler實作);最後将他們注冊到context的一個屬性中。同時在annotationconfiguration中還會向context中注冊一個屬性,其值是map,它包含了所有類對應的其子類或實作類。然後在containerinitializerconfiguration中,對每個在context中注冊的containerinitializer執行個體,對所有注冊的_annotatedtypenames,将該類以及該類的子類、實作類注冊到_applicabletypenames集合中;對所有注冊的非注解類型的_interestedtypes,将其所有的子類、實作類注冊到_applicabletypenames集合中(在jetty目前版本的實作中沒有包含_interestedtypes中的類執行個體本身,在servletcontainerinitializer的注釋中确實也沒有說明要包含這些類本身,感覺這個有點不合理。。。);最後調用containerinitializer中的callstartup()方法,它加載_applicabletypenames集合中的所有類,并将其傳入servletcontainerinitializer的onstartup()方法中(這裡沒有根據規範忽略不能加載成功的類執行個體)。