読者です 読者をやめる 読者になる 読者になる

WindowsのコマンドプロンプトでGrails 3開発する(エンコーディング地獄編)

どうも、ひさしぶりにはてダで書きます。

昨今、ソースコードUTF-8で書くのがデファクトだと思うんですが、WindowsでのJavaはデフォルトエンコーディングが相変わらずwindows-31jなので、何をするにも明示的にエンコードを指定しないといけません。 あと、コマンドプロンプトはデフォルトでUTF-8表示できないので、実動作はOKだけど出力が文字化けする、みたいな話もあって、結局どうしたらいいんだっけ、というのをGrails 3向きに整理してみました。

普段Windowsで開発していないので、もっといい方法があったら是非教えてください。 ちなみに、Windowsのバージョンは未だ7です。

Windows 10bashがきたら、LANGとかうまくJavaに伝わっていい感じにデフォルトエンコーディング問題は解消するんですかね。どうなんですかね。

結論 (2016/06/03時点)

仕込み

以下を設定する。前者はプロジェクトに対して1回、後者は環境に対して1回設定すればOK.

  • gradlew.bat内のjava起動行の末尾に "-Dfile.encoding=UTF-8" (ダブルクォートがポイント)を追加する。
    • この変更は、Git/SVNにコミットしてWindows開発者向けに共有する。
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でうまいこといける気はするが、色々ググったり試したりしてもいまいちいい感じに設定できなかったため、ひとまず上記での運用を勧めてみる。

試行錯誤編

  • 注意事項
    • ビルド時のエンコーディングと、実行時のエンコーディングの2箇所で問題が発生するため、確認時は毎回クリーンアップすること(grails clean or gradlew clean)。そうしないと、問題が切り分けられない。
    • あと、環境変数は設定したら、次のを試す前にクリアすること。(set HOGE= みたいに値を空にすればOK)

gradlewコマンド編

(A) 普通に起動する

gradlew clean
gradlew bootRun
  • →×
    • applicaiton.ymlに日本語がある場合起動時にエラーになる
      • ※実行時エラーではあるが、ビルド時に誤ったエンコーディング(windows-31j)でビルドされていると、実行時に正しいUTF-8を指定してもNGとなる(原因は後述)。
...
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
      • ※今回、実際アプリの挙動確認の代わりに手っ取り早くログファイルでの文字化け有無でアプリが正常にビルド&実行されているかを確認した
    • コマンドプロンプト標準出力文字化けなし=開発しやすい

(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

コマンドプロンプト上では一件正常に起動して、文字化けもせずにログ出力できているように見える。

しかし、ログファイルへの出力は文字化けしている。おそらく画面も文字化けする。

実行時デフォルトエンコーディングも当然重要ですよ、という話。