Agusa Lab. > Webware Project > GrapeWeb
 

Web アプリケーションの日本語はなぜ化ける?

概要

文字化けは Web アプリ開発で最も頭の痛い問題のひとつです。(少なくとも日本人向けの Web アプリでは)
今回のチュートリアルでは、文字化けが発生する理由から、発生箇所の特定および文字化け解消の方法までを、理解します。

:Note.
Web システム開発の日本語処理関係は、JAVA PRESS の Vol.37 でも特集が組まれています。

文字化けが発生する原因

なぜ、文字化けが発生するのでしょうか?そして、なぜ、その解決は難しいのでしょうか?
その理由は Web システム(分散システム)特有の複雑なアーキテクチャが原因です(少なくとも私はそう思います)。
ここでは、Struts アプリケーションが動作する仕組みを整理し、文字化けが発生する箇所を特定します。

第1回でも簡単に触れましたが、Struts アプリケーションは ユーザのアクセスに応じて下図のようにメッセージ通信を行い、アプリケーションとして動作します。
「文字化け」は、これらのメッセージ通信の間で文字コードの違いが存在する場合に発生します。

Struts の動作

  1. ユーザが Web ブラウザを利用してリクエストを送信
  2. ユーザのリクエストを Tomcat が受け取る
  3. Tomcat は Struts (Controller) にリクエストを渡す
  4. Struts (Controller) は必要に応じて、ビジネスロジックを実行する
  5. Struts (Controller)は表示するページを Tomcat に知らせる
  6. Tomcat は指定されたページ URL を Web ブラウザに返す
  7. Web ブラウザは指定された URL のページを表示

以降では、Struts アプリケーションで頻繁に発生する以下の「文字化け」について、その理由と共に対処法を紹介します。

  • JSP ファイルに記述した日本語が文字化けする
  • JSP でリソースファイルに記述した文字列を表示する際に文字化けする
  • JSP でフォーム入力を表示する際に文字化けする
  • JSP で Java プログラムでセットした文字列を表示する際に文字化けする

以降の例では、第2回で利用した リソースファイルを利用したログインアプリケーション を利用して説明します。

補足:Struts アプリケーションの仕組み(もしくは内臓?!)

文字化けの発生する原因やその対処法を知るためには、 多種言語からなる Struts アプリケーションの各要素が、 どのようにプログラムとして動作しているのかについて詳しく知る必要があります。
ここでは、「ソースファイルからアプリケーションがコンテナに配備されて動作するまで」の Struts アプリケーションの仕組みについて説明します。

コンテナへの配備まで

JSP や Java ファイルなどの作成から Web アプリケーションコンテナへの配備までは、 一般的に下図のような仕組みになります。

コンテナへの配備まで

Java ファイルは開発者が Java コンパイラを利用してコンパイルします。
Java 以外のファイルは、開発者が記述したファイルがそのまま Web アプリケーションとして配備されます。 

ユーザからのアクセス(初回)

コンテナに配備された Web アプリケーションは、初回ユーザアクセス時に必要なファイルをコンパイルします。

コンテナへの配備まで

Web アプリケーションの JSP ファイルは、 初めてユーザからアクセスがあった時に サーブレットコンテナによってコンパイルされます。

:Note.
この仕組みは、「Struts アプリケーションの仕組み」というわけではなく、Tomcat (Servlet コンテナ)の仕組みです。

JSP ファイルに記述した日本語の文字化け

まずは、サンプルアプリケーションで文字化けを発生させましょう。
login.jsp の適当な箇所に日本語を記述し、サンプルアプリケーションにアクセスしてみてください。

	(変更サンプル)
	<body bgcolor="white">
	<h1><bean:message bundle="messages" key="messages.welcome" /></h1>
	<h2><bean:message bundle="messages" key="messages.withargs" arg0="waiting" arg1="for" /></h2>
	<h3>名前とパスワードを入力してください</h3>
	
	<html:form action="/Login">
	<table border="1">
	<tr>
		<td align="right">名前: </td>
		<td><input type="text" name="username" /></td>
	</tr>
	<tr>
		<td align="right">パスワード: </td>
		<td><input type="password" name="passwd" /></td>
	</tr>
	<tr>
		<td colspan="2" align="right"><input type="submit" /></td>
	</tr>
	</table>
	</html:form>
		

実験する環境によっては、文字化けがしないかも知れません。 しかし、実際に上記のコードは文字化けを発生させます。 (チュートリアル中のデモを見てください)

では、この文字化けは何が原因で生じたのでしょうか?
この文字化けは"JSP ファイルの文字コード"と "サーブレットコンテナを動作させている VM の file.encoding 属性"の違いによって生じた文字化けです。
サンプルアプリケーションの JSP ファイルは全て UTF-8 の文字コードで記述されています。 それに対してデモで利用している Tomcat を動作させる VM の file.encoding 属性は Shift_JIS です。 (余談ですが、この file.encoding 属性は Mac OS X の Java VM のデフォルト設定です。)

既に紹介したように、JSP ファイルは "Tomcat によって Servlet ファイルに変換され"、 "Tomcat が Java コンパイラを利用して"コンパイルします。 この時、Tomcat は特に文字コードの指定を行いません。
そのため、JSP ファイルはデフォルトで "Tomcat を動作させる VM の file.encoding 属性" で Servlet ファイルに変換され、 コンパイルされるのです。

では、どうすればこの文字化けを防げるでしょうか?
もちろん、「Tomcat が動作する VM の file.encoding 属性に合わせて、JSP ファイルを記述する」あるいはその逆によって 文字化けを防ぐことは出来ますが、それでは環境が変わるたびに、どちらかを修正することになってしまいます。

このような状況に対処するためには、 page ディレクティブ(<%@ page %> タグ)とその属性を利用します。
page ディレクティブは、HTML の <meta> タグに相当するようなものであり、JSP ファイルの属性を指定するために利用します。
今回の文字化けは、Tomcat が JSP ファイルの文字コードを知らないために発生しました。 page ディレクティブを利用して文字コードを指定するには、その属性 pageEncoding を利用します。
また、<meta> と同様に page ディレクティブにも、contentType に相当する属性が存在します。
これらを利用して、JSP ページの属性を指定すれば、「JSP ファイルに記述した日本語の文字化け」を防ぐことができます。

	(記述サンプル)
	<@ taglib uri="/tags/struts-html" prefix="html">
	<%@ taglib uri="/tags/struts-bean" prefix="bean" %>
	<%@ page pageEncoding="UTF-8" %/>
		
:Warning!
page ディレクティブは JSP 1.2 仕様によって策定されました。 そのため、それ以前のバージョンの JSP では利用できません。(Tomcat では、4 系以降から利用可能)
:Note.
ディレクティブは、JSP において各種の宣言を行うタグの総称です。

それでは、実際に上記サンプルを JSP に記述して再びサンプルアプリケーションにアクセスしてください。
確かに文字化けが解消されているハズです。

JSP でリソースファイルに記述した文字列を表示する際に文字化けする

先程と同様に、この症状に関してもまずは文字化けを発生させます。
サンプルアプリケーションの messages.properties を適当なエディタで開き、messages.welcome の値を日本語にし、 Tomcat の再起動後、サンプルアプリケーションにアクセスしてください。

	(変更サンプル)
	messages.welcome=ようこそ
		

先程と同じように、文字化けが発生するかどうかは環境次第ですが、上記の変更も文字化けが発生します。

この文字化けは "リソースファイルの文字コード"と "サーブレットコンテナを動作させている VM の file.encoding 属性"、 "JSP ファイルの文字コード"の違い など様々な要因が考えられます。

今回も、もちろん全ての文字エンコーディングを統一すれば良いのですが、それだとやはり他の環境に対応できません。
そこで、JDK に付随の native2ascii コマンドを利用して、プロパティファイルを ASCII ファイルに変換するという手法を取ります。
具体的には下記のように変換します。

	% native2ascii (入力ファイル) > (出力ファイル)
		

それでは、実際に上記の手順で変換したファイルを classes/messages.properties として置き換えて、再度、 サンプルアプリケーションにアクセスしてみてください。

:Warning!
native2ascii の入力となるファイルの文字コードは -encoding オプションにより指定可能ですが、 デフォルトでは Java のデフォルトエンコーディング(VM の file.encoding 属性)と同じであることに注意してください。
:Note.
余談ですが、プロパティファイルの編集にはいろいろ便利なツールがあります。(たぶん、頻繁に行う作業なのに割と面倒くさいから)
私はこのプロパティエディタを使っています。

JSP でフォーム入力を表示する際に文字化けする

例のごとく、まずこの症状の文字化けを発生させましょう。
サンプルアプリケーションの error.jsp を以下のように編集してください。

	<%@ taglib uri="/tags/struts-bean" prefix="bean" %>
	<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8"%>
	<html>
	<head>
	<title>Login App. (Error)</title>
	</head>
	<body bgcolor="white">
	<h3>Error!!</h3>
	Please confirm name and password.<br />
	ユーザ名: <bean:write name="InputForm" property="username"/>
	</body>
	</html>
		

細かい説明は省略しますが、この編集によりサンプルアプリケーションは「認証に失敗した場合、入力されたユーザ名を表示する」ようになりました。
それでは、実際にサンプルアプリケーションにアクセスし、ユーザ名に日本語名を入れて動作させてみてください。
表示される名前が文字化けしているはずです。

この文字化けは、Tomcat、Struts 共に HTTP リクエストに対してエンコーディング処理を施さないことが原因です。
今回のチュートリアルの最初で説明したように、HTTP リクエストは Web ブラウザから Tomcat、Tomcat から Struts と渡されます。
このため、HTTP リクエストとして渡されるフォーム入力の中に日本語が含まれている場合、文字化けしてしまいます。

:Note.
Tomcat では、HTTP リクエストを "ISO-8859-1" で決め打ちして取得しています。

フォーム入力など、HTTP リクエストとして渡される文字列のエンコーディングは、 ブラウザが表示しているページの文字コードに依存します。
そのため、フォーム入力の文字化けを防ぐには、
(1). 指定の文字コードでブラウザにページを表示させる
(2). HTTP リクエストを正しくエンコードする
という処理が必要になります。

上記の (1) に関しては、今回のチュートリアルで既に学びました。 そこで、次は "HTTP リクエストを正しくエンコードする" 必要があるのですが、これには2通りの手法があります。
1 つは、Tomcat のフィルタ機能を利用して HTTP リクエスト/レスポンスをエンコーディングするフィルタを作成する
もう 1 つは、Struts の ActionServlet クラスをエンコーディングに対応できるように拡張する
です。

これは、どちらが”良い”ということはないと思います。
が、強いて言えば Tomcat のフィルタ機能に関しては、なぜか Mac のサーバ + Tomcat 5.0.28 では正しく動作しないので、 Mac のサーバで利用することも考えるのであれば、Struts の ActionServlet クラスを拡張した方が確実でしょう。
このような理由から、今回は Struts の ActionServlet クラスを拡張する方法を説明します。
(Tomcat フィルタについては、下記の "Note" を参照してください。)

:Note.
・Tomcat のエンコーディングフィルタを利用する方法
1. 文字列エンコーディング機能を持つ Filter を用意する。
一番、簡単なのは
$TOMCAT_HOME/webapps/jsp-examples/WEB-INF/classes/filters/SetCharacterEncoding.java
を使う方法だと思います。
こいつをコンパイルして(とりあえずは)、自分の Web アプリの
WEB-INF/classes/filters/SetCharacterEncoding.class
として置いてください。

2. web.xml に Filter を使うよう編集を加える。
web.xml 中、web-app の子要素、context-param と listener の間に子要素 filter を記述。
(正確な順番については DTD を参照のこと)
filter の name に "SetCharacterEncoding"、class に "filters.SetCharacterEncoding" を記述
filter の子要素に init-param を記述。
init-param の name に "encoding"、"value" にエンコードする文字コードを記述
filter の直後に、filter-mapping 要素を記述(filter 要素と兄弟)。
filter-mapping の name に "SetCharacterEncoding"、url-pattern に "/*" を記述

この手順を踏むことで Tomcat のフィルタ機能が使えるはずです。(Windows XP, JDK 1.4, Tomcat 5.0.28 では使えました。)

では、ActionServlet を拡張する方法について説明します。
Struts の ActionServlet では、POST は doPost メソッド、GET は doGet メソッドにより実装されていますが、 内部で process メソッドを呼び出しています。 この時、引数として HTTP リクエスト、HTTP レスポンスが渡されます。
そこで、この HTTP リクエストを任意の文字コードにエンコードした後に、process メソッドに処理を引き渡します。 また、文字コードの指定は外部ファイルである web.xml に記述した値を取得してくるようにします。
具体的には、以下のように ActionServlet を拡張します。

	(拡張 ActionServlet のコード)
	package grape.struts.action;

	import org.apache.struts.action.ActionServlet;

	public class EncodableActionServlet extends ActionServlet {
    
    	protected void process(javax.servlet.http.HttpServletRequest request,
				javax.servlet.http.HttpServletResponse response)
				throws java.io.IOException, javax.servlet.ServletException {
			// web.xml から encoding パラメータを取得する
			String encoding = getServletConfig().getInitParameter("encoding");
			if (encoding != null) {
				// 指定された文字コードでリクエストをエンコーディング
				request.setCharacterEncoding(encoding);
			}
			// Struts の ActionServlet に処理を引き渡す
			super.process(request, response);
		}
	
	}
		

上記の拡張 ActionServlet を利用するには、サンプルアプリケーションに以下の修正を加えます。
a. 拡張 ActionServlet クラスを作成する。
b. 拡張 ActionServlet を利用するために、web.xml を編集する

web.xml の修正点は以下の通りです。
・ servlet タグの子要素 servlet-class を、作成した ActionServlet クラスに変更
・ servlet タグの子要素 init-param を追加
  ・param-name に encoding を、param-value に UTF-8 (エンコーディングする文字コード) を指定

上記の修正を終えた後、サンプルアプリケーションを再起動し、再度アクセスしてみてください。

JSP で Java プログラムでセットした文字列を表示する際に文字化けする

最後は、Java プログラムにセットした文字列を JSP で表示する際に発生する文字化けに対する対策です。
これまでと同様に、文字化けを発生させましょう。サンプルアプリケーションの LoginAction.java を以下のように修正してください。
この修正により、ログインに成功した場合 "日本語の名前" と言う文字列が JSP で表示されることになります。
サンプルアプリケーションにアクセスして、ログインに成功すると...。期待通り、文字化けしたページが表示されると思います。 (これまた環境によりますが)

	... 略 ...
	if (inputForm.getPasswd().equals(passwordResouce.getMessage(inputForm.getUsername()))) {
		request.setAttribute("username1", "日本語の名前");
		return mapping.findForward("success");
	} else {
		return mapping.findForward("error");
	}
	... 略 ...
		

この文字化けは先程の文字化けの逆パターンです。
つまり、先程は JSP から送られてくるメッセージに対して、エンコーディング処理が行われていないため生じた文字化けでしたが、 今度は Java プログラムから JSP に対して送りだすメッセージに対してエンコーディング処理が施されていないために生じています。

Java プログラムから JSP への応答は HTTP レスポンスで返されます。
そこで、先程と同じように response に対して文字エンコーディングを施します。
具体的には、先程の拡張 ActionServlet の if ブロックを以下のように修正(1 行追加)してください。

	if (encoding != null) {
		// 指定された文字コードでリクエストをエンコーディング
		request.setCharacterEncoding(encoding);
		// 指定された文字コードでレスポンスをエンコーディング
		response.setCharacterEncoding(encoding);
	}
		

さて、再びサンプルアプリケーションを再起動し、アクセスすると...。文字化けが治りましたね。

終わりに

今回は、Web アプリケーション開発の主要な問題の 1 つである文字化けについて取り上げました。
ここで取り上げた話題が全ての問題を網羅しているというわけではないですが、 全ての問題は今回の最初の図と照らし合わせて落ち着いて考えることができれば、それほど難しくないはずです。
「手段を知るのではなく、理屈を理解する」ことが、やはり基本で、かつ最重要なことです。

おまけ

今回、作成した Encodable ActionServlet をサーバに置いておきました。 使用法は、今回の説明通りです。
ダウンロードはこちらから
大したものではありませんが、十分、役に立つと思いますので、どうぞご自由にお使いください。

念のため、今回利用したサンプルアプリケーション(最終版)も こちらに置いておきます。

補足: GET で取得するパラメータの文字化け対策

この回では、POST メソッドで HTTP パラメータが渡される場合の文字化け対策について議論していました。 しかし、実際には GET メソッドを利用したい場合もあると思います。 (HTTP リクエストの中に値を全て埋め込んで、HTTP リクエストでページの状態を特定する場合など) このような場合、本チュートリアルで説明した対策では文字化けを防ぐ事ができないという報告をいただきました。 報告と同時に、対処法も教えていただいたのでこちらに記載しておきます。

GET メソッドによる文字化けは GET メソッドの特徴に起因します。 GET メソッドでは HTTP リクエストに値を埋め込みます。 第1回でも説明したように HTTP リクエストを最初に処理するのは、Servlet コンテナです。 そのため、Servlet コンテナのリクエスト処理に文字化け対策が行われている必要があります。

Servlet コンテナが Tomcat である場合、server.xml (Tomcat の設定ファイル) の <Connector> 要素に属性 URIEncoding を利用してリクエスト処理のエンコーディングを指定します。

	<Connector port="8080" URIEncoding="UTF-8" />
		

by Nobuyuki KANEKO