什么是Servlet
Servlet的作用是為Java程序提供一個統(tǒng)一的web應用的規(guī)范他匪,方便程序員統(tǒng)一的使用這種規(guī)范來編寫程序岖圈,應用容器可以使用提供的規(guī)范來實現(xiàn)自己的特性。比如tomcat的代碼和jetty的代碼就不一樣,但作為程序員你只需要了解servlet規(guī)范就可以從request中取值,你可以操作session等等癌压。不用在意應用服務器底層的實現(xiàn)的差別而影響你的開發(fā)。
HTTP 協(xié)議只是一個規(guī)范荆陆,定義服務請求和響應的大致式樣滩届。Java servlet 類將HTTP中那些低層的結構包裝在 Java 類中,這些類所包含的便利方法使其在 Java 語言環(huán)境中更易于處理被啼。
正如您正使用的特定 servlet 容器的配置文件中所定義的帜消,當用戶通過 URL 發(fā)出一個請求時,這些 Java servlet 類就將之轉(zhuǎn)換成一個 HttpServletRequest浓体,并發(fā)送給 URL 所指向的目標泡挺。當服務器端完成其工作時,Java 運行時環(huán)境(Java Runtime Environment)就將結果包裝在一個 HttpServletResponse 中命浴,然后將原 HTTP 響應送回給發(fā)出該請求的客戶機娄猫。在與 Web 應用程序進行交互時,通常會發(fā)出多個請求并獲得多個響應生闲。所有這些都是在一個會話語境中媳溺,Java 語言將之包裝在一個 HttpSession 對象中。在處理響應時碍讯,您可以訪問該對象悬蔽,并在創(chuàng)建響應時向其添加事件。它提供了一些跨請求的語境捉兴。
容器(如 Tomcat)將為 servlet 管理運行時環(huán)境蝎困。您可以配置該容器,定制 J2EE 服務器的工作方式倍啥,以便將 servlet 暴露給外部世界禾乘。正如我們將看到的,通過該容器中的各種配置文件虽缕,您在 URL(由用戶在瀏覽器中輸入)與服務器端組件之間搭建了一座橋梁始藕,這些組件將處理您需要該 URL 轉(zhuǎn)換的請求。在運行應用程序時彼宠,該容器將加載并初始化 servlet鳄虱,管理其生命周期弟塞。
Servlet體系結構
Servlet
Servlet的框架是由兩個Java包組成的:javax.servlet與javax.servlet.http凭峡。在javax.servlet包中定義了所有的Servlet類都必須實現(xiàn)或者擴展的通用接口和類。在javax.servlet.http包中定義了采用Http協(xié)議通信的HttpServlet類决记。Servlet的框架的核心是javax.servlet.Servlet接口摧冀,所有的Servlet都必須實現(xiàn)這個接口。
在Servlet接口中定義了5個方法:
1. init(ServletConfig)方法:負責初始化Servlet對象,在Servlet的生命周期中索昂,該方法執(zhí)行一次建车;該方法執(zhí)行在單線程的環(huán)境下,因此開發(fā)者不用考慮線程安全的問題椒惨;
2. service(ServletRequest req,ServletResponse res)方法:負責響應客戶的請求缤至;為了提高效率,Servlet規(guī)范要求一個Servlet實例必須能夠同時服務于多個客戶端請求康谆,即service()方法運行在多線程的環(huán)境下领斥,Servlet開發(fā)者必須保證該方法的線程安全性;
3. destroy()方法:當Servlet對象退出生命周期時沃暗,負責釋放占用的資源月洛;
4. getServletInfo:就是字面意思,返回Servlet的描述孽锥;
5. getServletConfig:這個方法返回由Servlet容器傳給init方法的ServletConfig嚼黔。
ServletRequest & ServletResponse
對于每一個HTTP請求,servlet容器會創(chuàng)建一個封裝了HTTP請求的ServletRequest實例傳遞給servlet的service方法惜辑,ServletResponse則表示一個Servlet響應唬涧,其隱藏了將響應發(fā)給瀏覽器的復雜性。通過ServletRequest的方法你可以獲取一些請求相關的參數(shù)盛撑,而ServletResponse則可以將設置一些返回參數(shù)信息爵卒,并且設置返回內(nèi)容。
ServletConfig
ServletConfig封裝可以通過@WebServlet或者web.xml傳給一個Servlet的配置信息撵彻,以這種方式傳遞的每一條信息都稱做初始化信息钓株,初始化信息就是一個個K-V鍵值對。為了從一個Servlet內(nèi)部獲取某個初始參數(shù)的值陌僵,init方法中調(diào)用ServletConfig的getinitParameter方法或getinitParameterNames方法獲取轴合,除此之外,還可以通過getServletContext獲取ServletContext對象碗短。
ServletContext
ServletContext是代表了Servlet應用程序受葛。每個Web應用程序只有一個context。在分布式環(huán)境中偎谁,一個應用程序同時部署到多個容器中总滩,并且每臺Java虛擬機都有一個ServletContext對象。有了ServletContext對象后巡雨,就可以共享能通過應用程序的所有資源訪問的信息闰渔,促進Web對象的動態(tài)注冊,共享的信息通過一個內(nèi)部Map中的對象保存在ServiceContext中來實現(xiàn)铐望。保存在ServletContext中的對象稱作屬性冈涧。操作屬性的方法:
GenericServlet
前面編寫的Servlet應用中通過實現(xiàn)Servlet接口來編寫Servlet茂附,但是我們每次都必須為Servlet中的所有方法都提供實現(xiàn),還需要將ServletConfig對象保存到一個類級別的變量中督弓,GenericServlet抽象類就是為了為我們省略一些模板代碼营曼,實現(xiàn)了Servlet和ServletConfig,完成了一下幾個工作:
將init方法中的ServletConfig賦給一個類級變量愚隧,使的可以通過getServletConfig來獲取蒂阱。
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
同時為避免覆蓋init方法后在子類中必須調(diào)用super.init(servletConfig),GenericServlet還提供了一個不帶參數(shù)的init方法狂塘,當ServletConfig賦值完成就會被第帶參數(shù)的init方法調(diào)用蒜危。這樣就可以通過覆蓋不帶參數(shù)的init方法編寫初始化代碼,而ServletConfig實例依然得以保存
為Servlet接口中的所有方法提供默認實現(xiàn)睹耐。
提供方法來包裝ServletConfig中的方法辐赞。
HTTPServlet
在編寫Servlet應用程序時,大多數(shù)都要用到HTTP硝训,也就是說可以利用HTTP提供的特性响委,javax.servlet.http包含了編寫Servlet應用程序的類和接口,其中很多覆蓋了javax.servlet中的類型窖梁,我們自己在編寫應用時大多時候也是繼承的HttpServlet赘风。
Servlet工作原理
當Web服務器接收到一個HTTP請求時,它會先判斷請求內(nèi)容——如果是靜態(tài)網(wǎng)頁數(shù)據(jù)纵刘,Web服務器將會自行處理邀窃,然后產(chǎn)生響應信息;如果牽涉到動態(tài)數(shù)據(jù)假哎,Web服務器會將請求轉(zhuǎn)交給Servlet容器瞬捕。此時Servlet容器會找到對應的處理該請求的Servlet實例來處理,結果會送回Web服務器舵抹,再由Web服務器傳回用戶端肪虎。
針對同一個Servlet,Servlet容器會在第一次收到http請求時建立一個Servlet實例惧蛹,然后啟動一個線程扇救。第二次收到http請求時,Servlet容器無須建立相同的Servlet實例香嗓,而是啟動第二個線程來服務客戶端請求迅腔。所以多線程方式不但可以提高Web應用程序的執(zhí)行效率,也可以降低Web服務器的系統(tǒng)負擔靠娱。
接著我們描述一下Tomcat與Servlet是如何工作的沧烈,首先看下面的時序圖:
Web Client 向Servlet容器(Tomcat)發(fā)出Http請求;
Servlet容器接收Web Client的請求饱岸;
Servlet容器創(chuàng)建一個HttpRequest對象掺出,將Web Client請求的信息封裝到這個對象中;
Servlet容器創(chuàng)建一個HttpResponse對象苫费;
Servlet容器調(diào)用HttpServlet對象的service方法汤锨,把HttpRequest對象與HttpResponse對象作為參數(shù)傳給 HttpServlet對象;
HttpServlet調(diào)用HttpRequest對象的有關方法百框,獲取Http請求信息闲礼;
HttpServlet調(diào)用HttpResponse對象的有關方法,生成響應數(shù)據(jù)铐维;
Servlet容器把HttpServlet的響應結果傳給Web Client柬泽;
Servlet生命周期
在Servlet接口中定義了5個方法,其中3個方法代表了Servlet的生命周期:
1. init(ServletConfig)方法:負責初始化Servlet對象嫁蛇,在Servlet的生命周期中锨并,該方法執(zhí)行一次;該方法執(zhí)行在單線程的環(huán)境下睬棚,因此開發(fā)者不用考慮線程安全的問題第煮;
2. service(ServletRequest req,ServletResponse res)方法:負責響應客戶的請求;為了提高效率抑党,Servlet規(guī)范要求一個Servlet實例必須能夠同時服務于多個客戶端請求包警,即service()方法運行在多線程的環(huán)境下,Servlet開發(fā)者必須保證該方法的線程安全性底靠;
3. destroy()方法:當Servlet對象退出生命周期時害晦,負責釋放占用的資源;
編程注意事項說明:
- 當Server Thread線程執(zhí)行Servlet實例的init()方法時暑中,所有的Client Service Thread線程都不能執(zhí)行該實例的service()方法壹瘟,更沒有線程能夠執(zhí)行該實例的destroy()方法,因此Servlet的init()方法是工作在單線程的環(huán)境下鳄逾,開發(fā)者不必考慮任何線程安全的問題俐筋。
- 當服務器接收到來自客戶端的多個請求時,服務器會在單獨的Client Service Thread線程中執(zhí)行Servlet實例的service()方法服務于每個客戶端严衬。此時會有多個線程同時執(zhí)行同一個Servlet實例的service()方法澄者,因此必須考慮線程安全的問題。
- 雖然service()方法運行在多線程的環(huán)境下请琳,并不一定要同步該方法粱挡。而是要看這個方法在執(zhí)行過程中訪問的資源類型及對資源的訪問方式。分析如下:
1. 如果service()方法沒有訪問Servlet的成員變量也沒有訪問全局的資源比如靜態(tài)變量俄精、文件询筏、數(shù)據(jù)庫連接等,而是只使用了當前線程自己的資源竖慧,比如非指向全局資源的臨時變量嫌套、request和response對象等逆屡。該方法本身就是線程安全的,不必進行任何的同步控制踱讨。
2. 如果service()方法訪問了Servlet的成員變量魏蔗,但是對該變量的操作是只讀操作,該方法本身就是線程安全的痹筛,不必進行任何的同步控制莺治。
3. 如果service()方法訪問了Servlet的成員變量,并且對該變量的操作既有讀又有寫帚稠,通常需要加上同步控制語句谣旁。
4. 如果service()方法訪問了全局的靜態(tài)變量,如果同一時刻系統(tǒng)中也可能有其它線程訪問該靜態(tài)變量滋早,如果既有讀也有寫的操作榄审,通常需要加上同步控制語句。
5. 如果service()方法訪問了全局的資源杆麸,比如文件瘟判、數(shù)據(jù)庫連接等,通常需要加上同步控制語句角溃。
在創(chuàng)建一個 Java servlet 時拷获,一般需要子類 HttpServlet。該類中的方法允許您訪問請求和響應包裝器(wrapper)减细,您可以用這個包裝器來處理請求和創(chuàng)建響應匆瓜。Servlet的生命周期,簡單的概括這就分為四步:
Servlet類加載--->實例化--->服務--->銷毀未蝌;
創(chuàng)建Servlet對象的時機:
- 默認情況下驮吱,在Servlet容器啟動后:客戶首次向Servlet發(fā)出請求,Servlet容器會判斷內(nèi)存中是否存在指定的Servlet對象萧吠,如果沒有則創(chuàng)建它左冬,然后根據(jù)客戶的請求創(chuàng)建HttpRequest、HttpResponse對象纸型,從而調(diào)用Servlet對象的service方法拇砰;
- Servlet容器啟動時:當web.xml文件中如果<servlet>元素中指定了<load-on-startup>子元素時,Servlet容器在啟動web服務器時狰腌,將按照順序創(chuàng)建并初始化Servlet對象除破;
- Servlet的類文件被更新后,重新創(chuàng)建Servlet琼腔。Servlet容器在啟動時自動創(chuàng)建Servlet瑰枫,這是由在web.xml文件中為Servlet設置的<load-on-startup>屬性決定的。從中我們也能看到同一個類型的Servlet對象在Servlet容器中以單例的形式存在丹莲;
注意:在web.xml文件中光坝,某些Servlet只有<serlvet>元素尸诽,沒有<servlet-mapping>元素,這樣我們無法通過url的方式訪問這些Servlet盯另,這種Servlet通常會在<servlet>元素中配置一個<load-on-startup>子元素性含,讓容器在啟動的時候自動加載這些Servlet并調(diào)用init(ServletConfig config)方法來初始化該Servlet。其中方法參數(shù)config中包含了Servlet的配置信息土铺,比如初始化參數(shù)胶滋,該對象由服務器創(chuàng)建板鬓。
銷毀Servlet對象的時機:
Servlet容器停止或者重新啟動:Servlet容器調(diào)用Servlet對象的destroy方法來釋放資源悲敷。以上所講的就是Servlet對象的生命周期。那么Servlet容器如何知道創(chuàng)建哪一個Servlet對象俭令?Servlet對象如何配置后德?實際上這些信息是通過讀取web.xml配置文件來實現(xiàn)的。
<servlet>
<!-- Servlet對象的名稱 -->
<servlet-name>action<servlet-name>
<!-- 創(chuàng)建Servlet對象所要調(diào)用的類 -->
<servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
<init-param>
<!-- 參數(shù)名稱 -->
<param-name>config</param-name>
<!-- 參數(shù)值 -->
<param-value>/WEB-INF/struts-config.xml</param-value>
</init-param>
<init-param>
<param-name>detail</param-name>
<param-value>2</param-value>
</init-param>
<init-param>
<param-name>debug</param-name>
<param-value>2</param-value>
</init-param>
<!-- Servlet容器啟動時加載Servlet對象的順序 -->
<load-on-startup>2</load-on-startup>
</servlet>
<!-- 要與servlet中的servlet-name配置節(jié)內(nèi)容對應 -->
<servlet-mapping>
<servlet-name>action</servlet-name>
<!-- 客戶訪問的Servlet的相對URL路徑 -->
<url-pattern>*.do</url-pattern>
</servlet-mapping>
當Servlet容器啟動的時候讀取<servlet>配置節(jié)信息抄腔,根據(jù)<servlet-class>配置節(jié)信息創(chuàng)建Servlet對象瓢湃,同時根據(jù)<init-param>配置節(jié)信息創(chuàng)建HttpServletConfig對象,然后執(zhí)行Servlet對象的init方法赫蛇,并且根據(jù)<load-on-startup>配置節(jié)信息來決定創(chuàng)建Servlet對象的順序绵患,如果此配置節(jié)信息為負數(shù)或者沒有配置,那么在Servlet容器啟動時悟耘,將不加載此Servlet對象落蝙。當客戶訪問Servlet容器時,Servlet容器根據(jù)客戶訪問的URL地址暂幼,通過<servlet-mapping>配置節(jié)中的<url-pattern>配置節(jié)信息找到指定的Servlet對象筏勒,并調(diào)用此Servlet對象的service方法。
在整個Servlet的生命周期過程中旺嬉,創(chuàng)建Servlet實例管行、調(diào)用實例的init()和destroy()方法都只進行一次,當初始化完成后邪媳,Servlet容器會將該實例保存在內(nèi)存中捐顷,通過調(diào)用它的service()方法,為接收到的請求服務雨效。下面給出Servlet整個生命周期過程的UML序列圖套菜,如圖所示:
如果需要讓Servlet容器在啟動時即加載Servlet,可以在web.xml文件中配置<load-on-startup>元素设易。
Servlet中的Listener
Listener 使用的非常廣泛逗柴,它是基于觀察者模式設計的,Listener 的設計對開發(fā) Servlet 應用程序提供了一種快捷的手段顿肺,能夠方便的從另一個縱向維度控制程序和數(shù)據(jù)戏溺。目前 Servlet 中提供了 5 種兩類事件的觀察者接口渣蜗,它們分別是:4 個 EventListeners 類型的,ServletContextAttributeListener旷祸、ServletRequestAttributeListener耕拷、ServletRequestListener、HttpSessionAttributeListener 和 2 個 LifecycleListeners 類型的托享,ServletContextListener骚烧、HttpSessionListener。如下圖所示:
它們基本上涵蓋了整個 Servlet 生命周期中闰围,你感興趣的每種事件赃绊。這些 Listener 的實現(xiàn)類可以配置在 web.xml 中的 <listener> 標簽中。當然也可以在應用程序中動態(tài)添加 Listener羡榴,需要注意的是 ServletContextListener 在容器啟動之后就不能再添加新的碧查,因為它所監(jiān)聽的事件已經(jīng)不會再出現(xiàn)。掌握這些 Listener 的使用校仑,能夠讓我們的程序設計的更加靈活忠售。
Cookie與Session
Servlet 能夠給我們提供兩部分數(shù)據(jù),一個是在 Servlet 初始化時調(diào)用 init 方法時設置的 ServletConfig迄沫,這個類基本上含有了 Servlet 本身和 Servlet 所運行的 Servlet 容器中的基本信息稻扬。還有一部分數(shù)據(jù)是由 ServletRequest 類提供,從提供的方法中發(fā)現(xiàn)主要是描述這次請求的 HTTP 協(xié)議的信息羊瘩。關于這一塊還有一個讓很多人迷惑的 Session 與 Cookie泰佳。
Session 與 Cookie 的作用都是為了保持訪問用戶與后端服務器的交互狀態(tài)。它們有各自的優(yōu)點也有各自的缺陷困后。然而具有諷刺意味的是它們優(yōu)點和它們的使用場景又是矛盾的乐纸,例如使用 Cookie 來傳遞信息時,隨著 Cookie 個數(shù)的增多和訪問量的增加摇予,它占用的網(wǎng)絡帶寬也也會越來越大汽绢。所以大訪問量的時候希望用 Session,但是 Session 的致命弱點是不容易在多臺服務器之間共享侧戴,所以這也限制了 Session 的使用宁昭。
不管 Session 和 Cookie 有什么不足,我們還是要用它們酗宋。下面詳細講一下叁熔,Session 如何基于 Cookie 來工作悼尾。實際上有三種方式能可以讓 Session 正常工作:
- 基于 URL Path Parameter,默認就支持
- 基于 Cookie,如果你沒有修改 Context 容器個 cookies 標識的話耳贬,默認也是支持的
- 基于 SSL,默認不支持,只有 connector.getAttribute("SSLEnabled") 為 TRUE 時才支持
第一種情況下,當瀏覽器不支持 Cookie 功能時漱挚,瀏覽器會將用戶的 SessionCookieName 重寫到用戶請求的 URL 參數(shù)中,它的傳遞格式如:
/path/Servlet?name=value&name2=value2&JSESSIONID=value3
接著 Request 根據(jù)這個 JSESSIONID 參數(shù)拿到 Session ID 并設置到 request.setRequestedSessionId 中渺氧。
請注意如果客戶端也支持 Cookie 的話旨涝,Tomcat 仍然會解析 Cookie 中的 Session ID,并會覆蓋 URL 中的 Session ID侣背。
如果是第三種情況的話將會根據(jù) javax.servlet.request.ssl_session 屬性值設置 Session ID白华。
有了 Session ID 服務器端就可以創(chuàng)建 HttpSession 對象了,第一次觸發(fā)是通過 request. getSession() 方法贩耐,如果當前的 Session ID 還沒有對應的 HttpSession 對象那么就創(chuàng)建一個新的弧腥,并將這個對象加到 org.apache.catalina. Manager 的 sessions 容器中保存,Manager 類將管理所有 Session 的生命周期憔杨,Session 過期將被回收鸟赫,服務器關閉蒜胖,Session 將被序列化到磁盤等消别。只要這個 HttpSession 對象存在,用戶就可以根據(jù) Session ID 來獲取到這個對象台谢,也就達到了狀態(tài)的保持寻狂。
上從圖中可以看出從 request.getSession 中獲取的 HttpSession 對象實際上是 StandardSession 對象的門面對象,這與前面的 Request 和 Servlet 是一樣的原理朋沮。下圖是 Session 工作的時序圖:
還有一點與 Session 關聯(lián)的 Cookie 與其它 Cookie 沒有什么不同蛇券,這個配置的配置可以通過 web.xml 中的 session-config 配置項來指定。
參考