講一下servlet的生命周期與運(yùn)行時(shí)的線程模型,對了解servlet的運(yùn)行原理有所幫助,這樣才能避免一些有沖突的設(shè)計(jì)。
如果你不滿足以下任一條件,請繼續(xù)閱讀,否則請?zhí)^此后的部分,進(jìn)入下一章:第 15 章 分頁。
了解servlet的生命周期。
了解servlet運(yùn)行時(shí)的線程模型,及設(shè)計(jì)程序時(shí)需要注意的部分。
我們之前使用的都是javax.servlet.http.HttpServlet,這個(gè)類實(shí)現(xiàn)了javax.servlet.Servlet接口,而這個(gè)接口中定義的三個(gè)方法是所有servlet都必須實(shí)現(xiàn)的。
package javax.servlet;public interface Servlet {void init(ServletConfig config);void service(ServletRequest request, ServletResponse response);void destroy();}
如圖所示,tomcat之類的服務(wù)器首先根據(jù)web.xml中的定義實(shí)例化servlet,然后調(diào)用它的init()方法進(jìn)行初始化,init()方法的ServletConfig參數(shù)是服務(wù)器傳遞進(jìn)servlet的,其中包含web.xml配置的初始化信息和ServletContext對象等共享內(nèi)容。
初始化后的servlet實(shí)例便進(jìn)入等待請求的狀態(tài),當(dāng)有與servlet-mapping匹配的請求進(jìn)入時(shí),服務(wù)器會(huì)調(diào)用servlet實(shí)例的service方法,傳入ServletRequest與ServletResponse兩個(gè)參數(shù)等待servlet處理完畢。
注意一點(diǎn),對于每個(gè)web應(yīng)用,內(nèi)存中只存在一個(gè)servlet實(shí)例,所有請求都是調(diào)用這個(gè)servlet實(shí)例,所以我們說servlet不是線程安全的,所有操作都要限制在service()方法中進(jìn)行,不要在servlet中定義類變量。(doGet()和doPost()是HttpServlet覆蓋service()方法后分支出來的輔助方法,實(shí)際上服務(wù)器調(diào)用的還是service()。)
當(dāng)web應(yīng)用卸載時(shí),服務(wù)器會(huì)調(diào)用每個(gè)已經(jīng)初始化的servlet的destroy(),然后銷毀這些servlet實(shí)例,如果你需要在servlet銷毀時(shí)釋放什么資源的話,可以寫在destory()方法中。
那么servlet是在什么時(shí)候進(jìn)行初始化的呢?我們可以通過web.xml中的load-on-startup標(biāo)簽。
<servlet><servlet-name>TestServlet</servlet-name><servlet-class>anni.TestServlet</servlet-class><load-on-startup>1</load-on-startup></servlet>
load-on-startup的值是一個(gè)整數(shù),當(dāng)它大于等于零的時(shí)候服務(wù)器會(huì)在web發(fā)布的時(shí)候初始化servlet。當(dāng)它小于零或者我們沒有設(shè)置load-on-startup的時(shí)候,服務(wù)器會(huì)在用戶第一次訪問servlet的時(shí)候才去初始化servlet。
或許你對load-on-startup為什么是一個(gè)整數(shù)存有疑問,為什么不是true和false呢?這是因?yàn)槿绻覀冊趙eb.xml中設(shè)置了多個(gè)servlet的時(shí)候,可以使用load-on-startup來指定servlet的加載順序,服務(wù)器會(huì)根據(jù)load-on-startup的大小依次對servlet進(jìn)行初始化。不過即使我們將load-on-startup設(shè)置重復(fù)也不會(huì)出現(xiàn)異常,服務(wù)器會(huì)自己決定初始化順序。
回頭看看javax.servlet.Filter中也有init()和destroy()方法,它的聲明周期與servlet基本一致,服務(wù)器使用init()對Filter初始化,銷毀Filter的時(shí)候調(diào)用destroy()方法,只是過濾器就不在有l(wèi)oad-on-startup設(shè)置了,它總是會(huì)在服務(wù)器啟動(dòng)的時(shí)候進(jìn)行初始化,然后按照web.xml定義的順序依次執(zhí)行。
我們做一個(gè)試驗(yàn),以此來證明某些編寫servlet的方法是絕對錯(cuò)誤的。
第一步,我們打開瀏覽器,瀏覽14-02的index.jsp頁面,輸入“叮咚”。
第二步,我們再打開一個(gè)14-02/index.jsp頁面,輸入“lingirl”。
第三步,點(diǎn)擊第一個(gè)頁面的提交按鈕,然后在10秒之內(nèi)點(diǎn)擊另一個(gè)頁面的提交按鈕,等兩個(gè)頁面都提交成功后,我們會(huì)看到如下頁面。
url上有亂碼這個(gè)就是提交“叮咚”的頁面,會(huì)驚訝吧?本來這時(shí)應(yīng)該顯示“叮咚”的。
這個(gè)頁面對應(yīng)提交“lingirl”的頁面,它似乎是顯示正常的。
到底是哪里出錯(cuò)了,為什么第一個(gè)頁面提交了數(shù)據(jù),卻得到第二個(gè)頁面提交的結(jié)果,首先讓我們看一下TestServlet的代碼。
package anni;import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class TestServlet extends HttpServlet {private String username;public void doGet(HttpServletRequest request,HttpServletResponse response)throws ServletException, IOException {this.username = request.getParameter("username");try {Thread.sleep(10000);} catch(InterruptedException ex) {}response.getWriter().write(this.username);}}
doGet()方法中從request中獲得username參數(shù),然后賦給this.username,這是一個(gè)類變量。然后暫停10秒,這10秒我們假設(shè)正在進(jìn)行一些很費(fèi)時(shí)間的計(jì)算,這樣我們就有十秒鐘去點(diǎn)兩個(gè)頁面的提交按鈕了。最后將this.username寫入response。
你也許在想:“這沒有問題啊,第一個(gè)頁面提交了數(shù)據(jù),等待10秒返回,第二個(gè)頁面再提交數(shù)據(jù),等待10秒返回,兩者并不沖突啊。”可實(shí)際上在多線程模型中不會(huì)有這種隊(duì)列讓請求一個(gè)一個(gè)執(zhí)行,所有請求都是蜂擁而至。
在這個(gè)例子里,第一個(gè)請求過來將“叮咚”賦值給this.username后進(jìn)行等待,10秒之內(nèi)我們的第二個(gè)請求又調(diào)用了doGet()方法,并把this.username修改為“lingirl”,等到10秒后第一個(gè)請求結(jié)束等待后,獲得的this.username已經(jīng)是“lingirl”了。
this.username這種寫法在servlet中是絕對禁用的,如果有什么信息需要保存,可以考慮放到session或ServletContext中。
寫在<%%>之間的代碼,在轉(zhuǎn)換成servlet之后都會(huì)service()方法內(nèi)運(yùn)行,所以我們不必?fù)?dān)心出現(xiàn)上邊this.username的問題。
但是我們可以用<%!%>(注意多出來的感嘆號)定義類變量或類方法,把上一個(gè)罪大惡極的servlet改造成jsp的話,就像這樣。
<%@ page contentType="text/html; charset=gb2312"%><%!String username;%><%this.username = request.getParameter("username");try {Thread.sleep(10000);} catch(InterruptedException ex) {}out.write(this.username);%>
使用14-03下的例子可以測試jsp出錯(cuò)的效果,記得要在10秒之內(nèi)點(diǎn)擊兩次。
<%!%>似乎是一個(gè)巨大的陷阱,如果我們使用它定義類變量就一定會(huì)出現(xiàn)多線程錯(cuò)誤。
不過凡事都有正反兩面,當(dāng)我們需要在jsp中定義一個(gè)通用方法時(shí),就需要借助<%!%>的力量了,假設(shè)我們需要一個(gè)方法,根據(jù)用戶的性別顯示不同的html內(nèi)容,如果sex = 0就輸出紅色的“男”,如果sex = 1就輸出綠色的“女”。為實(shí)現(xiàn)這個(gè)功能,我們可以定義一個(gè)sexRenderer()方法。
14-04/index.jsp頁面顯示效果如下:
index.jsp中的代碼分兩部分。
第一部分定義sexRenderer()方法和
<%!public String sexRenderer(int sex) {if (sex == 0) {return "<span style='color:red;'>男</span>";} else if (sex == 1) {return "<span style='color:green;'>女</span>";} else {return "";}}%>
第二部分循環(huán)顯示保存了性別信息的數(shù)組,顯示的時(shí)候?qū)?huì)調(diào)用sexRenderer()方法。
<%int[] people = {0, 1, 1, 0};for (int i = 0; i < this.people.length; i++) {%><tr><td><%=this.sexRenderer(this.people[i])%></td></tr><%}%>
好的,現(xiàn)在我們知道可以在<%!%>中定義方法和變量了。但是同時(shí)也要了解的是<%!%>已經(jīng)脫離了service()方法,這就導(dǎo)致不能在它里邊使用request,response這些默認(rèn)變量了,如果想要調(diào)用request只能寫成void doSomething(HttpServletRequest request)的形式了,稍微注意一下即可。
分別是request, response, out, pageContext, session, application, page, config, exception。
讓我們看看它們與servlet中變量的對應(yīng)關(guān)系。
首先要明確的是,這九個(gè)變量都只在<%%>中有效,<%!%>中是無法調(diào)用這九個(gè)對象的。實(shí)際上<%%>最后會(huì)成為service()方法中的代碼,我們這里就看看如何在service()方法中獲得這些對象吧。
request
public void service(ServletRequest req, ServletResponse res) { HttpServletRequest request = (HttpServletRequest) req; }
jsp中的request就是service()中傳入的req參數(shù),因?yàn)閟ervice中定義的是ServletRequest類型,我們還需要轉(zhuǎn)換成HttpServletRequest類型。
response
public void service(ServletRequest req, ServletResponse res) { HttpServletResponse response = (HttpServletResponse) res; }
與上例相同,response也是service()中傳入的res參數(shù)。
out
Writer out = response.getWriter();
out對應(yīng)著從response中取出的writer對象,負(fù)責(zé)向響應(yīng)中輸出數(shù)據(jù)。不過jsp和servlet中的out還是有一點(diǎn)區(qū)別,雖然它們都實(shí)現(xiàn)了java.io.Writer接口,但servlet中實(shí)際類型是java.io.PrintWriter,而jsp中實(shí)際類型是javax.servlet.jsp.JspWriter。
pageContext
這是jsp獨(dú)有的,servlet里沒有page的概念。
session
HttpSession session = request.getSession();
直接從request中獲得會(huì)話。
application
ServletConext application = getServletConfig().getServletContext();
可以通過servletConfig獲得ServletContext,這是整個(gè)web應(yīng)用共享的一個(gè)對象。
page
Object page = this;
page就代表當(dāng)前jsp對象,也可以直接使用this引用。
config
ServletConfig config = getServletConfig();
這是在servlet初始化時(shí)由服務(wù)器傳入的對象,可以通過它獲得web.xml中定義的初始化參數(shù)。
exception
想在jsp中使用這個(gè)對象需要滿足一些條件了。
首先我們要在14-05/index.jsp中故意拋出一個(gè)異常。
<%@ page contentType="text/html; charset=gb2312" errorPage="error.jsp"%> <% String str = null; str.length(); %>
str值是null,直接在null上調(diào)用length()方法會(huì)引發(fā)NullPointerException,然后我們可以看到頁面第一行使用jsp指令(directive)設(shè)置了errorPage="error.jsp",這樣在出現(xiàn)異常的時(shí)候就會(huì)自動(dòng)forward到error.jsp中?,F(xiàn)在看看error.jsp中有些什么。
<%@ page contentType="text/html; charset=gb2312" isErrorPage="true"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=gb2312" /> <title>index</title> </head> <body> <%=exception%> </body> </html>
最主要的是在jsp指令(directive)中設(shè)置isErrorPage="true",這樣我們就可以在jsp中使用exception對象了,實(shí)際上這個(gè)異常是從request中取出來的。
到此為止,jsp九大默認(rèn)對象已經(jīng)講解完畢,其中常用的還是四個(gè)作用域?qū)?yīng)的對象,其他的了解即可。