一喷屋、基本概念
Servlet是運(yùn)行在Web服務(wù)器上的小程序琳拨,通過(guò)http協(xié)議和客戶(hù)端進(jìn)行交互。
這里的客戶(hù)端一般為瀏覽器屯曹,發(fā)送http請(qǐng)求(request)給服務(wù)器(如Tomcat)狱庇。服務(wù)器接收到請(qǐng)求后選擇相應(yīng)的Servlet進(jìn)行處理,并給出響應(yīng)(response)恶耽。
從這里可以看出Servlet并不是獨(dú)立運(yùn)行的程序密任,而是以服務(wù)器為宿主,由服務(wù)器進(jìn)行調(diào)度的偷俭。通常我們把能夠運(yùn)行Servlet的服務(wù)器稱(chēng)作Servlet容器浪讳,如Tomcat。
這里Tomcat為什么能夠根據(jù)客戶(hù)端的請(qǐng)求去選擇相應(yīng)的Servlet去執(zhí)行的呢涌萤?答案是:Servlet規(guī)范淹遵。因?yàn)镾ervlet和Servlet容器都是遵照Servlet規(guī)范去開(kāi)發(fā)的口猜。簡(jiǎn)單點(diǎn)說(shuō):我們要寫(xiě)一個(gè)Servlet,就需要直接或間接實(shí)現(xiàn)javax.servlet.Servlet透揣。并且在web.xml中進(jìn)行相應(yīng)的配置济炎。Tomcat在接收到客戶(hù)端的請(qǐng)求時(shí),會(huì)根據(jù)web.xml里面的配置去加載辐真、初始化對(duì)應(yīng)的Servlet實(shí)例须尚。這個(gè)就是規(guī)范,就是雙方約定好的侍咱。
二耐床、樣例分析
在進(jìn)一步解釋Servlet原理、分析源碼之前楔脯,我們先介紹下如何在JavaWeb中使用Servlet撩轰。方法很簡(jiǎn)單:1.編寫(xiě)自己的Servlet類(lèi),這里可以使用開(kāi)發(fā)工具(STS淤年、Myeclipse等)根據(jù)向?qū)Э焖俚纳梢粋€(gè)Servlet類(lèi)钧敞。2.在web.xml中配置servlet蜡豹。這里的知識(shí)很簡(jiǎn)單麸粮,所以不做過(guò)多贅述。直接上代碼镜廉。(這里需要注意的是弄诲,servlet3.0之后提供了注解WebServlet的方式配置servlet,這里就不做介紹了娇唯,感興趣的可以自行去百度齐遵,只是配置的形式不同而已,沒(méi)有本質(zhì)區(qū)別塔插。所以下文還是為web.xml為例)
TestServlet.java
public class TestServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public TestServlet() {
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().append("Served at: ").append(request.getContextPath());
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
web.xml
<servlet>
<servlet-name>TestServlet</servlet-name>
<servlet-class>com.nantang.servlet.TestServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>TestServlet</servlet-name>
<url-pattern>/test</url-pattern>
</servlet-mapping>
啟動(dòng)Tomcat梗摇,瀏覽器訪(fǎng)問(wèn)/test。將會(huì)訪(fǎng)問(wèn)TestServlet想许。返回客戶(hù)端請(qǐng)求的上下文路徑伶授。
這里需要擴(kuò)展的有幾點(diǎn):
1.如果一個(gè)servlet需要映射多個(gè)url-pattern,那么就在<servlet-mapping></servlet-mapping>標(biāo)簽下寫(xiě)多個(gè)<url-pattern></url-pattern>流纹,如:
<servlet-mapping>
<servlet-name>TestServlet</servlet-name>
<url-pattern>/test1</url-pattern>
<url-pattern>/test2</url-pattern>
</servlet-mapping>
2.對(duì)于不同的servlet糜烹,不允許出現(xiàn)相同的url-pattern。
3.如果不同的servlet漱凝,它們的url-patter存在包含關(guān)系疮蹦,那么容器會(huì)調(diào)用更具象的servlet去處理客戶(hù)端請(qǐng)求。比如有兩個(gè)servlet茸炒,servlet1的url-pattern是"/"愕乎,servlet2的url-pattern是"/test"阵苇。那么這個(gè)時(shí)候如果客戶(hù)端調(diào)用的url是http://localhost:8080/demo/test,容器會(huì)使用servlet2去處理客戶(hù)端的請(qǐng)求感论。雖然說(shuō)"/"和"/test"都匹配客戶(hù)請(qǐng)求的url慎玖,但是容器會(huì)選擇更貼切的。這里不會(huì)出現(xiàn)多個(gè)servlet處理同一個(gè)請(qǐng)求的現(xiàn)象笛粘。
三趁怔、源碼分析
上面說(shuō)過(guò),我們自己編寫(xiě)的Servlet類(lèi)都必須直接或間接實(shí)現(xiàn)javax.servlet.Servlet薪前∽∈茫可是上面的例子TestServlet繼承的是HttpServlet,那是因?yàn)镠ttpServlet間接的實(shí)現(xiàn)了javax.servlet.Servlet炭菌。下面是HttpServlet的繼承層級(jí)(類(lèi)圖中的方法并沒(méi)有一一列舉为严,因?yàn)橄旅鏁?huì)逐一解釋?zhuān)?/p>
下面我們由上往下層層分析:
1 ServletContext
一個(gè)web應(yīng)用對(duì)應(yīng)一個(gè)ServletContext實(shí)例,關(guān)于ServletContext的詳細(xì)介紹垛膝,可以參考另一篇博文ServletContext鳍侣。
2.ServletConfig
ServletConfig實(shí)例是由servlet容器構(gòu)造的,當(dāng)需要初始化servlet的時(shí)候吼拥,容器根據(jù)web.xml中的配置以及運(yùn)行時(shí)環(huán)境構(gòu)造出ServletConfig實(shí)例倚聚,并通過(guò)回調(diào)servlet的init方法傳遞給servlet(這個(gè)方法后面會(huì)講到)。所以一個(gè)servlet實(shí)例對(duì)應(yīng)一個(gè)ServletConfig實(shí)例凿可。
public interface ServletConfig {
public String getServletName();
public ServletContext getServletContext();
public String getInitParameter(String name);
public Enumeration getInitParameterNames();
}
2.1 getServletName
getServletName方法返回servlet實(shí)例的名稱(chēng)惑折,這個(gè)就是我們?cè)趙eb.xml中<servlet-name>標(biāo)簽中配置的名字,當(dāng)然也可以在服務(wù)器控制臺(tái)去配置枯跑。如果這兩個(gè)地方都沒(méi)有配置servlet名稱(chēng)惨驶,那么將會(huì)返回servlet的類(lèi)名。
2.2 getServletContext
getServletContext方法返回ServletContext實(shí)例敛助,也就是我們上面說(shuō)的應(yīng)用上下文粗卜。
2.3 getInitParameter和getInitParameterNames
這兩個(gè)方法是用來(lái)獲取servlet的初始化參數(shù)的,這個(gè)參數(shù)是在web.xml里面配置的(如下所示)纳击。getInitParameter是根據(jù)參數(shù)名獲取參數(shù)值续扔,getInitParameterNames獲取參數(shù)名集合。
這里需要注意的是當(dāng)需要配置多個(gè)初始化參數(shù)時(shí)评疗,應(yīng)該寫(xiě)多個(gè)<init-param></init-param>對(duì)测砂,而不是在一個(gè)<init-param></init-param>對(duì)里面寫(xiě)多個(gè)<param-name></param-name>和<param-value></param-value>對(duì)。
<servlet>
<servlet-name>TestServlet</servlet-name>
<servlet-class>com.nantang.servlet.TestServlet</servlet-class>
<init-param>
<param-name>a</param-name>
<param-value>1</param-value>
</init-param>
<init-param>
<param-name>b</param-name>
<param-value>2</param-value>
</init-param>
</servlet>
3 Servlet
最原始最簡(jiǎn)單的JaveWeb模型百匆,就是一個(gè)servlet容器上運(yùn)行著若干個(gè)servlet用來(lái)處理客戶(hù)端的請(qǐng)求砌些。所以說(shuō)servlet是JavaWeb最核心的東西,我們的業(yè)務(wù)邏輯基本上都是通過(guò)servlet實(shí)現(xiàn)的(雖然現(xiàn)在有各種框架,不用去直接編寫(xiě)servlet存璃,但本質(zhì)上還是在使用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();
}
所有的servlet都是javax.servlet.Servlet的子類(lèi),就像Java里面所有的類(lèi)都是Object的子類(lèi)一樣纵东。Servlet類(lèi)規(guī)定了每個(gè)servlet應(yīng)該實(shí)現(xiàn)的方法粘招,這個(gè)是遵循Servlet規(guī)范的。但是自定義的servlet一般不用直接實(shí)現(xiàn)Servlet偎球,而是繼承javax.servlet.GenericServlet或者javax.servlet.http.HttpServlet就行了洒扎。我們上面的TestServlet就是繼承HttpServlet,這是因?yàn)镠ttpServlet間接實(shí)現(xiàn)了Servlet衰絮,提供了通用的功能袍冷。所以我們?cè)谧远x的TestServlet里面只需要專(zhuān)注實(shí)現(xiàn)業(yè)務(wù)邏輯就行了。
Servlet里面有三個(gè)比較重要的方法:init猫牡、service胡诗、destroy。它們被稱(chēng)作是servlet生命周期的方法淌友,它們都是由servlet容器調(diào)用煌恢。另外兩個(gè)方法用于獲取servlet相關(guān)信息的,需要根據(jù)業(yè)務(wù)邏輯進(jìn)行實(shí)現(xiàn)和調(diào)用震庭。
3.1 init
init方法是servlet的初始化方法瑰抵,當(dāng)客戶(hù)端第一次請(qǐng)求servlet的時(shí)候,JVM對(duì)servlet類(lèi)進(jìn)行加載和實(shí)例化归薛。(如果需要容器啟動(dòng)時(shí)就初始化servlet谍憔,可以在web.xml配置<load-on-startup>1</load-on-startup>)
這里需要注意的是,servlet會(huì)先執(zhí)行默認(rèn)的構(gòu)造函數(shù)主籍,然后回調(diào)servlet實(shí)例的init方法,傳入ServletConfig參數(shù)逛球。這個(gè)參數(shù)上面說(shuō)過(guò)千元,是servlet容器根據(jù)web.xml中的配置和運(yùn)行時(shí)環(huán)境構(gòu)造的實(shí)例。通過(guò)init方法注入到servlet颤绕。init方法在servlet的生命周期中只會(huì)被調(diào)用一次幸海,在客戶(hù)端的后續(xù)請(qǐng)求中將不會(huì)再調(diào)用。
3.2 service
service方法是處理業(yè)務(wù)邏輯的核心方法奥务。當(dāng)servlet容器接收到客戶(hù)端的請(qǐng)求后物独,會(huì)根據(jù)web.xml中配置的<url-pattern>找到相應(yīng)的servlet,回調(diào)service方法處理客戶(hù)端的請(qǐng)求并給出響應(yīng)氯葬。
3.3 destroy
JDK文檔解釋這個(gè)方法說(shuō):這個(gè)方法會(huì)在所有的線(xiàn)程的service()方法執(zhí)行完成或者超時(shí)后執(zhí)行挡篓。這里只是說(shuō)明了,當(dāng)servlet容器要去調(diào)用destroy方式的時(shí)候,需要等待一會(huì)官研,等待所有線(xiàn)程都執(zhí)行完或者達(dá)到超時(shí)的限制秽澳。
這里并沒(méi)有說(shuō)清楚什么情況下servlet容器會(huì)觸發(fā)這個(gè)動(dòng)作。How Tomcat Works一書(shū)中對(duì)這個(gè)做了解釋?zhuān)寒?dāng)servlet容器關(guān)閉或需要更多內(nèi)存的時(shí)候戏羽,會(huì)銷(xiāo)毀servlet担神。這個(gè)方法就使得servlet容器擁有回收資源的能力。
同樣地始花,destroy方法在servlet的生命周期中只會(huì)被調(diào)用一次妄讯。
3.4 getServletConfig
這個(gè)方法返回ServletConfig實(shí)例,這個(gè)對(duì)象即為servlet容器回調(diào)init方法的時(shí)候傳入的實(shí)例酷宵。所以自定義的Servlet一般的實(shí)現(xiàn)方式為:在init方法里面把傳入的ServletConfig存儲(chǔ)到servlet的屬性字段捞挥。在getServletConfig的實(shí)現(xiàn)里返回該實(shí)例。這個(gè)在后續(xù)解釋javax.servlet.GenericServlet的源碼時(shí)忧吟,能夠看到砌函。
3.5 getServletInfo
返回關(guān)于servlet的信息,這個(gè)由自定義的servlet自行實(shí)現(xiàn)溜族,不過(guò)一般建議返回servlet的作者讹俊、版本號(hào)、版權(quán)等信息煌抒。
4.GenericServlet
GenericServlet從名字就能看的出來(lái)是servlet的一般實(shí)現(xiàn)仍劈,實(shí)現(xiàn)了servlet具有的通用功能,所以我們自定義的servlet一般不需要直接實(shí)現(xiàn)Servlet接口寡壮,只需要集成GenericServlet贩疙。GenericServlet實(shí)現(xiàn)了Servlet和ServletConfig接口。
4.1 GenericServlet對(duì)Servlet接口的實(shí)現(xiàn)
private transient ServletConfig config;
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public void init() throws ServletException {}
public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
public void destroy() {}
public ServletConfig getServletConfig() {
return config;
}
public String getServletInfo() {
return "";
}
可以說(shuō)况既,GenericServlet對(duì)Servlet方法的實(shí)現(xiàn)邏輯非常簡(jiǎn)單这溅。就是把一些必要的邏輯寫(xiě)了下。
1.init方法就是把容器傳入的ServletConfig實(shí)力存儲(chǔ)在類(lèi)的私有屬性conifg里面棒仍,然后調(diào)用一個(gè)init無(wú)參的空方法悲靴。這么做的意義在于,我們?nèi)绻朐谧远x的servlet類(lèi)里面在初始化的時(shí)候添加些業(yè)務(wù)邏輯莫其,只需要重寫(xiě)無(wú)參的init方法就好了癞尚,我們不需要關(guān)注ServletConfig實(shí)例的存儲(chǔ)細(xì)節(jié)了。
2.service和destroy方法并未實(shí)現(xiàn)具體邏輯乱陡。
3.getServletConfig就是返回init方法里面存儲(chǔ)的config浇揩。getServletInfo就是返回空字符串,如果有業(yè)務(wù)需要憨颠,可以在子類(lèi)里面重寫(xiě)胳徽。
4.2 GenericServlet對(duì)于ServletConfig接口的實(shí)現(xiàn)
public String getServletName() {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}
return sc.getServletName();
}
public ServletContext getServletContext() {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}
return sc.getServletContext();
}
public String getInitParameter(String name) {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}
return sc.getInitParameter(name);
}
public Enumeration getInitParameterNames() {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}
return sc.getInitParameterNames();
}
這四個(gè)方法的實(shí)現(xiàn)就跟一個(gè)模子刻出來(lái)的一樣,都是取得ServletConfig實(shí)例,然后調(diào)用相應(yīng)的方法膜廊。其實(shí)GenericServlet完全沒(méi)有必要實(shí)現(xiàn)ServletConfig乏沸,這么做僅僅是為了方便。當(dāng)我們集成GenericServlet寫(xiě)自己的servlet的時(shí)候爪瓜,如果需要獲取servlet的配置信息如初始化參數(shù)蹬跃,就不需要寫(xiě)形如:“ServletConfig sc = getServletConfig();if (sc == null) ...;return sc.getInitParameterNames();”這些冗余代碼了。除此之外铆铆,沒(méi)有別的意義蝶缀。
5 HttpServlet
HttpServlet是一個(gè)針對(duì)HTTP協(xié)議的通用實(shí)現(xiàn),它實(shí)現(xiàn)了HTTP協(xié)議中的基本方法get薄货、post等翁都,通過(guò)重寫(xiě)service方法實(shí)現(xiàn)方法的分派。
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException{
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
} catch (ClassCastException e) {
throw new ServletException("non-HTTP request or response");
}
service(request, response);
}
重寫(xiě)的service方法將參數(shù)轉(zhuǎn)換成HttpServletRequest和HttpServletResponse谅猾,并調(diào)用自己的另一個(gè)重載service方法柄慰。
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}
這個(gè)方法的的邏輯也很簡(jiǎn)單,就是解析出客戶(hù)端的request是哪種中方法税娜,如果是get方法則調(diào)用doGet坐搔,如果是post則調(diào)用doPost等等。這樣我們?cè)诶^承HttpServlet的時(shí)候就無(wú)需重寫(xiě)service方法敬矩,我們可以根據(jù)自己的業(yè)務(wù)重寫(xiě)相應(yīng)的方法概行。一般情況下我們的應(yīng)用基本就是get和post調(diào)用。那么我們只需要重寫(xiě)doGet和doPost就行了弧岳。
這里需要注意的是3-15行代碼凳忙,這里對(duì)資源(比如頁(yè)面)的修改時(shí)間進(jìn)行驗(yàn)證,判斷客戶(hù)端是否是第一次請(qǐng)求該資源禽炬,或者該資源是否被修改過(guò)涧卵。如果這兩個(gè)條件有一個(gè)被滿(mǎn)足那么就調(diào)用doGet方法。否則返回狀態(tài)304(HttpServletResponse.SC_NOT_MODIFIED)瞎抛,這個(gè)狀態(tài)就是告訴客戶(hù)端(瀏覽器)艺演,可以只用自己上一次對(duì)該資源的緩存。
不過(guò)HttpServlet對(duì)于判斷資源修改時(shí)間的邏輯非常簡(jiǎn)單粗暴:
protected long getLastModified(HttpServletRequest req) {
return -1;
}
方法始終返回-1桐臊,這樣就會(huì)導(dǎo)致每次都會(huì)調(diào)用doGet方法從服務(wù)器取資源而不會(huì)使用瀏覽器的本地緩存。所以如果我們自己的servlet要使用瀏覽器的緩存晓殊,降低服務(wù)器的壓力断凶,就需要重寫(xiě)getLastModified方法。
最后我們來(lái)看一下HttpServlet對(duì)http一些方法的實(shí)現(xiàn)巫俺,在所有的方法中认烁,HttpServlet已經(jīng)對(duì)doOptions和doTrace方法實(shí)現(xiàn)了通用的邏輯,所以我們一般不用重寫(xiě)這兩個(gè)方法,感興趣的可以自己去看下源碼却嗡。
這里我們列舉下最常用的兩個(gè)方法doGet和doPost:
protected void doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException{
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
} else {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
}
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_post_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
} else {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
}
}
其實(shí)這兩個(gè)實(shí)現(xiàn)里面也沒(méi)做什么有用的邏輯舶沛,所以一般情況下都要重寫(xiě)這兩個(gè)方法,就像我們最初的TestServlet那樣窗价。doHead如庭、doPut、doDelete也是這樣的代碼模板撼港,所以如果有業(yè)務(wù)需要的話(huà)坪它,我們都要重寫(xiě)對(duì)應(yīng)的方法。
四帝牡、總結(jié)
講了這么多往毡,可以這么說(shuō):Servlet是JavaWeb里面最核心的組件。只有對(duì)它完全融會(huì)貫通靶溜,才能去進(jìn)一步去理解上層框架Struts开瞭、Spring等。
另外需要明確的是:一個(gè)Web應(yīng)用對(duì)應(yīng)一個(gè)ServletContext罩息,一個(gè)Servlet對(duì)應(yīng)一個(gè)ServletConfig嗤详。每個(gè)Servlet都是單例的,所以需要自己處理好并發(fā)的場(chǎng)景扣汪。