多バイト文字の変換とかいわゆる文字化けとその対処に関する自分用のまとめ

webアプリはいろいろ関わってきてるはずなのに、このあたりの話を全然理解してなかったことに愕然とする。
普通のひとには常識なんだろうけど、今回調べたことで体得できたよ。

追記

少し理解が進んだので追記。

TomcatのGETパラメータのデフォルトの挙動

iso-8859-1とみなしてURLエンコードしてくれる。また、GETパラメータには、HttpServletRequest#setCharacterEncodingは適用されない。

URIEncoding

URI文字列に多バイト文字があった場合にどの文字コードとみなしてURLエンコードするかを設定する。パラメータに限らず、URIのファイル名とかもこれに則る。

useBodyEncodingForURI

これをtrueにすると、GETパラメータであっても、HttpServletRequest#setCharacterEncodingを適用させてくれる。SetCharacterEncodingFilterを使っているのなら、基本的にこれでいいと思うけど、日本語ファイル名とかあるときは、URIEncodingのほうが適当なんだろう。

以下の内容のまとめ

JavaでTomcat6を想定。
以下のようにすればたいがいは問題ないはず。

  • システムのページやらコードやらをすべてUTF-8に統一して、POSTで渡すのをUTF-8に統一
  • Connectorタグに URIEncoding="UTF-8" を追加してGETメソッドで渡ってくるのををUTF-8に統一
  • SetCharacterEncodingFilterを使ってfilterでrequest.setCharacterEncoding("UTF-8")を指定
対応できるもの
  • ページ内から送られるPOSTリクエスト全て
  • IE6, IE7, FF3, 設定を変えたFF2からのGETリクエスト全て
対応できないもの
  • ページ外から送られるPOSTリクエスト(エンコードされるまえの文字コードが合っていれば対応できる)
  • デフォルト設定のFF2でのロケーションバーからのGETリクエスト

対応できないものは上記に限られるので、シチュエーションに合わせて対応するかしないかを選べる。

クライアント側の話

OperaとかSafariは常用してないのでわかりません。

ロケーションバーから入力 formから入力 デフォルトのnetwork.standard-url.encode-utf8の値
FF2 システムのデフォルト文字コードでURLエンコードされるっぽい ページの文字コードでURLエンコードされる false
FF3 UTF-8でURLエンコードされる ページの文字コードでURLエンコードされる true
IE6,7 UTF-8でURLエンコードされる ページの文字コードでURLエンコードされる -
  • GETならばメッセージヘッダ
  • POSTならばメッセージボディ

で渡される。

「ページの文字コードでURLエンコードされる」というのは、

文字コードが選択されるということ。

「システムのデフォルト文字コードでURLエンコードするっぽい」と書いてるのはWindowsシステムしか会社にないため。帰ったらMacで試してみよう。

うん、UTF-8の方言のX-MAC-JAPANESEでURLエンコードされてるな。

サーバ側の話

JavaでTomcat6を使ったときの話。

GETメソッド(メッセージヘッダ) POSTメソッド(メッセージボディ) 手間
setCharacterEncoding ×
URIEncoding ×
new String(request.getParameter("text").getBytes("iso-8859-1"), "UTF-8") ×
request#getParameterしたのをgetBytesで崩して、コンストラクタで再組み立てする
new String(request.getParameter("text").getBytes("iso-8859-1"), "UTF-8")

みたいな。
iso-8859-1を指定するのはURLデコードされた文字列をバイナリとして扱うため?ダメだ。さっそくここの理解が怪しいぜ。

iso-8859-1にしてるのは、メッセージヘッダはデフォルトでiso-8859-1でTomcatエンコードしてくれるから

この場合は、バイナリがUTF-8エンコードされてると想定して、UTF-8でデコードしてる。

  • メリット
    • これをやるだけでGETの場合もPOSTの場合も対応できる
  • デメリット
    • デフォルト状態のFF2でロケーションバーから入力されたときに対応できない
    • request#getPrameterするときに毎回しないといけないので面倒
URIEncoding

server.xml

<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443" URIEncoding="UTF-8"/>

とかって書けばいい。これで必ずUTF-8でURLエンコードされているとみなすようになる。

useBodyEncodingForURI="true"

でもいいけど、下位互換性のためっぽいし、URIEncodingでいいんじゃないかと思う。どっちでもいいと思うけど。
役割が違う。

  • メリット
  • デメリット
    • Appサーバ全体で設定しないといけないし、server.xmlに書くのがなんかイヤ
setCharacterEncodingを使う
request.setCharacterEncoding("UTF-8");

みたいな。
メッセージボディのエンコードを指定するだけなので、メッセージヘッダで値を送られてくるGETメソッドには対応できない。

examplesに入ってるfilters.SetCharacterEncodingFilter.javaをパクって使うと便利。

	<filter>
		<filter-name>SetCharacterEncodingFilter</filter-name>
		<filter-class>jp.foo.filter.SetCharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>SetCharacterEncodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
  • メリット
    • メッセージボディのエンコードを一括指定するので、POSTメソッドで送られた場合に一気に対応できて便利
  • デメリット
    • メッセージボディのエンコードを指定するだけなので、メッセージヘッダで値を送られてくるGETメソッドには対応できない。GETメソッドに対応するには別途対策が必要
    • filterを使わずに各ページで設定とかするんだとだいぶムダ。Servletから上書きできるんだからfilter使うのが普通だろうね