在上一文中,我們詳細的介紹了HttpServletRequest對象,並且重點的介紹一個Http請求中包含請求行、請求頭、請求體三部分,並且講解了如何通過HttpServletRequest中封裝的方法來方便的獲取對應的頭信息及用戶的請求參數。

在上文中我們也講到了Servlet的功能和重要性,其在MVC架構中充當Controller的角色,因此其不僅要獲取客戶端的數據(用戶輸入的表單數據、查詢參數等),並在處理結束后,給客戶端一個響應,也正如我們上圖中所示,Servlet需要對客戶端的HTTP請求進行一個HTTP響應。而Http的響應正是由本文中的主角HttpServletResponse來完成的,下面就讓我們一起學習如何對一個Http請求作出正確(適當的,處理成功或失敗、無權限、參數錯誤等)的響應。其局部(省去了Servlet容器)的執行過程如下圖所示:

1.設置響應的狀態碼
這里我們通過一個截圖來看下什么是響應的狀態碼,滴、滴、滴(😅,用圖就用全套,此部分圖片來自上篇文章),下圖最大的紅框中的綠色小燈泡旁的200字樣,就是我們這里說的狀態碼了,綠燈表示其為響應成功。

有些同學可能會比較疑惑了,我在開發的過程中並沒有設置response的狀態碼為200呀,怎么這里的200是哪來的?這里需要給大家說一下了,在我們程序正常執行(完整的處理了Http請求,沒發生bug)的時候,Web服務器會默認產生一個狀態碼為200。還有,在我們url路徑輸錯的時返回的404錯誤,調用servelt發生異常直接拋出返回的500錯誤等,都是web服務器幫我們默認產生的。


在開發的過程中,500錯誤是我們遇到最多的錯誤了,如空指針異常、sql異常、狀態異常等導致的請求中斷,對於這些異常,如果直接將報錯信息暴露給用戶,那么我們的系統的體驗就會非常的差。為了增加系統的用戶友好程序,我們必須對異常進行處理,但是也需要將錯誤信息正確的提示給用戶,讓其可以有下一步處理或者聯系客服。
這樣我們就需要來設置狀態碼,讓前端可以根據狀態碼及其他返回信息進行相應的頁面處理了。那么問題來了,我們如何手動的設置狀態碼呢?HttpServletResponse中為我們提供了一下幾個方法:

其中比較重要的方法為setStatus(int sc)
,我們可以通過其方便的設置給客戶端響應的狀態碼;對於sendError()
的兩個方法,會將Servlet之前寫入緩沖區的數據全部清除,但是其也有較好的使用場景,就是對同一種異常設置專門的錯誤提示頁面,比如用戶未登錄,可以在過濾器(Filter)中判斷出此種情況,並直接調用sendError跳轉至相應的頁面,在此頁面上友好的向用戶提示錯誤信息,but此功能完全可以使用重定向來完成,且重定向可以獲取更多的請求和響應信息,因此算是一個比較雞肋的功能吧。
我們簡單的對setStatus、sendError進行測試,這里我們新建個Servelt,命名為ResponseTestServlet,其doGet代碼如下:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//設置返回客戶端的contentType
response.setContentType("text/html;charset=utf-8");
//設置狀態碼
response.setStatus(500);
//response.sendError(500);
//獲取輸出流
PrintWriter out = response.getWriter();
out.println("雖然我的狀態為500,但是信息正常輸出了");
}
我們直接在瀏覽器中調用ResponseTestServlet,其運行結果如下圖圖左所示:


當我們執行response.sendError(500)
時,我覺得大家應該都已經預料到結果頁面了,其結果如上圖圖右所示。那我們應該如何設置錯誤碼對應的錯誤頁面呢?這里我們需要在web.xml中增加如下配置,需要注意的是,配置的路徑必須以’/'開頭,即必須是絕對路徑===打包發布時的路徑(可參考此文):
<error-page>
<error-code>500</error-code>
<location>/index.jsp</location>
</error-page>
在此在瀏覽器上執行,其運行結果如下圖所示(偷個懶,沒有寫錯誤顯示頁面😅):

如何根據錯誤信息,設置合理的狀態碼呢?這就需要我們知道每個狀態碼表示的含義。Http錯誤碼總共分為5類,即1xx、2xx、3xx、4xx、5xx,分別表示通知信息、成功信息、重定向信息、客戶端錯誤、服務端錯誤,下面列舉一些常見的錯誤碼:
Name | discribtion | 釋義 |
---|---|---|
200 | SC_OK | 此次請求已經成功 |
301 | SC_MOVED_PERMANENTLY | 請求的網頁已永久移動到新位置 |
302 | SC_MOVED_TEMPORARILY | 臨時移動、請求地址不變 |
401 | SC_UNAUTHORIZED | 未授權、用戶需登錄 |
403 | SC_FORBIDDEN | 服務器拒絕了此次請求(權限問題) |
404 | SC_NOT_FOUND | 服務器沒找到URI匹配的 |
405 | SC_METHOD_NOT_ALLOWED | 調用的方法不允許使用(get、post不匹配) |
500 | SC_INTERNAL_SERVER_ERROR | 服務器內部發生異常,請求中斷 |
502 | SC_BAD_GATEWAY | 網關錯誤(如Nginx),無法收到服務器的響應 |
504 | SC_GATEWAY_TIMEOUT | 請求超時,在約定時間內沒有收到Http響應 |
2.設置響應消息頭
在上文中,我們在chrom調試工具中查看了Http請求的請求頭、請求體,Chrome提供的信息不止於此,我們來看下圖,可以看到,Response的Headers信息。

這也說明了,相應於Request中的請求頭,Response也有對應的響應頭,這些響應頭主要如下圖所示:

當然,為了方便的設置響應頭中對應的信息,HttpServletResponse也提供了一系列的方法,主要相關方法如下:

需要注意的是,addHeader、addIntHeader、addDateHeader都有一個對應的setxxxx方法,兩者的區別就如同集合和列表,setxxxx方法不允許出現重復的header,而addxxxx方法可以;setContentType、setCharacterEncoding方法皆是是指返回給客戶端的內容的編碼方式的,推薦直接使用setContentType設置客戶端內容的MIME類型及編碼方式,比如setContentType("text/html; charset=UTF-8")
等價於setContentType("text/html");setCharacterEncoding("charset=UTF-8")
兩條語句同時執行。
這里,為了演示上面這些方法,我們將ResponseTestServlet中的doGet方法修改如下:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//設置返回客戶端的contentType
response.setContentType("text/html;charset=utf-8");
//設置狀態碼
//response.setStatus(500);
//response.sendError(500);
PrintWriter out = response.getWriter();
//out.println("雖然我的狀態為500,但是信息正常輸出了");
//添加類型為String的header
response.addHeader("Location", "#");
//添加類型為long的header
response.addDateHeader("Date", new Date().getTime());
//創建一個Cookie
Cookie cookie = new Cookie("name", "李子樹");
//添加一個cookie
response.addCookie(cookie);
}
其執行結果如下圖所示,我們添加的header都能看到:

3.發送響應消息體
前面說了這么多,到了這里才是真正的重頭戲!這里才是最直觀的響應給客戶看到的內容,在早期JSP還沒有誕生的時代,許多動態頁面是通過在Serlvet中使用HttpServletResponse輸出到頁面上的,就算到現在,教材上仍有這部分的演示代碼。下面,就讓我們一起來看下HttpServletResponse是如何發送消息體到客戶端的。
首先我們來看兩個HttpServletResponse提供的兩個方法:

從中我們可以看到,getOutputStream()方法返回ServletOutputStream對象,更適合向客戶端寫入二進制數據,並且Servlet容器不會對這些二進制數據進行編碼,因此我們常用ServletOutputStream來向客戶端發送如圖片、文件等內容;對於getWriter()方法返回的PrintWriter對象,里面封裝了更多的寫入字符文本的函數,並且我們上文提到的setContentType()方法設置的MIME類型對其輸出內容有效,因此也可以很好地解決中文亂碼問題。
還有一點需要注意的是,這兩個方法在一個response對象中不可以同時調用,否則會拋出一個IllegalStateException,也就是非法狀態異常,因為輸出流只能有一個(如果可以多次獲取的話,客戶端又如何確認哪個Http響應是最后一個呢)。
下面我們對來簡單的介紹下ServletOutputStream對象和PrintWriter對象中的方法,我們首先來看下ServletOutputStream這個對象(抽象類)的概述(Outline),可以看到,其重載了幾乎可以輸出各種數據類型的print()、println()方法,但是通過查看源碼可以發現,這些方法都是通過其父類OutputStream(java.io.OutputStream)的write()方法進行的消息體的輸出。

下面我們來看下PrintWriter對象的概述,其方法較多,我們只截取部分主要方法,如下圖所示,PrintWriter中提供的輸出方法更多,其輸出方法都是通過Writer(java.io.Writer)類中的write()方法來進行的消息體的輸出。


因為PrintWriter的輸出功能在前面已經使用N遍了,下面我們主要演示下如何通過ServletOutputStream來輸出內容下面我們簡單的通過代碼演示下ServletOutputStream的使用,我們在ResponseTestServlet中的doGet中代碼修改如下(注釋之前的部分):
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//設置返回客戶端的contentType
response.setContentType("text/html;charset=utf-8");
//...
ServletOutputStream out = response.getOutputStream();
//通過ServletOutputStream向客戶端輸出值
out.print("We Are Young Man!");
}
其執行結果如下圖所示,瀏覽器中輸出了Servlet給的響應。

這么看,ServletOutputStream和PrintWriter似乎沒什么區別,ServletOutputStream一樣可以輸出字符串呀,但是,注意了,當我們吧輸出內容改為中文,代碼修改為out.print("我們不一樣!");
,讓我在來看下執行結果:

啥情況,居然發生了錯誤?代碼中不是通過setContentType設置了編碼格式為UTF-8了么,為什么頁面中會提示不是ISO 8859-1字符?這里我們在回過頭來看一句話,ServletOutputStream輸出二進制數據,並且Servlet容器不會對這些二進制數據進行編碼,這里就是說,你輸入二進制流是什么,Servlet容器不會對你的輸出流編碼,因此上面setContentType是無效的。那有為什么會產生異常呢,我們來看下ServletOutputStream中的print(String s)的源碼。(注意,println方法中調用的print方法)

public void print(String s) throws IOException {
if (s==null) s="null";
int len = s.length();
for (int i = 0; i < len; i++) {
char c = s.charAt (i);
// XXX NOTE: This is clearly incorrect for many strings,
// but is the only consistent approach within the current
// servlet framework. It must suffice until servlet output
// streams properly encode their output.
//
if ((c & 0xff00) != 0) { // high order byte must be zero
String errMsg = lStrings.getString("err.not_iso8859_1");
Object[] errArgs = new Object[1];
errArgs[0] = Character.valueOf(c);
errMsg = MessageFormat.format(errMsg, errArgs);
throw new CharConversionException(errMsg);
}
write (c);
}
}
注意下中間的一段注釋,明確的告知了這個方法對許多字符是不正確的,iso 8859-1編碼方式完全不支持中文,因此這里在轉換的過程中會直接的拋出異常,我們在上個運行結果上看到的報錯信息的根由也是在此。
通過源碼我們也可以看到,print並沒有進行轉碼,只是判斷一個字節的高地址的一個字節(8位)是否為0(注:iso 8859-1只使用了一個字節來進行編碼),一次來判斷字符是否是iso 8859-1字符集中的字符。那這樣的話,ServletOutputStream就真的無法輸出中文了么?

山重水復疑無路,柳暗花明又一村。如果Servlet容器不對二進制數據進行任何的處理,那么,我們是不是可以換個思路?直接將String轉為指定編碼方式的byte[](字節數組),並通過ServletOutputStream中的write(byte b[])方法將字符數組輸出的到客戶端。對應的,我們將上面的代碼修改如下:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//設置返回客戶端的contentType
response.setContentType("text/html;charset=utf-8");
//...
ServletOutputStream out = response.getOutputStream();
//通過ServletOutputStream向客戶端輸出值
//通過getBytes獲取字節數組,並指定編碼方式
out.print("我們不一樣!".getBytes("UTF-8"));
}
其運行結果也如下所示:

我么可以看到,瀏覽器中正常的顯示了中文輸出。但是如果我們的每個含有中文的字符串都需要使用這種方式輸出,那不是態麻煩了。這也是我們在描述ServletOutputStream是說的,其適合(suitable)輸出二進制數據。因此在對客戶端的Http請求進行響應式,我們也要選擇合理的輸出方式。
4.總結
本文主要講解了Servlet如何對Http請求進行響應,Http響應對應Http請求的三部分內容,分別為響應行、響應頭和消息體,以及對應的如何通過HttpServletResponse設置對應的狀態碼、響應頭,並詳細的解釋了getOutputStream()和getWriter()的區別及其使用場景。
參考閱讀:Http1.1狀態碼定義
又到了分隔線以下,本文到此就結束了,本文內容全部都是由博主自己進行整理並結合自身的理解進行總結,如果有什么錯誤,還請批評指正。
Java web這一專欄會是一個系列博客,喜歡的話可以持續關注,如果本文對你有所幫助,還請還請點贊、評論加關注。
有任何疑問,可以評論區留言。