JSP 中的字符集編碼與亂碼問題 轉

在說完了網頁中的編碼與亂碼(),servlet 中的編碼問題後,這次來探討一下 JSP 中的編碼與亂碼問題。

在之前,曾談到過 JSP 與 HTML 間的關係,JSP 本質上是一個 HTML 的模板,用於在服務端動態生成 HTML,這點跟 servlet 是類似。

其實 JSP 本質就是 servlet,一個 JSP 頁面它會被編譯成一個 java 文件,實際上就是一個 servlet 類(或其子類,在文章的後面會具體討論這個問題)。

一個具有多個編碼的 JSP 頁面

關於 JSP 中的編碼設置,有好幾處的地方值得注意。用一個例子來說吧,比如下面這個叫 testEncoding.jsp 的頁面:

jsp contentType pageEncoding meta charset diff value

有三處地方出現了編碼相關的信息(contentType,pageEncoding,meta charset),而且還是矛盾的,有 utf-8,gbk 和 iso-8859-1。(正常情況下你應該保持三者一致)那麼現在問題來了:

  1. 這個源文件到底是什麼編碼?
  2. 最終生成併發送到瀏覽器的字節流又是什麼編碼?
  3. 會不會出現亂碼?(包括用編輯器打開以及在瀏覽器中打開時)

這三個矛盾的編碼可能還是會讓很多人困惑的,可能會覺得被搞暈了,下面將一一分析以上問題。

源文件的編碼與 pageEncoding

自然,需要明白 @page 指令中各個屬性的含義,比如 contentType,有了前面的網頁中的編碼與亂碼(3)的基礎等,應該不難猜出它就是指定了 response header 中的 content-type 字段的值。

那麼 pageEncoding 又是什麼呢?其實從字面上也不難猜出,它就是頁面的編碼,也就是這個 JSP 源文件本身的編碼。

在 eclipse 中,在文件上“右鍵--屬性(properties)”,在彈出的屬性框中可以發現文件的編碼本身就是 gbk:

jsp file properties text file encoding determined from content

而且它還加了一句話(determined from content:GBK),也就是從內容中推斷出來的。而頁面的內容中不就是 pageEncoding 那裏纔有 gbk 嗎,所以這個屬性就是源文件編碼本身。如果你調整了它的值,推斷出的值也會跟着改變,你可以自行試試。

注意:只有智能的編輯器(比如專門的 JSP 編輯器)才能做這些推斷,普通的編輯器是不行的。在前面的網頁中的編碼與亂碼(2)中曾經談過這個問題。

JSP 頁面編碼的官方參考

關於 JSP 頁面編碼的官方參考見這裏:https://docs.oracle.com/cd/E19316-01/819-3669/bnayf/index.html

在此官方文檔裏它提到:

For JSP pages, the page encoding is the character encoding in which the file is encoded.

For JSP pages in standard syntax, the page encoding is determined from the following sources:

  • The page encoding value of a JSP property group (see Setting Properties for Groups of JSP Pages) whose URL pattern matches the page.

  • The pageEncoding attribute of the page directive of the page. It is a translation-time error to name different encodings in the pageEncoding attribute of the page directive of a JSP page and in a JSP property group.

  • The CHARSET value of the contentType attribute of the page directive.

If none of these is provided, ISO-8859-1 is used as the default page encoding.

簡單翻譯一下,JSP 頁面編碼的確定有四個途徑(越前面的優先級越高):

  1. JSP 屬性組(JSP property group)中的配置。
  2. page 指令中的 pageEncoding 屬性。
  3. page 指令中的 contentType 屬性下的 charset 的值。
  4. 以上均無提供,使用缺省值,具體爲 ISO-8859-1。

由上可見,pageEncoding 屬性它的優先級還不是最高的,最高的是 JSP 屬性組中的配置,這個到底是什麼呢?

JSP 屬性組中的 page-encoding

其實它就是 web.xml 中的一個配置項,比如下面就配置了一個全局的 JSP 文件編碼,值爲 utf-8:

web.xml jsp-config jsp-property-group page-encoding config

具體爲 jsp-config 標籤下的 jsp-property-group 標籤下的 page-encoding 標籤所指定的值,再結合 url-pattern 標籤指定一個適配範圍。

假如增加以上配置,那麼它這裏配置的 utf-8 實際上與現在這個文件中的 pageEncoding=”gbk”是衝突的,這種不一致會導致 servlet 容器(這裏也就是 tomcat)報錯:

page-encoding specified in jsp-property-group is different from that specified in page directive. http status 500

既然你在 web.xml中配置了 page-encoding,tomcat 遵循 JSP 規範會優先採納它。

這也很好理解,既然你都專門配置了來告訴我文件的編碼,tomcat 都不需要自己去檢測了,它何樂而不爲呢?

但它接着又發現文件裏面還指定了 pageEncoding,值與配置的還不一致,於是它就被搞糊塗了,拋出了以上異常,抱怨兩者不一致。

其實是 JSP 中的規定,說不一致就拋異常,tomcat 作爲一個 servelt 容器只是遵循了這些規範。另:手動測試時,當在 web.xml 中增加配置後,你可能需要清理一下 tomcat 的緩存,以確保 JSP 文件被重新讀取編譯等。

所以,配置這種全局屬性要小心,要麼不要採用它;要麼可以通過 url-patterm 縮小匹配的範圍,確保它只應用在正確的文件或文件夾下。

另一方面要保證保存文件的真實編碼確實使用了配置的值。這點在前面也一再強調過。

由前面的優先級還可以知道,假如沒有 JSP 屬性組的配置,也沒有 pageEncoding 屬性,則會用 contentType 中的 charset 值作爲頁面的編碼。

儘管這個值實際是給響應流用的。在前面的網頁中的編碼與亂碼(3)篇章中曾詳細討論過這個問題。

那麼現在清楚了,這個 JSP 文件本身的編碼是 gbk,但它也可能受一些全局性配置的影響。

智能的 JSP 文本編輯器會採納 pageEncoding 的值來保存,當然,如果不是智能的編輯器,情況就不好說了,你完全可以在保存時自己指定一個編碼。

響應流中的編碼

以上就是第一個問題,接下來討論第二個問題:最終生成併發送到瀏覽器的字節流又是什麼編碼?

是 pageEncoding 中指定的 GBK 呢?還是 contentType 中指定 UTF-8 呢?亦或是 meta charset 中的 ISO-8859-1 呢?

如果在瀏覽器中訪問這個頁面,中文是能正確顯示的:

jsp encoding test page

這說明肯定不是 ISO-8859-1 了。再看 response 中的具體內容,也就是最終生成的 html,gbk 什麼的也全部消失了,只有 meta charset 還在:

jsp response content

再看 response header 中的 Content-Type,charset 是 utf-8:

Response header content-type charset utf-8

所以,響應流的編碼實際就是 utf-8。

響應流編碼的官方參考

關於這個響應流的編碼,同樣有官方文檔:https://docs.oracle.com/cd/E19316-01/819-3669/bnayg/index.html

具體來說是這樣的:

The response encoding is the character encoding of the textual response generated by a web component. The response encoding must be set appropriately so that the characters are rendered correctly for a given locale. A web container sets an initial response encoding for a JSP page from the following sources:

  • The CHARSET value of the contentType attribute of the page directive

  • The encoding specified by the pageEncoding attribute of the page directive

  • The page encoding value of a JSP property group whose URL pattern matches the page

If none of these is provided, ISO-8859-1 is used as the default response encoding.

簡單翻譯一下,它由以下幾個步驟來確定響應流的編碼(越前面的優先級越高):

  1. page 指令中的 contentType 屬性下的 charset 的值。
  2. page 指令中的 pageEncoding 屬性。
  3. JSP 屬性組(JSP property group)中的配置。
  4. 以上均無提供,使用缺省值,具體爲 ISO-8859-1。

發現什麼規律沒有呢?前面三項與之前的確定頁面編碼的順序恰好是相反的,也就是 contentType 中的值這時反而是優先級最高的;沒有 contentType,纔會看 pageEncoding。

所以在這個例子中,gbk 只是作爲文件編碼,tomcat 用它來把源文件正確讀取上來,之後就沒有它什麼事了。

而假如前兩者都不存在,就會採用 JSP 屬性組中的值,就是前面介紹的那個 web.xml 中的 page-encoding 配置(假如有的話)。

最後,假如以上手段都不能確定,就用缺省值 ISO-8859-1。

從 JSP 到 Servlet 再到 HTML

其實 JSP 文件會被轉換成一個 Java 文件,在這個例子中,具體在我的電腦上,可以去到 D:\dev\wp\neon3\workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\work\Catalina\localhost\jcc-web\org\apache\jsp\demo\encoding\page 下,

這個文件夾的前面部分就是上一篇 Java servlet 使用 PrintWriter 時的編碼與亂碼介紹的 tomcat 啓動命令行中的 –Dcatalina.base 中的值:

tomcat server command line catalina.base

不同的部署方式具體情況可能會有所差別。不同的 server 甚至不同的 IDE 工具,請自行查閱資料瞭解具體部署到的地方。

可以發現一個 testEncoding_jsp.java 和 testEncoding_jsp.class 的文件:

jsp servlet work location

與 testEncoding.jsp 是對應的(從名字與所處位置都不難推測出來)。它的內容如下:

jsp 生成的 servlet 類示例

從開頭的註釋可以看出它是由 tomcat 的 Jasper 組件生成的,繼承自 org.apache.jasper.runtime.HttpJspBase 這個類。

把這個文件導入某個 web 工程中,查看其類型層級(在 eclipse 中,具體操作爲:菜單--Navigate—Open Type Hierarchy):

jsp 生成的 servlet 類的類型層級

在左側的繼承樹中可以看到它繼承了 HttpJspBase,而後者又繼承了 HttpServlet ,所以它實際上就是一個 servlet。在右側的方法和屬性列表中還可以看到它有個 _jspService 方法,有 request 和 response 兩個參數,實際上就類似於 servlet 中的 service 方法。

去到這個 _jspService 方法中看一下,將其中部分內容(只截取了關鍵部分,因爲整個方法比較長)與原來的 JSP 頁面及最終的 html 輸出對比的話:

jsp to servlet to html

不難看出,jsp 中 page 指令中的 contentType 的值就成了 servlet 中的 response.setContentType 的值,最終成爲前面提到的瀏覽器中 header 響應中的 Content-Type:

Response header content-type charset utf-8

而 jsp 中的 java 腳本(<%%> 部分的代碼)就直接轉換成了代碼;其它 jsp 中跟 html 一樣的標籤在 servlet 中就直接用 out.write 輸出了,最終 out 輸出的結果就是在瀏覽器端看到的 response 的內容,也就是最終的 html 頁面。

pageEncoding 在這裏已經不存在了。那麼,至此,第二個問題答案也清楚了,響應流用 utf-8 編碼。

會不會亂碼?

最後一個問題,會不會出現亂碼?(包括用編輯器打開以及在瀏覽器中打開時)其實答案也很清楚了。

前面網頁中的編碼與亂碼(3)提到,response header 中的 Content-Type 下的 charset 編碼具有比 html 頁面中 meta charset 聲明的值更高的優先級,所以瀏覽器會選用 utf-8 而不是 ISO-8859-1 來解析,所以頁面顯示也是正常的。

而編輯 jsp 文件時,智能的編輯器會檢測到正確的編碼,是可以正常打開這個文件的。而普通的編輯器就不好說了,會受很多因素影響,比如所在系統環境的缺省編碼設置,以及編輯器本身是否具有一定的編碼探測能力等等,也可能會正常打開,也可能在打開時亂碼。

示例代碼(gitee)

最後,文中示例的代碼可見我的碼雲:https://gitee.com/goldenshaw/java_code_complete/commit/33f03921214a31b052a0e968805652dd0d46f849

好了,關於 JSP 中的字符集編碼與亂碼問題的探討就到這裏,在正常的開發活動中,你應該始終注意保持幾處編碼的一致,比如始終在各處統一使用 utf-8 編碼,這樣就能避免絕大多數問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章