Servlet 中文處理(Tomcat)


身為非西歐語系的國家,總是得處理編碼的問題,例如,你的使用者會發送中文,那你要如何正確處理請求參數,才可以得到正確的中文字元呢?在〈URL 編碼〉曾經談過 URL 編碼的問題,這是正確處理請求參數前必須知道的基礎。

請求參數的編碼處理,基本上必須分 POST 與 GET 的情況來說明,先來看 POST 的情況…

POST 請求

如果客戶端沒有在 Content-Type 標頭中設定字元編碼資訊(例如瀏覽器可以設定 Content-Type: text/html; charset=UTF-8),此時使用 HttpServletRequestgetCharacterEncoding() 傳回值會是 null,在這個情況下,容器若使用的預設編碼處理是 ISO-8859-1(大部份瀏覽器預設的字元集),而\客戶端使用 UTF-8 發送非ASCII字元的請求參數,而 Servlet 直接使用 getParameter() 等 取得該請求參數值,就會是不正確的結果也就是得到亂碼。

可以用另一種方式,來簡略表示出為何這個過程會出現亂碼,假設網頁編碼是 UTF-8,透過表單使用 POST 發出「林」這個中文字元,會將「林」作URL編碼為 %E6%9E%97 再送出,也就是瀏覽器相當於作了這個動作:

String text = java.net.URLEncoder.encode("林", "UTF-8");

在 Servlet 取得請求參數時,容器若預設使用 ISO-8859-1 來處理編碼,相當於作了這個動作:

String text = java.net.URLDecoder.decode("%E6%9E%97", "ISO-8859-1");

這樣作的話,顯示出來的中文字元就不正確了。

可以使用 HttpServletRequestsetCharacterEncoding() 方法指定取得 POST 請求參數時使用的編 碼。例如若瀏覽器以 UTF-8 來發送請求,接收時也要使用 UTF-8 編碼字串,則可以在取得任何請求值之「前」,執行以下陳述:

request.setCharacterEncoding("UTF-8");

這相當於要求容器作這個動作:

String text = java.net.URLDecoder.decode("%E6%9E%97", "UTF-8");

如此就可以取得正確的 「林」中文字元了。記得,一定要在取得任何請求參數前執行 setCharacterEncoding() 方法才有作用,在取得請求參數之後呼叫 setCharacterEncoding() 沒有任何作用。

GET 請求

HttpServletRequest 的 API 文件中,對 setCharacterEncoding() 的說明清楚提到:

Overrides the name of the character encoding used in the body of this request.

也就是說,這個方法對於請求本體中的字元編碼才有作用,也就是基本上這個方法只對 POST 產生作用,當請求是用 GET 發送時,沒有定義這個方法是否會影響 Web 容器處理編碼的方式(究其原因,是因為處理 URL 的是 HTTP 伺服器,而非 Web 容器)。

例如 Tomcat 在 GET 時,使用 setCharacterEncoding() 方法設定編碼就不會有作用。

一個處理字串編碼的方式,是透過 StringgetBytes() 指定編碼來取得該字串的位元組陣列,然後再重新建構為正確編碼的字串。

例如若瀏覽器使用 UTF-8 處理字元,HTTP 伺服器在 URI 上採用 ISO-8859-1 編碼,正確處理編碼的方式為:

String name = req.getParameter("name");
String name = new String(name.getBytes("ISO-8859-1"), "UTF-8");

舉例來說,在 UTF-8 的網頁中,對「林」這個字元,若使用表單發送 GET 請求,瀏覽器相當於作了這個動作:

String text = java.net.URLEncoder.encode("林", "UTF-8");

HTTP 伺服器若在 URI 採用 ISO-8859-1 編碼,相當於作了這個動作:

String text = java.net.URLDecoder.decode("%E6%9E%97", "ISO-8859-1");

使用 getParameter() 取得的字串就是上例 text 參考的字串,可以依下面的編碼轉換來得到正確的「林」字元:

text = new String(name.getBytes("ISO-8859-1"), "UTF-8");

以上是有關於取得中文請求參數,接下來看如何輸出中文內容。

回應

在沒有設定任何內容型態或編碼之前,HttpServletResponse 使用的字元編碼預設是 ISO-8859-1,也就是說,如果直接輸出中文,在瀏覽器上就會看到亂碼。有幾個方式可以影響 HttpServletResponse 輸出的編碼處理。

瀏覽器如果有發送 Accept-Language 標頭,可以使用 HttpServletRequestgetLocale() 來取得一個 Locale 物件,代表客戶端可接受的語系。

可以使用 HttpServletResponsesetLocale() 來設定地區(Locale)資訊,地區資訊就包括了語系與編碼資訊。語系資訊通常透過回應標頭 Content-Language 來設定,而 setLocale() 也會設定 HTTP 回應的 Content-Language 標頭。例如:

response.setLocale(Locale.TAIWAN);  

這會將 HTTP 回應的 Content-Language 設定為 zh-TW,而字元編碼處理設定為 BIG5。可以使用 HttpServletResponsegetCharacterEncoding() 方法取得編碼設定。

可以在web.xml中設定預設的區域與編碼對應。例如:

...
<locale-encoding-mapping-list>
    <locale-encoding-mapping>
        <locale>zh_TW</locale>
        <encoding>UTF-8</encoding>
    </locale-encoding-mapping>
</locale-encoding-mapping-list>
...

設定好以上資訊後,若使用 response.setLocale(Locale.TAIWAN),或者是 response.setLocale(new Locale("zh", "TW")),那麼 HttpServletResponse 的字元編碼處理就採 UTF-8,getCharacterEncoding() 取得的結果就是 "UTF-8"

也可以呼叫 HttpServletResponsesetCharacgerEncoding() 設定字元編碼:

response.setCharacterEncoding("UTF-8");

或者是在使用 HttpServletResponsesetContentType() 時,指定 charsetcharset 的值會自動用來呼叫 setCharacterEncoding()

例如以下不僅設定內容類型為 text/html,也會自動呼叫 setCharacterEncoding(),設定編碼為 "UTF-8"

response.setContentType("text/html; charset=UTF-8");

如果使用了 setCharacterEncoding()setContentType() 時指定了 charset,則 setLocale() 就會被忽略。

就結論來說,如果要接收中文請求參數並在回應時於瀏覽器正確顯示中文,必須同時設定 HttpServletRequestsetCharacterEncoding() 以及 HttpServletResponsesetCharacterEncoding()setContentType() 為正確的編碼。

Tomcat 可以在 server.xml 中設定 <Connector URIEncoding="utf-8"...>,決定 URI 的編碼處理,以前版本的設定是 <Connector URIEncoding="iso-8859-1"...>,因此過去處理 GET 的中文亂碼時,可以 new String(param.getBytes("ISO-8859-1"), "your encoding") 來解決,不過,如果 server.xml 中設定 <Connector URIEncoding="utf-8"...> 就不適用了(像是 Tomcat 8 之後),例如 GET 請求時的請求參數為 Big5,而 URI 編碼採用 UTF-8,可能會沒有對應編碼,也就是顯示時會是「?」,這樣通常就沒救了 …

如果以前就是用 new String(param.getBytes("ISO-8859-1"), "your encoding") 來處理 GET 時的亂碼,在新版本 Tomcat 上,將 <Connector URIEncoding="utf-8"...> 改回 <Connector URIEncoding="iso-8859-1"...> 就可以了…當然,這是暫時之計,UTF-8 目前是主流,還是讓應用程式支援 UTF-8 為上策。