WindowsのコマンドプロンプトでGrails 3開発する(エンコーディング地獄編)
どうも、ひさしぶりにはてダで書きます。
昨今、ソースコードはUTF-8で書くのがデファクトだと思うんですが、WindowsでのJavaはデフォルトエンコーディングが相変わらずwindows-31jなので、何をするにも明示的にエンコードを指定しないといけません。 あと、コマンドプロンプトはデフォルトでUTF-8表示できないので、実動作はOKだけど出力が文字化けする、みたいな話もあって、結局どうしたらいいんだっけ、というのをGrails 3向きに整理してみました。
普段Windowsで開発していないので、もっといい方法があったら是非教えてください。 ちなみに、Windowsのバージョンは未だ7です。
Windows 10のbashがきたら、LANGとかうまくJavaに伝わっていい感じにデフォルトエンコーディング問題は解消するんですかね。どうなんですかね。
結論 (2016/06/03時点)
仕込み
以下を設定する。前者はプロジェクトに対して1回、後者は環境に対して1回設定すればOK.
- gradlew.bat内のjava起動行の末尾に
"-Dfile.encoding=UTF-8"
(ダブルクォートがポイント)を追加する。
74: @rem Execute Gradle 75: "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% "-Dfile.encoding=UTF-8"
- 環境変数
GRAILS_OPTS=-Dfile.encoding=UTF-8
をグローバルに設定する(コンパネ等)。
開発時
コマンドプロンプト上でアプリを実行する場合は、gradlewコマンドを使う。
gradlew.bat bootRun
テストをする場合もgradlewがよい。
gradlew.bat test
create-xxxxなどを使う場合は、grailsコマンドを使う。
grailsコマンドを使ってアプリ実行やテストをする場合は、コマンドプロンプトへの標準出力が文字化けするが気にしなければOK。 文字化けした内容が気になるのであればgradlewで再実行すれば良い。
ちなみに、chcp 65001
を使ってコマンドプロンプト自体をUTF-8表示可能なようにカスタマイズすれば、grailsコマンドonlyでうまいこといける気はするが、色々ググったり試したりしてもいまいちいい感じに設定できなかったため、ひとまず上記での運用を勧めてみる。
試行錯誤編
- 注意事項
gradlewコマンド編
(A) 普通に起動する
gradlew clean gradlew bootRun
- →×
- applicaiton.ymlに日本語がある場合起動時にエラーになる
- ※実行時エラーではあるが、ビルド時に誤ったエンコーディング(windows-31j)でビルドされていると、実行時に正しいUTF-8を指定してもNGとなる(原因は後述)。
- applicaiton.ymlに日本語がある場合起動時にエラーになる
... Caused by: java.nio.charset.MalformedInputException: Input length = 2 at java.nio.charset.CoderResult.throwException(CoderResult.java:281) at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:339) at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178) at java.io.InputStreamReader.read(InputStreamReader.java:184) at org.yaml.snakeyaml.reader.UnicodeReader.read(UnicodeReader.java:123) at java.io.Reader.read(Reader.java:140) at org.yaml.snakeyaml.reader.StreamReader.update(StreamReader.java:184) ... 56 more :bootRun FAILED
(B) コマンドラインでエンコーディング指定する
gradlew clean gradlew bootRun -Dfile.encoding=UTF-8
- →○
- ログファイル文字化けなし=アプリの動作としてOK
- ※今回、実際アプリの挙動確認の代わりに手っ取り早くログファイルでの文字化け有無でアプリが正常にビルド&実行されているかを確認した
- コマンドプロンプト標準出力文字化けなし=開発しやすい
- ログファイル文字化けなし=アプリの動作としてOK
(C) 環境変数でエンコーディング指定する
JAVA_OPTS
などでもよいが、すべてのJVM起動時にフックできてかつ Pickup..
というメッセージが出力されて効いていることがわかりやすいため、undocumentedではあるが、_JAVA_OPTIONS
を使っている。(このような試行錯誤では、設定したと思っても設定方法自体が間違っていて効いていない、という何を確認しているのだかわからない残念な事故が多発するので、確実に効いて確実に確認できる方法がよい。)
gradlew clean export _JAVA_OPTIONS=-Dfile.encoding=UTF-8 gradlew bootRun
- →△
- ログファイル文字化けなし=アプリの動作としてOK
- コマンドプロンプト標準出力文字化けあり=開発しづらい
(D) gradlew.batを直接変更してエンコーディング指定する
gradlew.bat内のjava起動行の末尾に "-Dfile.encoding=UTF-8"
(ダブルクォートがポイント)を追加する。
74: @rem Execute Gradle 75: "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% "-Dfile.encoding=UTF-8"
gradlew clean gradlew bootRun
- →○
- ログファイル文字化けなし=アプリの動作としてOK
- コマンドプロンプト標準出力文字化けなし=開発しやすい
gradlew編のまとめ
基本は(B)でよい。 ただし、毎回毎回打つのが大変なのでサボりたくなるのが正しい開発者の姿。 しかし、安直に(C)のようにしても、挙動としては正しいのだが、コマンドプロンプト上の出力が化けるので若干使いづらい。
結局、(D)が良さそう。
(D)におけるポイントは、ダブルクォート。
コマンドライン引数で指定した場合とまったく同じように動作させるには、このダブルクォートが必要。
一つ前のように、環境変数で直接 -Dfile.encoding=UTF-8
を評価してしまうと、Gradleプロセス自体の文字エンコーディングがUTF-8になってしまう。
一般的にはそれで良いのだが、そうなるとGradleはコマンドプロンプトへの標準出力もUTF-8として出力しようとしてしまうため、結果としてコマンドプロンプト上の出力のみ化けた状態になってしまう。
ダブルクォートすることで、あえてGradleのメインプロセス自体はWindows標準のwindows-31jとして動作させつつ、Gradleプロセス内部でキックされるワーカプロセス(コンパイル用ワーカプロセス、Grailsアプリプロセス等)にのみ -Dfile.encoding=UTF-8
を渡してUTF-8として動作させる。
こうすることで、実際のワーカプロセスの動作はUTF-8となりつつ、Gradleがワーカプロセスの標準入出力とコマンドプロンプトへの橋渡しをするときに適切な変換を行うことで(要出典)、コマンドプロンプト上で文字化けせずに、しかも、ANSIカラー対応の出力すら可能となっている(※)。
※動作上はこのようにみえるのですが、実装詳細については確認できていないので、あくまで想像です。間違ってたらすいません。
grailsコマンド編
(A) 普通に起動する
grails clean grails run-app
- →×
- gradlew編(A)と同様に、applicaiton.ymlに日本語がある場合起動時にエラーになる
(B) コマンドラインでエンコーディング指定する
grails clean grails run-app -Dfile.encoding=UTF-8
- →×
- ログファイル文字化けあり=アプリの動作としてNG
- コマンドプロンプト標準出力文字化けあり=開発しづらい
gradlewでは効果があったコマンドライン引数であるが、ビルド時には効果があるようだが(日本語含みのapplication.yml起因のエラーが出ていない)、実行時には効いていないようだ。
(C) 環境変数_JAVA_OPTIONSでエンコーディング指定する
grails clean set _JAVA_OPTIONS=-Dfile.encoding=UTF-8 grails run-app
- →△
- ログファイル文字化けなし=アプリの動作としてOK
- コマンドプロンプト標準出力文字化けあり=開発しづらい
ビルドワーカプロセスにも実行ワーカプロセスにも確実に効くように、_JAVA_OPTIONS
を使ってみたところ、期待通りアプリの動作としてはOKとなった。
しかし、コマンドプロンプトの出力が文字化けするので、gradlewの(B)と比べると若干負けた気分。
(D) grails.batを直接変更してエンコーディング指定する
インストール済みのgrails.batをいじるのであまりお勧めはできないが、gradlewでの成功体験から試してみる。
$GRAILS_HOME/bin/grails.bat内のjava起動行の末尾に "-Dfile.encoding=UTF-8"
(ダブルクォートがポイント)を追加する。
74: @rem Execute grails 75: "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRAILS_OPTS% -classpath "%CLASSPATH%" org.grails.cli.GrailsCli %CMD_LINE_ARGS% "-Dfile.encoding=UTF-8"
grails clean grails run-app
- →×
- ログファイル文字化けあり=アプリの動作としてNG
- コマンドプロンプト標準出力文字化けあり=開発しづらい
まったく無力だった。
(E) 環境変数GRAILS_OPTSでエンコーディング指定する
_JAVA_OPTIONS
は強すぎるので、もう少し弱い環境変数ではどうか。うまいことバランスとれたりしないか。
grails clean set GRAILS_OPTS=-Dfile.encoding=UTF-8 grails run-app
- →△
- ログファイル文字化けなし=アプリの動作としてOK
- コマンドプロンプト標準出力文字化けあり=開発しづらい
やはりこんなところか...。
grails編のまとめ
grailsコマンドを使う場合は、コマンドプロンプト上の文字化けには目をつむって、(C)か(E)を採用するぐらい。
ただし、(C)の _JAVA_OPTIONS
を使ってしまうと、gradlew編の(C)の条件を満たしてしまい、gradlewを使った場合にもコマンドプロンプト出力が文字化けしてしまう。
よって、影響がgrailsコマンドに対して局所的になるように(E)の GRAILS_OPTS
を使った設定をするのがバランスが良いと思う。
MalformedInputExceptionの原因を探ってみる
※gradlewでの仕込みをいったんなくしてデフォルト状態に戻しておくこと
実行時のみエンコーディング指定
gradlew clean classes gradlew bootRun -Dfile.encoding=UTF-8
... Caused by: java.nio.charset.MalformedInputException: Input length = 2 at java.nio.charset.CoderResult.throwException(CoderResult.java:281) at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:339) at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178) at java.io.InputStreamReader.read(InputStreamReader.java:184) at org.yaml.snakeyaml.reader.UnicodeReader.read(UnicodeReader.java:123) at java.io.Reader.read(Reader.java:140) at org.yaml.snakeyaml.reader.StreamReader.update(StreamReader.java:184) ... 56 more :bootRun FAILED
application.ymlに日本語が含まれる場合、このようにエラーになる。
ためしに、build/resources/application.ymlの日本語コメントを削除してからbootRunすると...と試そうとして気づいた、このファイル....壊れてやがる!!!
diffをとってみると、 日本語部分が化けて制御文字含みになってしまっている のがわかる。 実行時にエラーになるのはコレが原因だったのだ!
$ diff grails-app/conf/application.yml build/resources/main/application.yml 10,12c10,12 < name: '@info.app.name@' < version: '@info.app.version@' < grailsVersion: '@info.app.grailsVersion@' --- > name: 'myapp' > version: '1.0' > grailsVersion: '3.1.7' 71c71 < # 空文字は空文字、nullはnullとして扱う --- > # 空▒?字▒?▒空▒?字▒?▒nullはnullとして扱▒? 73c73 < # 前後の半角スペースのトリムはしない --- > # 前後▒?▒半角スペ▒?▒スのトリ▒?はしな▒?
他の差分箇所を見ればわかるが、アプリ名などをビルドによって置換されている。 ファイルをそのままコピーするのではなく、いったん内容を解釈して文字列置換をしているのだ。
つまり、ビルド時に適切なデフォルトエンコーディングを指定しない場合、実際はUTF-8であるgrails-app/conf/application.ymlをwindows-31jとして読み込んで置換処理してしまい、結果としてマルチバイト部分が壊れてしまうことになる。 実行時に正しくデフォルトエンコーディングをUTF-8指定しても、すでに壊れたファイルを読み込むことになるため、当然エラーになる。
結局、実行時だけではなく、ビルド時にも適切なデフォルトエンコーディングを効かせないといけないよ、という話であり、まあ、当たり前の話なのであった。
(参考)ビルド時のみエンコーディング指定
gradlew clean classes -Dfile.encoding=UTF-8 gradlew bootRun
コマンドプロンプト上では一件正常に起動して、文字化けもせずにログ出力できているように見える。
しかし、ログファイルへの出力は文字化けしている。おそらく画面も文字化けする。
実行時デフォルトエンコーディングも当然重要ですよ、という話。