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,那麼他們的啟動流程都如上述那樣。
看完上面的宏觀的介紹啟動流程,我們有幾個地方會去探討:
- 有沒有辦法把Servlet預設在Container啟動就載入,達到第一次呼叫速度就很快?
- 當一個request進來的時候,那些方法會被呼叫?和他們的順序
- 既然每一個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啟動以後會依照什麼順序來執行什麼方法。
基本上流程如下:
- Container 載入 Servlet
- Servlet呼叫 init() - 只會呼叫一次
- Servlet載入完成,處於等待狀態
- 當有Request進來的時候,service() 會被呼叫。service()會判斷進來的是什麼request method,然後傳給對應的method
- 如果今天是一個get,那麼service()會傳給doGet()
- doGet()完成以後,回到等待狀態
- 當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需要注意的
這個告訴我們需要注意兩件事情:
- 少用全域變數 - 如果要用,最好把它當做唯獨的就好,不要修改值。
- 如果數值會變動,最好使用區域變數避免奇怪問題發生。
只要牽扯到Thread的bug大部份都是不好重現的,因此需要特別注意。
結語
這篇介紹了Servlet的生命週期,和我們在使用上可以用那些method來在流程不同階段使用。最後介紹了Servlet需要特別注意的地方。
在下一篇,我會介紹Servlet跳轉的機制。