前言
Servlet是一個java編寫的程序,此程序是在服務器端運行的,是按照Servlet規范編寫的一個
java類。Servlet是處理客戶端的請求,並將處理結果以響應的方式返回給客戶端。Servlet框架
是怎樣的呢?它的生命周期又是什麼情況呢?這是本文需要探求的。
Servlet框架
網上下載Servlet源碼,解壓之後發現其由兩個包組成:
1、javax.servlet
2、javax.servlet.http
javax.servlet
此包中定義了所有Servlet類都必須實現的接口或類。
接口定義:
ServletConfig接口---在初始化過程中由Servlet容器(Tomcat調用)
ServletContext接口---定義Servlet用於獲取容器信息的方法
ServletRequest接口---向服務器請求信息
ServletResponse接口 ---響應客戶端請求
Servlet接口---定義所有的Servlet必須實現的方法
類定義:
ServletInputStream類 --- 用於從客戶端讀取二進制數據
ServletOutputStream類 ---用於將二進制數據寫入到客戶端
GenricServlet--- 抽象類,定義一個通用的,獨立於底層協議的servlet。
java.servlet.http
此包中定義了使用HTTP通信協議的所有Servlet類應該實現的類、接口。
接口定義:
HttpServletRequest接口 --- 封裝http請求
HttpServletResponse接口 --- 封裝http響應
HttpSession接口 --- 用於表示客戶端存儲有關客戶的信息
HttpSessionAttributeListener接口---實現這個監聽接口,當用戶獲取Session的屬性列表發生
改變的時候得到通知。
類的定義:
HttpServlet類 --- 擴展了GenericServlet的抽象類
Cookie類 --- 創建一個Cookie,Cookie技術,用戶存儲服務器發送給客戶端的信息。
通過閱讀Servlet框架源碼,其主要的框架結構如下圖:
Servlet工作過程
通過上述Servlet框架的了解我們可以初步描述一下Servlet在Tomcat容器中是如何工作的。
來看下面的時序圖:
1、Web Client 向Servlet容器(Tomcat)發出Http請求
2、Servlet容器接收Web Client的請求
3、Servlet容器創建一個HttpRequest對象,將Web Client請求的信息封裝到這個對象中
4、Servlet容器創建一個HttpResponse對象
5、Servlet容器調用HttpServlet對象的service方法,把HttpRequest對象與HttpResponse
對象作為參數傳給 HttpServlet對象
6、HttpServlet調用HttpRequest對象的有關方法,獲取Http請求信息
7、HttpServlet調用HttpResponse對象的有關方法,生成響應數據
8、Servlet容器把HttpServlet的響應結果傳給Web Client
Tomcat和HttpServlet是如何進行交互的呢?從源碼中我們可以得到
Servlet生命周期
在Servlet框架中所有的Servlet類都必須實現Servlet這個接口。其中定義了三個方法:
1、init方法:負責初始化Servlet對象。
2、service方法:用於響應客戶端的請求
3、destroy:銷毀Servlet對象,釋放占用的資源。
Servlet生命周期四個階段:
● 加載階段:加載並實例化(創建Servlet實例)
● 初始化階段:調用init()方法
● 響應客戶請求階段:調用service()方法,doGet、doPost
● 終止階段:調用destroy()方法
加載階段
Tomcat從文件系統,遠程文件系統或其他網絡服務中通過類加載器來加載Servlet,並調用
Servlet的默認構造方法(不帶參構造器)
初始化階段init()方法
當Servlet容器啟動時:讀取web.xml配置文件中的信息,構造指定的Servlet對象,根據配置
文件的信息創建ServletConfig對象,並將其作為參數傳遞給init方法進行調用。
Tomcat啟動後:用戶首次想某個Servlet對象發送請求,Tomcat會判斷內存中是否存在指定的
servlet對象,如果沒有則會去創建它,然後創建HttpRequest,HttpResponse對象,調用service
方法處理用戶的請求。
從Servlet的構造開始我們沒有顯示的看到init()方法的調用,那麼init方法到底是何時進行調用
的呢?閱讀源碼可以知道:init方法是在實例化Servlet之後調用的,其參數ServletConfig是在
Servlet初始化階段Tomcat根據web.xml配置信息,和操作系統的相關環境生成並傳遞給init
方法的。
[java] * Called by the servlet container to indicate to a servlet that the
* servlet is being placed into service.
*
* <p>The servlet container calls the <code>init</code>
* method exactly once after instantiating the servlet.
* The <code>init</code> method must complete successfully
* before the servlet can receive any requests.
*
* <p>The servlet container cannot place the servlet into service
* if the <code>init</code> method
* <ol>
* <li>Throws a <code>ServletException</code>
* <li>Does not return within a time period defined by the Web server
* Called by the servlet container to indicate to a servlet that the
* servlet is being placed into service.
*
* <p>The servlet container calls the <code>init</code>
* method exactly once after instantiating the servlet.
* The <code>init</code> method must complete successfully
* before the servlet can receive any requests.
*
* <p>The servlet container cannot place the servlet into service
* if the <code>init</code> method
* <ol>
* <li>Throws a <code>ServletException</code>
* <li>Does not return within a time period defined by the Web server
[java] **
*
* A servlet configuration object used by a servlet container
* to pass information to a servlet during initialization.
*
*/
public interface ServletConfig {
/**
*
* A servlet configuration object used by a servlet container
* to pass information to a servlet during initialization.
*
*/
public interface ServletConfig {
響應客戶請求階段service方法
service()方法是在客戶端第一次訪問servlet時執行的,其實init方法同樣也是在有客戶端訪問
servlet的時候才被調用。不過需要特別注意的是討論init方法在session級別上時,當存在不同的
會話訪問相同的servlet時,Tomcat會開啟一個線程處理這個新的會話,但是此時Tomcat容器
不會實例化這個servlet對象,也就是有多個線程在共享這個servlet實例。換句話說Servlet對象在
servlet容器中是以單例的形式存在的!然而查看其源碼可以發現,Servlet在多線程下並未使用同
步機制,因此,在並發編程下servlet是線程不安全的。
對於Servlet的並發,線程安全的處理問題,筆者會找個時間好好的整理下思路。
對於不同的session訪問相同的serlvet對象,只有一次init的過程,筆者會在接下來予以演示。
閱讀HttpServlet的源碼可以知道,基於Http通信協議的HttpServlet在進行客戶端響應處理的
時候根據客戶端請求,響應的類別不同分別調用不同的方法,其中最常用的就是doGet、doPost
方法,這兩個方法是我們在編寫Servlet中的主要的邏輯處理階段。
[java] 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) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
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 {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
//
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);
}
}
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) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
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 {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
//
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);
}
}
終止階段:destroy()方法的調用
上面的探討中知道的了Servlet是如何加載、初始化、處理客戶端的請求響應的,那麼Servlet
在什麼時候終止呢?其生命周期又是在什麼時候結束的呢?
我們知道的是Servlet生命周期是有Tomcat容器來管理的,由此在Tomcat關閉、或者Restart
的時候,servlet的生命周期必然結束,destroy方法也必然被調用過。在客戶端與服務器的一次
Session會話中,session關閉之後servlet並未銷毀。後續演示。
總的來說servlet對象什麼時候destroy的呢?
1、Tomcat服務器stop
2、web項目reload
3、Tomcat容器所在的服務器shutdown(這不廢話嗎?)
更正之處:對於destroy()方法筆者略有疑惑,“它到底是如何銷毀Servlet的呢?”,基於這個問題
特意的去查看了源碼,結果發現destroy()方法在Servlet框架中並未具體去實現。它是由Coder
自己去實現的。因此“destroy()方法用戶銷毀Servlet”這種說法本身就是離譜的!查閱源碼:
[java]
/**
* Called by the servlet container to indicate to a servlet that the
* servlet is being taken out of service. See {@link Servlet#destroy}.
*
*
*/
/**
*
* Called by the servlet container to indicate to a servlet that the
* servlet is being taken out of service. This method is
* only called once all threads within the servlet's
* <code>service</code> method have exited or after a timeout
* period has passed. After the servlet container calls this
* method, it will not call the <code>service</code> method again
* on this servlet.
*
* <p>This method gives the servlet an opportunity
* to clean up any resources that are being held (for example, memory,
* file handles, threads) and make sure that any persistent state is
* synchronized with the servlet's current state in memory.
*
*/
public void destroy();
}
/**
* Called by the servlet container to indicate to a servlet that the
* servlet is being taken out of service. See {@link Servlet#destroy}.
*
*
*/
/**
*
* Called by the servlet container to indicate to a servlet that the
* servlet is being taken out of service. This method is
* only called once all threads within the servlet's
* <code>service</code> method have exited or after a timeout
* period has passed. After the servlet container calls this
* method, it will not call the <code>service</code> method again
* on this servlet.
*
* <p>This method gives the servlet an opportunity
* to clean up any resources that are being held (for example, memory,
* file handles, threads) and make sure that any persistent state is
* synchronized with the servlet's current state in memory.
*
*/
public void destroy();
}
閱讀上述注釋,很明白的是destroy的調用是表明Servlet結束其servcie階段,destroy方法的調用
實際是在servlet銷毀之前,由Tomcat來調用的,其作用是清理一些資源的占用情況,例如文件、
線程,而且確保任何持久的狀態和servlet的當前狀態在內存中是同步的。
不過destroy的調用情況上述的總結是正確的。
也就是說Servlet的銷毀時destroy()一定會被掉用,servlet方法基本是由Tomcat回調的!
但是destroy()方法的調用只是回收一些資源,並不意味著Servlet已經銷毀。至於何時銷毀,這個
筆者也不太明了,希望有人可以指出,不過據源碼是Servlet結束servcie服務時銷毀,Tomcat關閉
時也會銷毀。(皮之不存毛將安附焉?)
簡單的測試下:
我們在doPost()方法裡面簡單的調用下destory方法,run項目,之後另起一個Session訪問
輸出情況:
這就說明了destroy()和Servlet的銷毀不存在必然聯系,只是在Servlet銷毀之前,destroy方法,會
基由Tomcat回調,進行一些資源的清理,文件關閉。
Servlet生命周期演示
為了更進一步的了解Servlet生命周期(它確實十分重要),筆者新建一個簡單的web項目予以說明
不當之處請指正,一起交流。
Eclipse配合Tomcat如何新建一個web項目,這個筆者就不必多說了吧,挺簡單的,網上的總結
各式各樣的也不少。不過需要注意的是高版本的Eclipse若果讀者不注意的話產生的web項目是沒有
web.xml文件的,以注解的形式代替了。
runtime選擇自己配置好的Tomcat容器,web Module version選擇2.5就可以了,不要選擇3.0
這裡筆者貼出項目中的一些歌主要的文件。
web.xml配置文件。
[html] <?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>Servlet02</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<servlet>
<description></description>
<display-name>HelloServlet</display-name>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>com.kiritor.servlet.HelloServlet</servlet-class>
<init-param>
<description></description>
<param-name>info</param-name>
<param-value>this is a init message</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/HelloServlet</url-pattern>
</servlet-mapping>
</web-app>
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>Servlet02</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<servlet>
<description></description>
<display-name>HelloServlet</display-name>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>com.kiritor.servlet.HelloServlet</servlet-class>
<init-param>
<description></description>
<param-name>info</param-name>
<param-value>this is a init message</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/HelloServlet</url-pattern>
</servlet-mapping>
</web-app> 注意servlet映射的配置,以及初始化參數的配置。
自定義Servlet的代碼:
[java] package com.kiritor.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class HelloServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* Default constructor.
*/
public HelloServlet() {
super();
}
@Override
public void init(ServletConfig config) throws ServletException {
// TODO Auto-generated method stub
super.init(config);
System.out.println("init方法被執行");
System.out.println("相關的初始化參數:");
System.out.println(config.getInitParameter("info"));
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
PrintWriter printWriter = response.getWriter();
printWriter.write("Hello world");
}
@Override
protected void service(HttpServletRequest arg0, HttpServletResponse arg1)
throws ServletException, IOException {
super.service(arg0, arg1);
System.out.println("service方法被執行");
}
@Override
public void destroy() {
super.destroy();
System.out.println("destroy方法被執行");
}
}
package com.kiritor.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class HelloServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* Default constructor.
*/
public HelloServlet() {
super();
}
@Override
public void init(ServletConfig config) throws ServletException {
// TODO Auto-generated method stub
super.init(config);
System.out.println("init方法被執行");
System.out.println("相關的初始化參數:");
System.out.println(config.getInitParameter("info"));
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
PrintWriter printWriter = response.getWriter();
printWriter.write("Hello world");
}
@Override
protected void service(HttpServletRequest arg0, HttpServletResponse arg1)
throws ServletException, IOException {
super.service(arg0, arg1);
System.out.println("service方法被執行");
}
@Override
public void destroy() {
super.destroy();
System.out.println("destroy方法被執行");
}
}
接下來我們直接run該項目。看看後台輸出結果,現在貌似可以直接在Eclispe控制台查看
輸出信息了,十分方便。
init方法是在servlet實例化後由Tomcat容器進行調用的,生成了諸多信息,其中包含我們自己
定義的Servlet配置信息,在init方法中我們通過ServletConfig對象獲取到了。
之後service方法執行了。這裡新開啟一個session會話,看看情況。圖我就不貼了,控制台
多輸出一句service被執行。證明了servlet在容器中實例單例的形式存在。源碼層面上Servlet不是
單例的,只是由於容器對其的維護,使之產生了類似單例的效果。
接下來我們看destroy方法的調用情況,只是針對上述兩種情況來說的,不可能筆者還去關機‘
演示。首先我們關閉Tomcat服務器