Servlet - 生命週期

Servlet的生命週期或許乍看之下不是那麼重要,不過當我們需要精進使用能力的時候就會需要瞭解運作原理,而生命週期是非常重要的一環。在這一篇我會介紹Servlet的啟動流程和會經歷的一些方法,方便我們在不同階段執行我們需要的動作。

前言

Servlet的生命週期或許乍看之下不是那麼重要,不過當我們需要精進使用能力的時候就會需要瞭解運作原理,而生命週期是非常重要的一環。在這一篇我會介紹Servlet的啟動流程和會經歷的一些方法,方便我們在不同階段執行我們需要的動作。

Servlet啟動流程

基本上Servlet只是一個普通的Java Class。造成他和別的Class不同的是,它是繼承一個有實作Servlet API的Class。那我們提到過,能夠符合Servlet API Class 的運行環境叫做Container(例如Tomcat),而Container就是負責manage我們的Servlet object,包括運行,memory處理等等,對於Servlet object來說, Container就是他的Runtime。

那Container是什麼時候把某一個Servlet創建起來呢?我們都知道Java和C#一樣,是manage code,也就是程式碼是被編譯成為一個中繼語言,等到實際執行的時候,才有runtime把它實際跑起來。

那回到原來的問題,什麼時候Container會把某一個Servlet執行起來呢?為了保持Container的效能,因此預設Servlet是不被加載的。

當有request進來的時候,如果那個Servlet沒有被載入過,這個時候會把他載入。這也是為什麼第一次執行一個沒有人執行過的Servlet的時候,速度會很慢。

當Servlet有載入以後,每當有新的request進來同一個Servlet,就會用多執行緒的方式在執行那個Servlet。

因此,被載入過的Servlet會一直存在於Container直到Container服務重啟。

上面提到的Servlet表示每一個Servlet Class,因此如果我有兩個Servlet 1 和 Servlet 2,那麼他們的啟動流程都如上述那樣。

看完上面的宏觀的介紹啟動流程,我們有幾個地方會去探討:

  1. 有沒有辦法把Servlet預設在Container啟動就載入,達到第一次呼叫速度就很快?
  2. 當一個request進來的時候,那些方法會被呼叫?和他們的順序
  3. 既然每一個request呼叫同一個Servlet都是得到同一個servlet只不過是以多執行緒的方式來執行,是否會有問題?

啟動Container的時候就加載Servlet

我們有兩種方式可以設定,一種是在定義Servlet的web.xml裡面定義,一種則是使用Annotation的方式定義。

web.xml定義:

    
	TestServlet    
	com.sample.servlet.TestServlet  
	      
		name          
		alan  
	
	0

Annoation 定義:

@WebServlet(
		urlPatterns = { "/TestServlet" }, 
		initParams = { 
				@WebInitParam(name = "name", value = "alan")
		}, 
		loadOnStartup = 0)

load on startup 不需要多解釋,不過那個數字是什麼呢?基本上 0 <={數字} 表示啟動Container以後要啟動Servlet的順序。數字越小,啟動的順序越高。例如,如果有兩個Servlet,一個是2一個是1,那麼Servlet 1會先被載入,然後才是2

0 > {數字}表示啟動不載入(預設load on start up 是 -1)。

Servlet啟動執行的幾個方法

接下來我們介紹以下Servlet啟動以後會依照什麼順序來執行什麼方法。

基本上流程如下:

  1. Container 載入 Servlet
  2. Servlet呼叫 init() - 只會呼叫一次
  3. Servlet載入完成,處於等待狀態
  4. 當有Request進來的時候,service() 會被呼叫。service()會判斷進來的是什麼request method,然後傳給對應的method
  5. 如果今天是一個get,那麼service()會傳給doGet()
  6. doGet()完成以後,回到等待狀態
  7. 當Container重啟服務(或結束服務),呼叫destroy() - 只會呼叫一次
如果想要測試上述內容,可以創建一個Servlet,然後override上面提到的幾個method,然後寫一個log就可以看到他們執行的順序。

從上面我們可以看出init()和destroy()只會被呼叫一次,而不管是doGet()或者doPost(),其實都是會從service()轉過來的。

這個告訴我們,假設今天你有設定一個Init-Param是每一次request都會用到的,不妨在Servlet定義一個全域參數,然後在init()的時候從Init-Param裡面讀取出來放入參數(因為實際上只有創建一個Servlet object,因此每一個request都能用),這樣每一次doGet()就都不需要在去xml裡面把值讀出來,提升效能(不過相對的,會比較吃記憶體)。

在 Java EE 5 以後有支援兩個Annotation叫做@PostConstruct@PreDestroy。這兩個可以標記在一個非static、返回void 的method上面。而他們兩個的流程一個會在init()前面(在constructor 後面)和在destory()前面,也是只會執行一次。

Servlet多執行緒需要注意的地方

我們提到,其實Servlet只有一個會被執行起來,而多個request到同一個Servlet使用多執行緒在執行的,那熟悉執行緒的第一個問題是,這個執行緒是不是Thread safe?而答案是,Servlet 不是Thread safe。因此我們在使用上面需要特別小心。

舉個例子,假設我們今天的Servlet作用是有一個全域變數,這個變數會儲存使用者傳入的名字,並且列出這個人的名字在頁面。假設,在把名字(稱為A)assign 到那個全域變數以後,沒有直接輸出名字,而是做了別的可能很耗時間的動作,那麼假設另外一個人(稱為B)也呼叫了這個Servlet並且在A之前把名字列印出來了,那麼當A做完很耗時間的動作,到列印名字的時候,因為B已經做完了,所以目前全域變數的值是B的,所以A列印出來就變成不對的了。

上面的敘述有點常,我們其實可以用一個小實驗就可以看出問題。

public class HelloWorld extends HttpServlet {
	private static final long serialVersionUID = 1L;

	private String globalName; //我們的全域變數
....

protected void doPost(HttpServletRequest request, HttpServletResponse response)
 throws ServletException, IOException {	

	String name = request.getParameter("name");
		
	globalName = name;
		
	if(globalName.equals("A"))
	{
		try {
			Thread.sleep(10000); //模擬假設是A進來處理東西比較耗時
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
.....//下面就是把globalName列印在畫面

這個程式碼看似沒有問題,不過,假設你用兩個瀏覽器模擬連個request,一個輸入A(會進入Thread sleep 10 秒),一個輸入B。A先執行,然後在執行B。不過因為B會先完成,所以當A完成的時候globalName已經被改成B了,所以列出來的名字會是B。

這就是not Thread Safe的問題。

not Thread safe需要注意的

這個告訴我們需要注意兩件事情:

  1. 少用全域變數 - 如果要用,最好把它當做唯獨的就好,不要修改值。
  2. 如果數值會變動,最好使用區域變數避免奇怪問題發生。

只要牽扯到Thread的bug大部份都是不好重現的,因此需要特別注意。

結語

這篇介紹了Servlet的生命週期,和我們在使用上可以用那些method來在流程不同階段使用。最後介紹了Servlet需要特別注意的地方。

在下一篇,我會介紹Servlet跳轉的機制。


Google+

創用 CC 授權條款
Alan Tsai 的隨手筆記Alan Tsai製作,以創用CC 姓名標示 4.0 國際 授權條款釋出。