跳到主要內容

JavaEE 請求與回應處理

updated:2013/05

container載入類別後執行Servlet的建構式
建構式不應自行撰寫,使用預設即可
建構完成之後container會先執行Servlet的 init()
假使有程式碼(資料庫連結之類的)需要初始化,可覆寫 init() 達成需求

初始化完成後Servlet執行著 service() 來處理 Client 端請求
service() 依據請求類別自行呼叫相對應的method,不用也不需要修改
我們只需要覆寫 doGet() 或 doPost() 這類常見的請求類別讓service()調用即可
最後要結束 Servlet 時,container會呼叫Servlet的 destroy()
destroy() 讓我們能夠在Servlet結束前能做一些動作,並結束Servlet的存在

Servlet初始化完成後,當 Client 端的請求進來時,container會配置一個執行緒
執行緒會再呼叫該Servlet的service(),並將它建立在自己的stack上
其他方法如 doGet() 等會再堆疊於其上
如果有另外的請求會再建立其它的執行緒,記憶體的配置也會放在該執行緒的stack上
回應物件與請求物件也會依照每個執行緒配置一組的方式建立
注意,每個JVM只會為單一Servlet建立一個實例,處理多個請求時建立的是多執行緒

Servlet類別的繼承上有相當多的介面或類別,但最常被使用的還是HttpServlet
非 HTTP 通訊協定則必須使用其他類別,但這是非常少見的情形
HttpServlet裡面除了 doGet() 與 doPost() 外也會有其他的 HTTP 方法,但一樣很少被使用

doGet() - 透過請求URL向Server要求資源或檔案
doPost() - 除了GET的功能外,還能處理額外的資訊,但必須經由表單傳送請求
doOptions() - 要求請求的URL上的資源列出可回應的 HTTP 方法
doHead() - 如同GET一般,但是只回傳 header 的資訊
doTrace() - 能讓客戶端看到伺服器所接到的訊息,測試或偵錯用
doPut() - 要求將主體資訊寫明在請求URL上
doDelete() - 要求刪除請求URL上的資源或檔案

doGet() 與 doPost() 除了資料量大小有差異外,最大的考量是安全性
GET請求會將需要的參數附加在請求URL後面,能讓使用者設成書籤
POST會將資訊寫在資訊主體(payload)裡,而不會將參數寫在請求URL上
安全上來說當然是POST比較安全,而且資料長度也不像GET會受限於URL的長度限制
以原始設計考量來使用這兩者會是另一個考量點,關係到等冪性(idempotent)

等冪性指的是可以重覆做同樣的一件事而不會有副作用
例如GET原始設計考量是用來作為查詢用,也是等冪的規格
POST則是將資料提供給Server作改變的處理,在這層意思上不是等冪的
每次POST送出一筆資料很可能就是一次不可逆的處理
如果硬要寫出與原始考量不相同的處理也可以做到
例如用 doGet() 造成不可逆的處理
但是不符合規範的寫法還是需要盡量避免,依照需求選擇要覆寫的方法即可

Servlet中要存取請求所送過來的參數的寫法如下:
String demo = request.getParameter("demo");      //取得參數demo的值

getParameter()有個小技巧注意一下,如果request沒有這個參數則會回傳null
所以若要比較取得的參數時,寫成 "test string".equals( demo) 的形式會比較好
這樣比 if(demo != null &&demo.equals("test string") ) 來的精簡及有效率


存取具有多個值的單一參數(如:Checkbox、List等)寫法:
String demo = request.getParameterValues("test")[0];
String[] demos = request.getParameterValues("test");

若想取得所有參數的名稱可用getParameterNames():
Enumeration<String> e = request.getParameterNames();
while(e.hasMoreElements()){
        String param = e.nextElement();
         ...
}

getParameterMap()則會將請求參數以Map物件形式回傳
Key是請求參數的名稱,Value則是其值(因複數值考量為String[]型別)

實務上常遇到參數的編碼問題,可分為POST和GET兩種情況

POST-
如果沒在Content-Type標頭中設定字元編碼(如網頁的contentType="text/html; charset=UTF-8")
此時HttpServletRequest的getCharacterEncoding()傳回值是null
container會使用其預設的ISO-8859-1去解讀參數(大多數container和瀏覽器使用這編碼)
Servlet 取得非ASCII字元的請求參數便會是亂碼形式
在request取得任何參數值前使用setCharacterEncoding()指定編碼可處理這問題
取得參數後呼叫seCharacterEncoding()則沒有作用
request.seCharacterEncoding("UTF-8");
//等同要求container 執行String text = java.net.URLDecoder.decode("...text..", "UTF-8");

GET-
seCharacterEncoding()方法只針對POST
因為GET的參數編碼是在URL上,這裡是由HTTP Server負責實作
如Tomcat處理URL預設用ISO-8859-1
編碼的處理應該由String的getBytes()指定字串的編碼後讀取,再重新進行字串的編碼
String demo = request.getParameter("demo");
String demo = new String(demo.getBytes("ISO-8859-1"), "UTF-8");


HttpServletRequest 有許多可以取得 Servlet / JSP 路徑的method
getRequestURI()可以取得請求的完整路徑
getContextPath()可以取得請求對應的Web application的路徑
getServletPath()可以取得對應到Servlet的路徑


當然還有其他的資訊可以取得
//取得客戶端平台與瀏覽器資訊,另有類似parameter用法的getHeaders()、getHeaderNames()
String client = request.getHeader("User-Agent");  
Cookie[] cookies = request.getCookies();              //與請求相關聯的Cookie
HttpSession session = request.getSession();            //與客戶端相關聯的session
String usedMethod = request.getMethod();             //請求使用的HTTP方法(Post、Get等)
InputStream input = request.getInputStream();        //請求的輸入串流
String protocolString = request.getScheme();          //請求的protocol,如:https or http
String serverName = request.getServerName();      //請求對象的主網域

getHeader() 和 getIntHeader() 的差別在於如果header是整數時,可用後者省去轉換麻煩
getInputStream() 和 getReader() 這兩個方法所取得的串流都是不包含header的主體資訊
來個範例示範串流處理:
package tw.vencs;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class test extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
                         throws ServletException, IOException {
     
        response.setContentType("application/jar");
     
        ServletContext ctx = getServletContext();
        InputStream is = ctx.getResourceAsStream("/test.jar");
     
        int read = 0;
        byte[] bytes = new byte[1024];
     
        OutputStream os = response.getOutputStream();
        //每次讀取1byte的內容,當讀不到時(讀取結束)會回傳 -1
        while((read = is.read(bytes)) != -1){  
            os.write(bytes, 0, read);
        }
        os.flush();
        os.close();
    }
}

這個Servlet可以提供 JAR 檔下載,context會在後續文章提到
setContentType() 裡的application會告知瀏覽器這個連結並不是html頁面,而是應用程式檔
jar告知瀏覽器該使用jar檔方式來接收這個檔案
不同類型的檔案需要不同的設定,其他的MIME type需要時再查詢即可(如 image/png )
另一種做法是在在web.xml中設定副檔名與MIME type的關係,如:
<mime-mapping>
    <extension>pdf</extension>
    <mime-type>application/pdf</mime-type>
</mime-mapping>

HttpServletResponse除了本篇所示範的 getOutputStream() + write() 外
另外還有常見的 getWriter() + println() 組合
前者是針對位元組資料作輸出,後者是專門針對字元資料作輸出
實際上傳輸檔案只要將連結指向檔案即可,這個範例只是用Servlet來實現它

回應物件中還可以針對header作設定,如下範例:
.setHeader("var1", "yes");    //如果有此header存在,會覆寫其值;否則會增加新的標頭與值
.addHeader("var1", "no");    //如果有此header存在,會添加其值;否則會增加新的標頭與值
如果header值是整數或日期,也可用setIntHeader()、addIntHeader()、setDateHeader()、getDate...

而回應物件也有對應設定META的方法

response.setLocale(Locale.TAIWAN);
對應Content-Language設為zh-TW,而編碼方式預設使用BIG5,當然現在不推薦使用BIG5編碼就是了
在web.xml裡也有對應的配置
<locale-encoding-mapping-list>
    <locale-encoding-mapping>
        <locale>zh-TW</locale>
        <encoding>UTF-8</encoding>
    </locale-encoding-mapping>
</locale-encoding-mapping-list>

上面的配置除了指定國家外,也將頁面的編碼改為通用的UTF-8
不過如果區域META的配置需求不高,直接設定編碼META即可:response.setCharacterEncoding("UTF-8");
另一種做法 response.setContentType("text/html;charset=UTF-8"); 也有相同作用
不過注意設定編碼META後會讓Locale Meta失效

假如請求的過程不順利,例如前面示範的jar檔傳輸發生找不到檔案的情況
如果要使用Server預設的錯誤訊息,則可以使用response.sendError();
response.sendError(HttpServletResponse.SC_NOT_FOUND); 會將頁面導向Server預設的404錯誤訊息
要輸出自訂訊息則可以改用sendError()的另一個版本
response.sendError(HttpServletResponse.SC_NOT_FOUND, "oh!找不到檔案!");
其餘的狀態代碼則可以查詢JavaEE api


最後提到頁面處理流程
透過重導與請求分派的機制,可以將後續的處理交給其他頁面或Servlet繼續執行
重導與請求分派很類似,差別點在於重導的作用在 client 端的瀏覽器上,請求分派則發生在 server內部
所以進行重導會看到URL被更改,請求分派則不會對瀏覽器顯示的網址做任何變動
也因為流程的不一致也影響 request、response物件的使用
重導的過程是產生一個新的request物件,請求分派從頭到尾都使用相同的reques、response物件
如果要區分用途,則後續頁面跟當前頁面關聯不高的話建議使用重導,反之則用請求分派

重導須要以HttpServletResponse達成,寫法如下:
response.sendRedirect("URL");     //接收的參數是String型別,非URL型別物件

Servlet/JSP 的URL使用直接的URL或是相對的URL皆可
只是使用相對的URL要注意有使用 "/" 與不使用 "/" 的差別
如果目標網址是http://www.demo.tw/test/test.do
使用 "show/1.jpg" 作為相對URL時會連結到 http://www.demo.tw/test/show/1.jpg
使用 "/show/1.jpg" 卻是連結到 http://www.demo.tw/show/1.jpg,相對於 Web container 的位置
重導需要在回應輸出HTML之前被呼叫,當回應已經被送出後則會無法再重導

重導要做到傳輸資料,可以利用URL夾帶參數,如:page?paramA=a&paramB=b
另一個方式是透過session的屬性設定傳輸,關於session請參照JSP & Servlet Session、Cookie處理

請求分派可以用 RequestDispatcher 進行處理,它的取得方式如下:
RequestDispatcher dispatcher = request.getRequestDispatcher("URL");  //參數為指定的頁面URL

RequestDispatcher有兩種方法較常使用,分別是 include()、forward()
使用時必須傳入request和response物件,如:dispatcher.include(req, resp);
include()是在目前頁面的處理流程中插入一段由其他頁面進行的處理
forward()則是將處理轉交給其他Servlet/JSP

被include的Servlet中除了getSession()取得的HttpSession物件外
其他針對 request header的設定都會被忽略
要使用forward()的話,則要目前的頁面沒有任何HTML輸出
如果有存在緩衝區裡的串流資料,則資料會被忽略
如果已經有回應確認後卻還是呼叫forward(),頁面會都出IllegalStateException
不管是include()或forward()的頁面,request會在屬性裡儲存上一個Servlet的資訊,如:
javax.servlet.include.request_uri      //forward版本就是中間的include改成forward
javax.servlet.include.context_path
javax.servlet.include.servlet_path
javax.servlet.include.path_info
javax.servlet.include.query_string

out.println(request.getAttribute("javax.servlet.forward.query_string").toString());   //print query string

RequestDispatcher也能藉由URL夾帶參數
以RequestDispatcher為例, req.getRequestDispatcher("page?paramA=a&paramB=b").include(req, resp);
被引入的頁面中就能藉由 getParameter() 使用帶進來的參數
而在像這種傳遞request和response物件的處理方式中
當然RequestDispatcher因為不會改變網址列,所以夾帶的參數也不會外洩

另外因請求分派的過程都使用相同的請求、回應物件,也可以用屬性設定的方式傳遞
屬性傳遞的優勢在於可以直接傳遞物件,如:request.setAttribute("atr1", obj);
取出時則用 type obj = (type)request.getAttribute("atr1");
只是要注意所有的設定、屬性都只存在於這個request物件,本次請求結束後會跟著一起消失
與屬性有關的方法有setAttribute()、getAttribute()、removeAttribute()、getAttributeNames()
removeAttribute()會移除指定名稱的屬性,getAttributeNames()則會取得所有的屬性名稱

留言