2011年06月21日

OutputStreamWriterやOutputStreamReaderの罠?

Javaにて、テキストファイルをオープンする際、最近ではUTF-8で記載したファイルも開くため、エンコーディングを事前に指定することも多いと思います。
本来は勝手にエンコーディングを見つけてくれると助かりますが、そういう処理も重たいですし、何よりプロジェクトにおいては共通化することがポイントだったりしますので、全体の決めでUTF-8とすることも多いでしょう。
もしかしたら、下記の内容、私のJavaの不勉強さで間違ったことを記載しているかもしれません。
その際はご容赦ください。


そういった場合に例えばPrintWriterを利用する場合、簡単には以下のように記載します。

PrintWriter pw = null;

try {
pw = new PrintWriter("ファイル名", "UTF-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (pw != null) {
pw.close();
}
}

このように実行した場合、PrintWriterのコンストラクタは以下のコードが実行されます。

public PrintWriter(String fileName, String csn)
throws FileNotFoundException, UnsupportedEncodingException {

this(new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(fileName), csn)), false);
}

※ 出典はJDK1.6 Update 26のソースコード(Copyright (c) 2006, Oracle)です。Oracle様、記載し申し訳ございません。

ポイントは、csnに変なエンコードを入れた場合、UnsupportedEncodingExceptionが出ることです。
この例外が出る前にはFileOututStreamの処理は成功しておりインスタンスは生成済みです。
その後、OutputStreamWriterのコンストラクタで例外が発生します。
その場合、FileOutputStreamのインスタンスはどうなってしまうのでしょうか。。。
普通はそのうち運がよければガベージコレクションが発動すると思います。
ガベージコレクションが発動すればfinalize内にclose()呼び出しがあるので、そこで確保されたリソースは解放されます。が、逆にガベージコレクションが発動されるまで開放されません。
何かしらのリソースをロックしてしまう可能性がありますので、注意する必要がありそうです。


普通は文字コードの設定などはプロジェクトの初期に行ってしまい、変更はしないものです。
ですが、ふとした作業(例えば変更してはいけないところを誤って変更していたなんてこともないとは言い切れないのが、この業界です)で、エンコーディングの指定を存在し得ないものにしていたなんてこともあるでしょう。通常はUnsupportedEncodingExceptionがスローされて、それをキャッチして異常終了させるとは思います。ですが、フレームワークによっては、そういった例外をもみ消して縮退運転的なことをさせることはないでしょうか?
それも普通はシステムの異常なのだから一度は止めてということもあるでしょうが、代替手段で行い、メインサーバは停止できないこともあります。私が関わっている案件はまさに止めるわけにはいかないシステム(止めてもいいけど、その後のリカバリが遅れたら〇〇億円とかになったらとか思うと。。。)です。。。

もみ消した場合は、ファイルがロックされたままだったり、利用しているStreamの種類によってはJNIの外でメモリリークも発生し、最悪はOutOfMemoryErrorとなり得ます。

なので、面倒なのですが私は必ずこのように記載するようにしています。


public void sample(File file, String encoding) {
PrintWriter pw = null;
BufferedWriter bw = null;
OutputStreamWriter osw = null;
FileOutputStream fos = null;

try {
fos = new FileOutputStream(file);
osw = new OutputStreamWriter(fos, encoding);
bw = new BufferedWriter(osw);
pw = new PrintWriter(bw);
} catch (Exception e) {
e.printStackTrace();
} finally {
close(pw, bw, osw, fos);
}
}

// 下記は本当はF/W側共通メソッドにしています。
public void close(Closeable... closeables) {

if (closeables == null) {
return;
}

for (Closeable c : closeables) {
if (c != null) {
try {
c.close();
} catch (Throwable e) {
}
}
}
}

こうすることで、実は例外発生時のスタックトレースも分割されるので、若干の処理速度とメモリ効率を犠牲にして、障害回復のしやすさを優先しています。
同じことはOutputStreamReaderでも言えると思います。

あとは、全てのJVMでfinalize()メソッドが確実に呼ばれるかどうか分かりませんので、確実にclose()するよう心がけておけばよいと考えます。
posted by Kiruahさん at 00:12| Comment(0) | TrackBack(0) | ノウハウ