Javaで日付の文字列をDate型に変換する(またはその逆)際にSimpleDateFormatクラスはよく使われると思います。
このクラス。スレッドセーフでないのをご存知で使われてますか? 私はほぼ(というか全然)気にしておりませんでしたが、そのためマルチスレッド環境下では障害の原因となります。
事の始まり
テスト環境のテーブルで日付項目に2160年のデータが見つかりました。
そのカラムは処理日時を保持する仕様です。正しくは2016年のデータ。100年も未来ですね。
日付はクライアントからRESTful APIにて文字列で渡ってきて、サーバー側でDate型に変換→ビジネスロジックを経て→テーブルへ保存します。
ログを調べてみるとクライアントからのデータは正しく2016年となっていました。
ただ、テーブルへのinsertしているログでは2160年になっていました。
原因
調査してみるとSimpleDateFormatをマルチスレッド環境下で1インスタンスを使っていたことが判明
調査
調査のためサンプルコードを書いてみました。
同一インスタンスのSimpleDateFormatクラスを100スレッド並行で動かして、日付文字列をDate型に変換する。
SimpleDateFormatMulti.java
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
public class SimpleDateFormatMulti {
public static void main(String[] args) {
SimpleDateFormatMulti sdfm = new SimpleDateFormatMulti();
sdfm.go();
}
public void go() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
for (int i = 0; i < 1000; i++) {
new SubThread(sdf).start();
}
}
}
class SubThread extends Thread {
SimpleDateFormat sdf;
SubThread(SimpleDateFormat sdf) {
this.sdf = sdf;
}
public void run() {
try {
Date date = sdf.parse("20160102030405");
Calendar cal = Calendar.getInstance();
cal.setTime(date);
System.out.print(String.format("%04d/%02d/%02d %02d:%02d:%02d",//
cal.get(Calendar.YEAR),//
cal.get(Calendar.MONTH+1),//
cal.get(Calendar.DAY_OF_MONTH),//
cal.get(Calendar.HOUR_OF_DAY),//
cal.get(Calendar.MINUTE),//
cal.get(Calendar.SECOND)));
if ((cal.get(Calendar.YEAR) == 2016) && //
(cal.get(Calendar.MONTH+1) == 1) && //
(cal.get(Calendar.DAY_OF_MONTH) == 2) && //
(cal.get(Calendar.HOUR_OF_DAY) == 3) && //
(cal.get(Calendar.MINUTE) == 4) && //
(cal.get(Calendar.SECOND) == 5)) {
System.out.println("==OK!");
} else {
System.out.println("==NG!");
}
} catch (ParseException e) {
System.out.println("ParseException!");
} catch (Exception e) {
System.out.println("Exception!");
}
}
}
変換する日付文字列は"2016/01/02 03:04:05"
変換結果をsysoutしてみたら、Exceptionも多く発生しましたが中にはまったく違う日時に変換されたケースも
下記の3行目が2015/01/31と変換されてしまっている。
:
2016/01/02 03:04:05==OK!
2016/01/02 03:04:05==OK!
2015/01/31 03:04:05==NG!
2016/01/02 03:04:05==OK!
:
さて、ここからが本題ですが、サーバーのFWはDIコンテナ(Seasar2)を使っています。 問題のロジックもDIしており、日付変換のロジックをユーティリティクラスのように外だしにしてSimpleDateFormatをクラス変数で定義していました。
こういったケースよくあると思います。
DIコンテナにインスタンスの生成を任せているため結果的に同一インスタンスでマルチスレッド。という状況になるわけです。
また、たちが悪いのがExceptionで失敗すればいいのですが、上記調査結果のようにまれに日付として正しい変換になったりします。
つまり、知らないうちに間違った日付がinsertされていく・・・・
怖いですね。
たぶん気づくのはそれを端緒にした二次不具合だと思うので。
スレッドセーフ気をつけましょう。