前言
這篇文章的出發(fā)點(diǎn)是為了整理Servlet相關(guān)知識點(diǎn),以免在相關(guān)概念混淆或分不清的時(shí)候到處查閱資料。
一、什么是Servlet
-
Servlet是JavaWeb的三大組件(Servlet、Filter、Listener)中最重要的組件,它屬于動態(tài)資源。 - Servlet的作用是處理請求,服務(wù)器會把接收到的請求交給Servlet來處理,在Servlet中通常需要:接收請求數(shù)據(jù);處理請求;完成響應(yīng)。
- 每個(gè)
Servlet都是唯一的,不同的Servlet處理不同的請求,用下圖說明:

說明:假如兩個(gè)瀏覽器同時(shí)發(fā)送a請求,并不會創(chuàng)建兩個(gè)相同的Servlet,這兩個(gè)請求使用同一Servlet實(shí)例:Servlet是線程不安全的。
二、Servlet生命周期
Servlet的生命周期要從Servlet接口來認(rèn)識,Servlet接口一共5個(gè)方法,其中前三個(gè)是生命周期方法:
(1) void init(ServletConfig)
(2) void service(ServletRequest,ServletResponse)
(3) void destory()
(4) ServletConfig getServletConfig()
(5) String getServletInfo()
出生:服務(wù)器創(chuàng)建Servlet
- 當(dāng)Servlet第一次被請求時(shí),或服務(wù)器啟動時(shí),服務(wù)器會創(chuàng)建Servlet實(shí)例。
- 默認(rèn)情況下,服務(wù)器會在某個(gè)Servlet第一次收到請求時(shí)創(chuàng)建它。也可以在web.xml中對Servlet進(jìn)行配置,使服務(wù)器啟動時(shí)就創(chuàng)建Servlet。
(1)在<servlet>元素中配置<load-on-startup>元素可以讓服務(wù)器在啟動時(shí)就創(chuàng)建該Servlet
(2)假如多個(gè)<servlet>元素都?xì)W配置了<load-on-startup>元素,那么就會按照數(shù)字大小順序來啟動,數(shù)字越小,啟動越早!
(3)<load-on-startup>元素的值必須是大于等于0的整數(shù)。
<servlet>
<servlet-name>hello2</servlet-name>
<servlet-class>cn.itcast.servlet.Hello2Servlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
服務(wù)器默認(rèn)是在servlet第一次被請求時(shí)創(chuàng)建Servlet實(shí)例,如果希望服務(wù)器啟動時(shí)就創(chuàng)建Servlet實(shí)現(xiàn)需要在web.xml中配置。
- 服務(wù)器只為一個(gè)類型的Servlet創(chuàng)建一個(gè)實(shí)例對象,所以Servlet是單例的。
長大:服務(wù)器初始化Servlet
- 當(dāng)服務(wù)器創(chuàng)建Servlet實(shí)例后會馬上調(diào)用Servlet的init(ServletConfig)方法,完成對Servlet的初始化;
- init(ServletConfig)只會被調(diào)用一次
- 服務(wù)器會在調(diào)用init()方法時(shí)傳遞ServletConfig參數(shù)
工作:服務(wù)器使用Servlet處理請求
- 當(dāng)Servlet被請求時(shí),服務(wù)器會調(diào)用Servlet的
service(ServletRequest,ServletResponse)方法; - 該方法每處理一次請求,就會被調(diào)用一次,所以它可能會被調(diào)用N次;
- 因?yàn)镾ervlet是單例的,所以可能在同一時(shí)刻一個(gè)Servlet對象會被多個(gè)請求同時(shí)訪問,所以這可能出現(xiàn)線程安全問題;
- Servlet不是線程安全的,這有助與提高效率,但不能讓Servlet具有狀態(tài),以免多個(gè)線程爭搶數(shù)據(jù);
死亡:服務(wù)器銷毀Servlet
- 服務(wù)器通常不會銷毀Servlet,通常只有在服務(wù)器關(guān)閉時(shí)才會銷毀Servlet;
- 服務(wù)器會在銷毀Servlet之前調(diào)用Servlet的destory()方法
- 可以在destory()方法中給出釋放Servlet占有的資源,但通常Servlet是沒什么可要釋放的,所以該方法一般都是空的
三、實(shí)現(xiàn)Servlet的方式
1、實(shí)現(xiàn)Servlet有下面三種方式
- 實(shí)現(xiàn)javax.servlet.Servlet接口;
繼承javax.servlet.GenericServlet類;
繼承javax.servlet.http.HttpServlet類;(一般采用這種方式)
三者關(guān)系為:
GenericServlet是Servlet接口的實(shí)現(xiàn)類;HttpServlet類是GenericServlet的子類,它提供了對HTTP請求的特殊支持。
public abstract class GenericServlet implements Servlet, ServletConfig,
java.io.Serializable {......}
public abstract class HttpServlet extends GenericServlet {......}
2、Servlet接口
public interface Servlet {
public void init(ServletConfig config) throws ServletException;
public ServletConfig getServletConfig();
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
public String getServletInfo();
public void destroy();
-
ServletConfig
ServletConfig對象是由服務(wù)器創(chuàng)建的,然后傳遞給Servlet的init()方法,對應(yīng)web.xml文件中的<servlet>元素。該對象的常用方法有如下幾個(gè):
String getServletName()
獲取Servlet在web.xml文件中的配置名稱,即<servlet-name>指定的名稱;
ServletContext getServletContext()
用來獲取ServletContext對象
String getInitParameter(String name)
用來獲取在web.xml中配置的初始化參數(shù),通過參數(shù)名來獲取參數(shù)值;
Enumeration getInitParameterNames()
用來獲取在web.xml中配置的所有初始化參數(shù)名稱;
例如web.xml有如下servlet元素:
<servlet>
<servlet-name>Hello</servlet-name>
<servlet-class>com.example.servlet.HelloServlet</servlet-class>
<init-param>
<param-name>paramName1</param-name>
<param-value>paramValue1</param-value>
</init-param>
<init-param>
<param-name>paramName2</param-name>
<param-value>paramValue2</param-value>
</init-param>
</servlet>
那么,可以通過以下方法來獲取相應(yīng)的信息:
public class Hello implements Servlet {
public void init(ServletConfig config) throws ServletException {
config.getServletName();//將得到Hello
config.getInitParameter("paramName1");//將得到paramValue1
config.getInitParameter("paramName2");//將得到paramValue2
...
}
...
}
-
ServletRequest
表示請求對象,它封裝了所有與請求相關(guān)的數(shù)據(jù),它是由服務(wù)器創(chuàng)建的; -
ServletResponse
表示響應(yīng)對象,在service()方法中完成對客戶端的響應(yīng)需要使用這個(gè)對象;
3、GenericServlet
-
GenericServlet是Servlet接口的實(shí)現(xiàn)類,可以通過繼承GenericServlet來編寫自己的Servlet。下面是GenericServlet類的源代碼:
public abstract class GenericServlet implements Servlet, ServletConfig,
java.io.Serializable {
private static final long serialVersionUID = 1L;
private transient ServletConfig config;
public GenericServlet() {}
@Override
public void destroy() {}
@Override
public String getInitParameter(String name) {
return getServletConfig().getInitParameter(name);
}
@Override
public Enumeration<String> getInitParameterNames() {
return getServletConfig().getInitParameterNames();
}
@Override
public ServletConfig getServletConfig() {
return config;
}
@Override
public ServletContext getServletContext() {
return getServletConfig().getServletContext();
}
@Override
public String getServletInfo() {
return "";
}
@Override
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public void init() throws ServletException {}
public void log(String msg) {
getServletContext().log(getServletName() + ": " + msg);
}
public void log(String message, Throwable t) {
getServletContext().log(getServletName() + ": " + message, t);
}
@Override
public abstract void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
@Override
public String getServletName() {
return config.getServletName();
}
}
從代碼中可以看出:
(1)
GenericServlet定義了一個(gè)ServletConfig config實(shí)例變量,并在init(ServletConfig)方法中把參數(shù)ServletConfig賦給了實(shí)例變量,然后在該類的很多方法中使用了實(shí)例變量config。子類不能覆蓋init(ServletConfig config),而應(yīng)該去覆蓋GenericServlet提供的init()方法,它是沒有參數(shù)的init()方法。
(2)GenericServlet實(shí)現(xiàn)了ServletConfig接口,所以可以直接調(diào)用getInitParameter()、getServletContext()等ServletConfig的方法。
4、HttpServlet
-
HttpServlet類是GenericServlet的子類,它提供了對HTTP請求的特殊支持,所以通常我們都會通過繼承HttpServlet來完成自定義的Servlet。 -
HttpServlet覆蓋了service()方法
HttpServlet把ServletRequest和ServletResponse強(qiáng)轉(zhuǎn)成了HttpServletRequest和HttpServletResponse,HttpServlet的service(HttpServletRequest,HttpServletResponse)方法會去判斷當(dāng)前請求是GET還是POST(或其他),如果是GET請求,那么會去調(diào)用本類的doGet()方法,如果是POST請求會去調(diào)用doPost()方法,也就是說,我們在子類中去覆蓋doGet()或doPost()方法即可。 -
HttpServletRequest
HttpServletRequest是ServletRequest的子類
public interface HttpServletRequest extends ServletRequest {......}
HttpServletRequest的方法:
String getParameter(String paramName)
獲取指定請求參數(shù)的值;
String getMethod()
:獲取請求方法,例如GET或POST;
String getHeader(String name)
:獲取指定請求頭的值;
void setCharacterEncoding(String encoding)
:設(shè)置請求體的編碼。
因?yàn)閌GET`請求沒有請求體,所以這個(gè)方法只只對POST請求有效。
當(dāng)調(diào)用`request.setCharacterEncoding(“utf-8”)`之后,
再通過`getParameter()`方法獲取參數(shù)值時(shí),
那么參數(shù)值都已經(jīng)通過了轉(zhuǎn)碼,即轉(zhuǎn)換成了UTF-8編碼。
所以,這個(gè)方法必須在調(diào)用getParameter()方法之前調(diào)用!
-
HttpServletResponse
HttpServletResponse是ServletResponse的子類。
public interface HttpServletResponse extends ServletResponse{......}
HttpServletResponse方法:
PrintWriter getWriter()
獲取字符響應(yīng)流,使用該流可以向客戶端輸出響應(yīng)信息。
例如response.getWriter().print(“<h1>Hello JavaWeb!</h1>”);
ServletOutputStream getOutputStream()
獲取字節(jié)響應(yīng)流,當(dāng)需要向客戶端響應(yīng)字節(jié)數(shù)據(jù)時(shí),需要使用這個(gè)流;
void setCharacterEncoding(String encoding)
用來設(shè)置字符響應(yīng)流的編碼,
例如在調(diào)用setCharacterEncoding(“utf-8”);之后,
再response.getWriter()獲取字符響應(yīng)流對象,這時(shí)的響應(yīng)流的編碼為utf-8,
使用response.getWriter()輸出的中文都會轉(zhuǎn)換成utf-8編碼后發(fā)送給客戶端;
void setHeader(String name, String value)
向客戶端添加響應(yīng)頭信息,
例如setHeader(“Refresh”, “3;url=http://www.example.cn”),
表示3秒后自動刷新到http://www.example.cn;
void setContentType(String contentType)
該方法是setHeader(“content-type”, “xxx”)的簡便方法,
即用來添加名為content-type響應(yīng)頭的方法。
content-type響應(yīng)頭用來設(shè)置響應(yīng)數(shù)據(jù)的`MIME`類型;
void sendError(int code, String errorMsg)
向客戶端發(fā)送狀態(tài)碼,以及錯(cuò)誤消息。
例如給客戶端發(fā)送404:response(404, “您要查找的資源不存在!”)。
四、Servlet與線程安全
因?yàn)橐粋€(gè)類型的
Servlet只有一個(gè)實(shí)例對象,那么就有可能會出現(xiàn)一個(gè)Servlet同時(shí)處理多個(gè)請求的情況,那么Servlet是否為線程安全的呢?答案是:“不是線程安全的”。這說明Servlet的工作效率很高,但也存在線程安全問題:當(dāng)兩個(gè)或多個(gè)線程同時(shí)訪問同一個(gè)Servlet時(shí),可能會發(fā)生多個(gè)線程同時(shí)訪問同一資源的情況,數(shù)據(jù)可能會變得不一致。
所以我們不應(yīng)該在Servlet中隨意創(chuàng)建成員變量,因?yàn)榭赡軙嬖谝粋€(gè)線程對這個(gè)成員變量進(jìn)行寫操作,另一個(gè)線程對這個(gè)成員變量進(jìn)行讀操作。
所以:
- 不要在Servlet中創(chuàng)建成員!創(chuàng)建局部變量即可!
- 可以創(chuàng)建無狀態(tài)成員!
- 可以創(chuàng)建有狀態(tài)的成員,但狀態(tài)必須為只讀的!
Servlet體系結(jié)構(gòu)是建立在Java多線程機(jī)制之上的,它的生命周期是由Web容器負(fù)責(zé)的。當(dāng)客戶端第一次請求某個(gè)Servlet 時(shí),Servlet容器將會根據(jù)web.xml配置文件實(shí)例化這個(gè)Servlet類。當(dāng)有新的客戶端請求該Servlet時(shí),一般不會再實(shí)例化該 Servlet類,也就是有多個(gè)線程在使用這個(gè)實(shí)例。Servlet容器會自動使用線程池等技術(shù)來支持系統(tǒng)的運(yùn)行。
有助理解的一個(gè)例子:
public class Concurrent extends HttpServlet {
PrintWriter output;
public void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username;
response.setContentType("text/html; charset=gb2312");
username = request.getParameter("username");
output = response.getWriter();
try {
Thread.sleep(5000); // 為了突出并發(fā)問題,在這設(shè)置一個(gè)延時(shí)
} catch (Exception e) {
}
output.println("用戶名:" + username + "<BR>");
}
}
該Servlet中定義了一個(gè)實(shí)例變量output(servlet實(shí)例的成員變量),在service方法將其賦值為用戶的輸出。當(dāng)一個(gè)用戶訪問該Servlet時(shí),程序會正常的運(yùn)行;但當(dāng)多個(gè)用戶并發(fā)訪問時(shí),就可能會出現(xiàn)其它用戶的信息顯示在另外一些用戶的瀏覽器上的問題。
解決方法:
- 使用Javax.servlet.SingleThreadModel(Servlet2.4中已經(jīng)廢棄該接口),此時(shí)Servlet容器將保證Servlet實(shí)例以單線程方式運(yùn)行,也就是說,同一時(shí)刻,只會有一個(gè)線程執(zhí)行Servlet的service()方法。但是,如果一個(gè)Servlet實(shí)現(xiàn)了 SingleThreadModel接口,Servlet引擎將為每個(gè)新的請求創(chuàng)建一個(gè)單獨(dú)的Servlet實(shí)例,這將引起大量的系統(tǒng)開銷。 SingleThreadModel在Servlet2.4中已不再提倡使用;
將前面的Concurrent Test類的類頭定義更改為:
Public class Concurrent Test extends HttpServlet implements SingleThreadModel {
……
}
- 去除實(shí)例變量,使用局部變量。這是保證Servlet線程安全的最佳選擇。
public class Concurrent extends HttpServlet {
public void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
PrintWriter output;
String username;
response.setContentType("text/html; charset=gb2312");
username = request.getParameter("username");
output = response.getWriter();
try {
Thread.sleep(5000); // 為了突出并發(fā)問題,在這設(shè)置一個(gè)延時(shí)
} catch (Exception e) {
}
output.println("用戶名:" + username + "<BR>");
}
}
- 使用同步代碼塊:synchronized{…};但是在程序中使用同步來保護(hù)要使用的共享的數(shù)據(jù),也會使系統(tǒng)的性能大大下降。這是因?yàn)楸煌降拇a塊在同一時(shí)刻只能有一個(gè)線程執(zhí)行它,使得其同時(shí)處理客戶請求的吞吐量降低,而且很多客戶處于阻塞狀態(tài)。另外為保證主存內(nèi)容和線程的工作內(nèi)存中的數(shù)據(jù)的一致性,要頻繁地刷新緩存,這也會大大地影響系統(tǒng)的性能。在實(shí)際的開發(fā)中也應(yīng)避免或最小化 Servlet 中的同步代碼;
public void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username;
response.setContentType("text/html; charset=gb2312");
username = request.getParameter("username");
synchronized(this){
output = response.getWriter();
try {
Thread.sleep(5000); // 為了突出并發(fā)問題,在這設(shè)置一個(gè)延時(shí)
} catch (Exception e) {
}
output.println("用戶名:" + username + "<BR>");
}
}
不同變量的線程安全問題
在Servlet和JSP中,變量可以歸為下面的幾類:類變量:request,response,session,config,application,以及JSP頁面內(nèi)置的page, pageContext。其中除了application外,其它都是線程安全的。
實(shí)例變量:實(shí)例變量是實(shí)例所有的,在堆中分配。在Servlet和JSP容器中,一般僅實(shí)例化一個(gè)Servlet和JSP實(shí)例,啟動多個(gè)該實(shí)例的線程來處理請求。而實(shí)例變量是該實(shí)例所有的線程所共享,所以,實(shí)例變量不是線程安全的。
局部變量:局部變量在堆棧中分配,因?yàn)槊恳粋€(gè)線程有自己的執(zhí)行堆棧,所以,局部變量是線程安全的。
五、<url-pattern>
-
<url-pattern>是<servlet-mapping>的子元素,用來指定Servlet的訪問路徑,即URL。 - 它必須以
/開頭! - 可以在<servlet-mapping>中給出多個(gè)<url-pattern>,例如:
<servlet-mapping>
<servlet-name>AServlet</servlet-name>
<url-pattern>/AServlet</url-pattern>
<url-pattern>/BServlet</url-pattern>
</servlet-mapping>
說明一個(gè)Servlet綁定了兩個(gè)URL,無論訪問/AServlet還是/BServlet,訪問的都是AServlet。
- 在<url-pattern>中使用通配符
(1)所謂通配符就是星號
*,使用通配符可以命名一個(gè)Servlet綁定一組URL。
(2)通配符要么為前綴,要么為后綴,不能出現(xiàn)在URL中間位置,也不能只有通配符。
(3)通配符是一種模糊匹配URL的方式,如果存在更具體的<url-pattern>,那么訪問路徑會去匹配具體的<url-pattern>。
六、ServletContext
6.1 ServletContext概述
- 服務(wù)器會為每個(gè)應(yīng)用創(chuàng)建唯一一個(gè)ServletContext對象;(應(yīng)用內(nèi)唯一)
- ServletContext對象的創(chuàng)建是在服務(wù)器啟動時(shí)完成的;(出生)
- ServletContext對象的銷毀是在服務(wù)器關(guān)閉時(shí)完成的。(死亡)
- ServletContext對象的作用是在整個(gè)Web應(yīng)用的動態(tài)資源之間共享數(shù)據(jù)。(作用)
6.2 獲取ServletContext
- 在Servlet中獲取ServletContext對象
在
void init(ServletConfig config)中:ServletContext context = config.getServletContext();,ServletConfig類的getServletContext()方法可以用來獲取ServletContext對象;
- 在GenericeServlet或HttpServlet中獲取ServletContext對象
直接使用
this.getServletContext()來獲取;
6.3 ServletContext對象的作用
-
ServletContext是JavaWeb四大域?qū)ο螅?code>PageContext、ServletRequest、HttpSession、ServletContext)之一。 - 所有域?qū)ο蠖加写嫒?shù)據(jù)的功能,因?yàn)橛驅(qū)ο髢?nèi)部有一個(gè)Map,用來存儲數(shù)據(jù)。
-
ServletContext對象用來操作數(shù)據(jù)的方法
void setAttribute(String name, Object value)
用來存儲一個(gè)對象,也可以稱之為存儲一個(gè)域?qū)傩裕?例如:servletContext.setAttribute(“xxx”, “XXX”),在ServletContext中保存了一個(gè)域?qū)傩?,域?qū)傩悦Q為xxx,域?qū)傩缘闹禐閄XX。
請注意,如果多次調(diào)用該方法,并且使用相同的name,那么會覆蓋上一次的值,這一特性與Map相同;
Object getAttribute(String name)
用來獲取ServletContext中的數(shù)據(jù),當(dāng)前在獲取之前需要先去存儲才行,
例如:String value = (String)servletContext.getAttribute(“xxx”);,獲取名為xxx的域?qū)傩裕?
void removeAttribute(String name)
用來移除ServletContext中的域?qū)傩?,如果參?shù)name指定的域?qū)傩圆淮嬖?,那么本方法什么都不做?
Enumeration getAttributeNames()
獲取所有域?qū)傩缘拿Q;
-
ServletContext獲取應(yīng)用初始化參數(shù)
(1)Servlet也可以獲取初始化參數(shù),但它是局部的參數(shù);也就是說,一個(gè)Servlet只能獲取自己的初始化參數(shù),不能獲取別人的,即初始化參數(shù)只為一個(gè)Servlet準(zhǔn)備!
(2)可以配置公共的初始化參數(shù),為所有Servlet而用!這需要使用ServletContext才能使用!
(3)還可以使用ServletContext來獲取在web.xml文件中配置的應(yīng)用初始化參數(shù)!注意,應(yīng)用初始化參數(shù)與Servlet初始化參數(shù)不同。
<web-app ...>
...
<context-param>
<param-name>paramName1</param-name>
<param-value>paramValue1</param-value>
</context-param>
<context-param>
<param-name>paramName2</param-name>
<param-value>paramValue2</param-value>
</context-param>
</web-app>
-
ServletContext獲取資源(略)