~saiya/hatenablog

No Code, No Life.

タイムゾーンを考慮した日時の扱いのベストプラクティス

www.m3tech.blog

タイムゾーンについて盛り上がっている気がしたので書いてみました!

国家的祭典のために急にサマータイムが導入されるといった話に限らず、 クラウドサービスが UTC+0 の日時になっているがユーザー層は日本時間である、といった理由でも タイムゾーンUTC オフセット (時差) を扱う必要性のある時代ですので、ご参考にしていただければ。

Logger を Spring に DI させれば同じようなコードを毎回書かなくて済むしテスト記述性も改善できる

SLF4J などの Logger を使うときに、毎回以下のようなボイラープレートを書くのにうんざりしていませんか? 少なくとも私は大変うんざりしています:

class TestController {   // Kotlin Example
    companion object {
        private val log = LoggerFactory.getLogger(TestController::class.java)
    }   // 長くてつらい...
}
class TestController {   // Java Example
    private static final Logger LOG = LoggerFactory.getLogger(TestController.class);
    // これは 1 行だが、とはいえ長いし class 名を二度書くのが辛い & コピペ間違いも起こしやすい
}

夜中に寝苦しくて眠れない時などに趣味で Kotlin + spring-boot 2.0 + その他諸々 にチャレンジしているのですが、その際に上記の悩みを Spring の DI で解消してみました (その結果としてテスト記述性も良くなりました) という久々の記事です。

なお、この記事は上記の通りの趣味的な都合により Kotlin, Spring メインの記述となっていますが、Java, Scala や Spring 以外の DI ツールでも類似・同様のことはおそらく可能ではないでしょうか (未検証)。

調べた限り世の中でも知られている苦労らしい

KotlinでLoggerの生成を少し楽にした - Qiita, Best practices for loggers - Kotlin Discussions などにおいて記述を多少省略する試みは広く行われているようです (Kotlin)。

しかし、数行にわたる記述を毎回書くという点は変わらない様子です。

では、どうするか

Spring の DI の一般的手法として、constructor injection (DI してほしいものをコンストラクタ引数で宣言する)という手法があります。

それを使い、かつ Kotlin の primary constructor で property を宣言する構文を用いることで、こんな風にできました:

@Controller
class TestController (val log : Logger) {   // ここで Logger をもらってしまう
    fun example() {
        log.info("It works")   // ↑の記述だけで Logger を使える
    }
}

Java では出来るもののトレードオフがあります、詳しくは後述。

Logger を DI するための下準備

上記のように Logger を受け取るコードをいきなり書くと「 Loggerインスタンスなんて見つからんぞ!」と Spring に怒られるはずです。 そのため「DI 先の class に応じた Logger インスタンスを作って DI する」ための仕込みが必要であり、それは以下のようにして実現できます:

@Configuration
class LoggerInjectionConfig {     // この class を component-scan で見える場所に置いておくか @Import で明示的に読み込む

    @Bean
    @Scope("prototype")
    fun logger(ip : InjectionPoint): Logger {    // Logger を DI する都度に呼ばれる
        return LoggerFactory.getLogger(
                ip.methodParameter?.containingClass
                        ?: ip.field?.declaringClass
                        ?: throw BeanCreationException("Cannot find type for Logger")
        )
    }
}

// Java 版は(めんどいので)書いてないですが、上記のコードの雰囲気で伝わるのではないかなぁ...

上記のように InjectionPoint なるものに DI 先の情報が渡ってくるので、それを使って Logger を生成する実装をしておいてやれば、先のように Logger を DI することが可能になります。

何がうれしいか

Logger を使う記述が簡潔になる (※Javaでは微妙な点あり)

@Controller
class TestController {   // 本手法の導入前
    companion object {
        private val log = LoggerFactory.getLogger(TestController::class.java)
    }
}

@Controller
class TestController (val log : Logger) {    // 本手法
}

と、大変簡潔になります。

Logger を使う処理というのは少なからず書くことがあるので、これによる頭の負荷の軽減は、上記の InjectionPoint 云々の実装を入れるには十分見合うものではないでしょうか。

ただし、Java の場合、コンストラクタ引数で受け取った Logger をフィールド変数に格納して...という一連の記述が冗長になってしまいます。そのため、Java で記述の簡潔さを優先する場合、field injection (フィールドに @Autowired を付けてフィールドに直接 DI する)を用いることになりますが、field injection は後述するテスト上のメリットが得られないといった負の側面があり近年の Spring の潮流としてもあまり推奨されていない、というトレードオフがあります。いっそ Lombok を使ったほうが良いかもしれませんね...。

public class TestController {
     
     @Autowired    // field injection すればこの記述だけで良いが、後述するテスト上のメリットが得られない点が微妙
     private Logger log; 


     public TestController(Logger log){
          this.log = log;
     }
     // かといって constructor injection すると、Kotlin と違って簡潔に記述できない
     // ( 記述の短縮どころか、伝統的な Logger の初期化方法より長くなっている )
}

ログ出力に対するテストがしやすくなる

そもそもの Spring などの DI を利用する一般的なメリットとして「DI するオブジェクトを差し替えることで、コードの再利用やテストがしやすくなる」というメリットがあるはずです。

しかし、一般的な Logger のイディオムは全くもって DI していないので、上記のメリットが得られなくなってしまっていないでしょうか?

例えば、以下のようなニーズを想定してみます:

  • 大事なログを出す処理なので「ログが期待通りに出ていること」をテストで assert したい
  • パラメタに渡した機密の情報(例えばクレジットカード番号とか)が「あらゆるログに一切出ていないこと」をテストで assert したい
  • ログに興味がないテストケースなので、ログは単に破棄したい
  • 上記のような制御を、設定ファイル決め打ちなどではなく、テストケースごとにプログラム記述で制御したい

一般的な Logger のイディオムを使っている場合、 Loggercompanion object / static final で決め打ちされてしまっているため、上記のようなニーズを満たすことが難しくなります。

リフレクションを用いて強引に Logger を差し替えるのは明らかに保守性も実装効率も悪いですし、かといって SLF4J や Log4j のバックエンドにまるまる自作の実装を差し込んで...といった手法では実装やメンテナンスに苦労が伴うことでしょう。

一方で、今回の手法を用いて Logger を DI するように実装していれば、spring-boot の @MockBean といった DI・モックツールの機能をフルに活用して何とでもできます。

上記の事例で示したようにログについてテストをしたい局面がいつ来るかは分からないことなので、当該手法を予め仕込んでおくことも個人的にはおすすめです。仕込みが無い状態で「クレジットカード番号があらゆるログに一切出ていないこと」というテストを後から実装するのは、概して苦労が伴うものと思われます。

重箱の隅な想定議論

ここから先は細かいことが気になる人向け。

続きを読む

英字配列キーボードな Mac でのキーボード設定 (with Windows VM)

Macbook を買う際には英字配列キーボードにしているのだが、その際のキーボード設定のやり方を毎回忘れてしまうのでメモがてら記事にした。

この設定をすることで、英字キーボードでありながら、日本語入力の On/Off を素早く切り替えることが可能になる。

以下の手順は Mac OS XWindows VM (VMWare Fusion) 環境で実践済みの手順だが、Mac/Windows 単独といった別の環境でも有用なはず。

この設定で実現されること

  1. 左 Command を「英数」、右 Command を「かな」キーとして使う
    • ただし、Command + 何かのキー の場合には普通の Command キーとして機能する
    • また、VM 系のソフトに対してのみはこのマッピングを適用しない
  2. Windows VM 側では右 Alt を「全角/半角」キーとして使う
    • VM 利用中は Command キーをそのまま使える (Windows キー扱いになる)

なお、もっと凝った設定をすることでより改善することもできるが、ここでは比較的手間のかからない方法を採用している。

1. 左 Command を「英数」、右 Command を「かな」キーとして使う

Karabiner (旧名 KeyRemap4MacBook) を使う。

Karabiner には「左右のコマンドキー(⌘)を「英数/かな」としても使う」という大変便利なキーマッピング設定がデフォルトで用意されており、VM を使わないならばこれで十分である。

しかし、Windows VM を使っている際にこの設定が適用されると残念なことになる*1ため、以下のようにして VM 系のソフト以外のみに上記設定を適用する:

  1. Karabiner (旧名 KeyRemap4MacBook) をインストール
  2. Karabiner の設定画面を開く
  3. "Misc & Uninstall" タブを開く
  4. "Open private.xml" し何らかのテキストエディタxml を開く
  5. 後述の XML をペーストし保存
  6. "Change Key" タブを開く
  7. "Reload XML" ボタンを押す (エラーになった場合 4 での XML 編集からやり直す)
  8. 検索ボックスに "コマンドキー" と入れて検索
  9. "コマンドキーの動作を優先モード v1 (VM 対策版)" または "コマンドキーの動作を優先モード v2 (VM 対策版)" を有効にする

v1 と v2 どちらにするかはお好みで。 個人的には、v1 の方がさくさく操作できるのだが、稀に誤爆して辛いので v2 のほうが無難*2という印象。

以下 private.xml の中身:

<?xml version="1.0"?>
<root>
  <item>
    <name>For Japanese</name>
    <item>
      <name>左右のコマンドキー(⌘)を「英数/かな」としても使う (VM 対策版)</name>
      <appendix>(左コマンドキーを英数キーにする)</appendix>
      <appendix>(右コマンドキーをかなキーにする)</appendix>

      <item>
        <name>コマンドキーの動作を優先モード v2 (VM 対策版)</name>
        <not>VIRTUALMACHINE</not>    <!-- ここが増えてるだけ -->
        <appendix>(コマンドキーの空打ちで「英数/かな」)</appendix>
        <appendix>(コマンドキーを押している間に他のキーを打つと通常のコマンドキーとして動作。</appendix>
        <appendix>ただし、他のキーを離す前にコマンドキーを離した場合は「英数/かな」として扱う。)</appendix>
        <identifier>remap.japanese_command2eisuukana_prefer_command_v2.nonvm</identifier>
        <autogen>
          __KeyOverlaidModifier__
          KeyCode::COMMAND_L,
          KeyCode::VK_CONFIG_SYNC_KEYDOWNUP_notsave_japanese_command2eisuukana_prefer_command_v2, ModifierFlag::COMMAND_L,
          KeyCode::JIS_EISUU,
        </autogen>
        <autogen>
          __KeyOverlaidModifier__
          KeyCode::COMMAND_R,
          KeyCode::VK_CONFIG_SYNC_KEYDOWNUP_notsave_japanese_command2eisuukana_prefer_command_v2, ModifierFlag::COMMAND_R,
          KeyCode::JIS_KANA,
        </autogen>
      </item>

      <item><name>──────────────────────────────</name></item>
      <item>
        <name>コマンドキーの動作を優先モード v1 (VM 対策版)</name>
        <not>VIRTUALMACHINE</not>    <!-- ここが増えてるだけ -->
        <appendix>(コマンドキーの空打ちで「英数/かな」)</appendix>
        <appendix>(コマンドキーを押している間に他のキーを打つと通常のコマンドキーとして動作)</appendix>
        <identifier>remap.jis_command2eisuukana_prefer_command.nonvm</identifier>
        <autogen>__KeyOverlaidModifier__ KeyCode::COMMAND_L, KeyCode::COMMAND_L, KeyCode::JIS_EISUU</autogen>
        <autogen>__KeyOverlaidModifier__ KeyCode::COMMAND_R, KeyCode::COMMAND_R, KeyCode::JIS_KANA</autogen>
      </item>
    </item>
  </item>
</root>

2. Windows VM 側では右 Alt を「全角/半角」キーとして使う

AutoHotkeyを使う。

これによって右 Alt で 全角/半角 操作が可能になる。

先述の設定で Mac 側では左右の Command で英数/かなを切り替えており、それと同じキーにならない点は多少気持ち悪い設定ではあるが、

  • スペースバーの横のキー(Command)は Windows キーにしておきたい
  • "全角/半角" と "英数", "かな" は別物でもいいのではないか (キーの数からして違うし) *3

という理由で筆者は VM では右 Alt にしている。

AutoHotkey の設定方法は以下:

  1. AutoHotkey をインストール
  2. スタートメニュー > すべてのプログラム > スタートアップ
  3. スタートアップを右クリックし「開く (O)」
  4. メモ帳等で "RAlt::vk19" (引用符は不要)とだけ書いたファイルを書く
  5. "右Altを全角半角にする.alt" といった名前で上記フォルダに保存
  6. ダブルクリックで "右Altを全角半角にする.alt" を実行

これで、Windows VM 側では右 Alt で IME を切り替えることができるようになる。

*1:Windows キーと IME の切り替え操作がバッティングする。例えば VMWare Fusion の場合なんと Windows キーと全角/半角キーをを同時に押した扱いになる。

*2:例えば「かな」キーの直後に W を押す操作をした場合、タイミング次第では v1 だと Command + W 扱いになってしまい作業中のウインドウを閉じることになる

*3:IME 側の設定も含めて頑張れば 英数, かな キー操作を Windows 側でも出来る。が、めんどいので最近そこまではやってない。

ENV.fetch のすすめ

Ruby にて環境変数の値を使う場合には ENV.fetch を使うと良いのではないか、という小ネタ。

そうすることで環境変数が存在しないエラーを早期に検知できる上、エラーの根本原因が例外に反映されるので分かりやすくなる。

この2行だけでほぼ言い尽くしてしまっているが、以下詳細説明:

ENV を使う背景

環境変数を読む理由は色々ありうるが、特に Heroku や Docker などの近代的な PaaS 環境・コンテナ環境において環境変数を読むことが多い。

例えば Ruby on Rails アプリで DB に接続する場合、接続先の DB のホストやユーザー情報を database.yml に決め打ちするのではなく、Heroku デプロイ時や Docker コンテナ起動時に指定したいということが多いためである。

また、Ruby on Rails では database.yml などの各種設定ファイルにて erb を使えるため、設定ファイル内で気軽に ENV を読めるという背景もある *1

ENV[...] より ENV.fetch

上記のような場合に

mysql:
  host: <%= ENV['MYSQL_HOST'] %>

のような書き方がよく見られるが、これは

mysql:
  host: <%= ENV.fetch('MYSQL_HOST') %>

の方がベターではないかというのが本エントリの趣旨。

読もうとしている環境変数が存在する場合は ENV[...] と ENV.fetch(...) は全く同じ振る舞いである。

しかし環境変数が存在しない場合、ENV[...] は nil になる*2が、fetch であれば設定ファイルの読み込み時点で以下のように例外が上がる。

Cannot load `Rails.application.database_configuration`: key not found: "MYSQL_HOST"

このように、あるべき環境変数が無いというエラーを早期に検知でき、かつエラー内容も分かりやすくなるのがこの手法のメリットである。

デフォルト値

なお、環境変数が無かった場合のデフォルト値を指定したい場合

mysql:
  host: <%= ENV.fetch('MYSQL_HOST', 'localhost') %>

のようにすれば良い。

このようにすると、環境変数が存在しなければ第二引数(上の例では localhost)が使われ、例外は発生しない。

*1:ERB.new(ファイル内容).result という風にすれば自前の設定ファイルに対しても erb を使える。Rails も内部ではこのようにしている。

*2:そのため、設定値を使う側でエラーになってしまい、エラー原因をたどる面倒が発生する

MySQL Connector/J (JDBC ドライバ)の罠まとめ

MySQL JDBC ドライバ(MySQL Connector/J)、JavaMySQL といえばまずコレだが、これまた地味に罠が多い(そして多くの人が踏んで苦しむ)のでまとめてみた。

(2015/03/19) こちら のコメント欄でご指摘ただいた wait_timeout の件について記事修正いたしました。

Summary

以下、いずれもプログラム設計時に理解しておかないと、開発中は大丈夫そうでも実用した途端に苦しまされれてしかも設計から治す羽目になる要注意な罠である:

  • SELECT 結果は全部メモリに載ってしまう (デフォルト設定で)

    • 大量 SELECT する場合は FetchSize, ResultSetType を要設定
      • 利用時には制約があるので、設計段階から考慮しなければならない (後述)
  • idle 時間の「合計で」コネクションが切られる 前回のクエリ処理から一定時間以上経過するとコネクションが切られる

    • Connector/J 限定ではないが, これも設計時点で要考慮
  • クライアントサイド プリペアドステートメントになっている (デフォルト設定で)

    • パフォーマンス向上目的に使えるものではない
    • ただし SQL インジェクション対策というセキュリティ目的では使うべき
  • 裏で勝手にスレッドが走っている -> そしてメモリリーク

    • tomcat など Servlet 環境で、デプロイ時のリソースリークの原因に

SELECT 結果は全部メモリに載ってしまう

デフォルトにより、ResultSets は完全に摘出され、メモリに保存されます。 ほとんどの場合において、これは最も効果的な操作方法であり、MySQL の設計により、ネットワーク プロトコルはより簡単に実装できます。 http://dev.mysql.com/doc/refman/5.1/ja/connector-j-reference-implementation-notes.html

MySQL Connector/J は、デフォルトで SELECT 結果をすべてメモリに格納してしまう。

そのため、大量のデータを処理するために SELECT するような実装を普通に書くと、確実に OutOfMemory の憂き目に・・・。

これを防ぐためには、

stmt = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_READ_ONLY); stmt.setFetchSize(Integer.MIN_VALUE); フェッチのサイズが Integer.MIN_VALUE の、前進専用、読み取り専用のコンビネーションは、行ごとに結果セットをストリームするようドライバに指示する信号として機能します。この後、このステートメントで作成された結果セットは行ごとに摘出されます。

の通りに ResultSetType や FetchSize を設定してやる必要がある。

さらに、追加の罠として、

接続にクエリを発行する前に、結果セットのすべての行を読まなければならず、さもなければ例外が投入されます。

すなわち「SELECT 結果を読み終わるか ResultSet を閉じるまでは、そのコネクションでは別のクエリを発行できないよ」制限もあるという・・・。

なので、

  • 大量データを SELECT する
  • かつ、同一トランザクションで SELECT 中に INSERT なり UPDATE なりしたい

みたいな設計は MySQL Connector/J では実現不能なので、設計段階から要注意。

このことを知らずに実装すると、全件オンメモリで SELECT して 1 件 1 件を読み取りながら INSERT/UPDATE するような実装をしてしまい、後から ResultSetType や FetchSize を直すと今度は上記制約で設計自体が破綻し...という憂き目に遭って修正者の胃が痛くなる (実体験)。

もし上記のような設計をしたくなった場合、テーブル構造を見なおすなり、一時ファイルを使うなり、あるいはヒープ容量が十分であることを確かめて使うなり、考えなおさなければならない。

前回のクエリ処理から一定時間以上経過するとコネクションが切られる

誤: idle 時間の「合計で」コネクションが切られる (少なくとも最新 MySQL 5.6 ではこうでない)

問題を発生させていたプログラムにもはや触れないため上記の誤解の理由は不明だが、少なくとも MySQL 5.6 では見出しの通りである*1

サーバー側の実装としては sql/sql_parse.cc にて以下のようになっている様子:

/**
  Read one command from connection and execute it (query or simple command).
  This function is called in loop from thread function.

  For profiling to work, it must never be called recursively.

  @retval
    0  success
  @retval
    1  request of thread shutdown (see dispatch_command() description)
*/

bool do_command(THD *thd)
{

   ( 中略 )

  /*
    This thread will do a blocking read from the client which
    will be interrupted when the next command is received from
    the client, the connection is closed or "net_wait_timeout"
    number of seconds has passed.
  */
  my_net_set_read_timeout(net, thd->variables.net_wait_timeout);

このような実装のため、「最後に何らかのクエリ処理をしてから」の経過時間でコネクション切断判定がなされている。

以下古い記述:

Connector/J 限定ではなく MySQL そのものの仕様だが、コネクションが何もクエリ処理していない時間の合計が一定時間を超えると MySQL サーバー側から接続を一方的に切られてしまう。

"mysql wait_timeout" を日本語 google でぐぐって真っ先に出てくるページが

この設定、接続のアイドル状態が一定時間続くと MySQL 側から自動的に接続を切る設定らしい。 MySQL の wait_timeout を変更。 - Qiita

と書いているため誤解されていることが多いが、接続のアイドル時間の合計であり、一定時間続くと、ではない。

よくある例として、「このバッチは断続的にクエリを発行し続けるから何時間走っても大丈夫!」とか思い込んで実装 -> 本番で wait_timeout を食らって死亡! という流れをとても良く見かけるが、実際には死んでしまう。そして、そういった実装は得てしてコネクション 1 本であること前提(トランザクションをずっと貼ってたりとか)であるため、コネクション再接続に耐えうる実装に治すためには大手術が必要になり修正者の怒りと憎しみを駆り立てる (実体験)。

以下は wait_timeout の正しい理解でも言えるはずの事柄:

また別のパターンとして、tomcat などで C3PO コネクションプールを普通に使っているとこの wait_timeout による断が検知されず、「本番稼働し始めて半日ほどすると謎の JDBC エラーが時々出る」みたいな事態になってしまい、コネクション死活監視の設定とテストを休日返上で羽目になったりする (実体験*2 )。

このような事態にならぬよう、サーバー側 wait_timeout を大きくする*3か、アプリ側でコネクションの死活チェックと再接続を前提とした設計にするよう考えておくべきである。

なお、普通は wait_timeout グローバル変数値が使われるが、interactive 扱いの接続では interactive_timeout 値が使われるので注意。どちらにせよ、コネクションはいつか切られる前提で設計・テストしたほうが良いが...。

クライアントサイド プリペアドステートメント

MySQL Connector/J 3.1.0 からは、サーバ側プリペアド ステートメントおよびバイナリ エンコードされた結果セットは、サーバがそれらをサポートする場合に使用されます。 http://dev.mysql.com/doc/refman/5.1/ja/connector-j-reference-implementation-notes.html

公式の記述が罠だが、最新の Connector/J (少なくとも 5.x) は、デフォルトでクライアントサイド プリペアドステートメントを使う設定になっている。

Connector/J 5.0/5.1のデフォルト設定では、server-sideではなくclient-sideのPrepared Statementが使用されるようになっている。 Connector/J 3.1.0にてデフォルト値はtrueで登場したものの、Connector/J 5.0.5/5.1にてfalseとなった。 Connector/J 5.1とServer Side Prepared Statement - mir the developer

そもそも、プリペアドステートメントの元々のメリットは、DB サーバー側で SQL を予め解析しておいてクエリ実行時に毎度 SQL の解析処理を行わないようにすることでクエリ速度の顕著な向上が得られる点である。しかし、クライアントサイド プリペアドステートメントは言ってしまえば Connector/J がクライアント側で SQL を組み立てているだけなので、この性能上のメリットは全くない。

なので、例えばコネクションプール機構で頑張って PreparedStatement インスタンスをキャッシュ・再利用する実装をしても速度は向上しないどころかキャッシュ処理している分だけメモリの無駄かつ低速になる(実体験)。

なお、useServerPrepStmts なるパラメタを使うことでサーバーサイド プリペアドステートメントにすることもできるが、ある程度の負荷を掛けると謎の例外が頻発するなどの有り様で中々不安定である*4。Connector/J デフォルトでサーバーサイド プリペアドステートメントを使わない設定になっているのもむべなるかな。

このような事情であるので、MySQL Connector/J では PreparedStatement を性能向上のために使おうとは思わないほうが良い。

ただし、これは重要だが、PreparedStatement を使うことはセキュリティ面では大変好ましい。SQL を自力の文字列処理で生成することは SQL インジェクション脆弱性の温床になるので、PreparedStatement のパラメタ機能を使うことはセキュリティのための至極基本的な処置である (性能面ではメリットがなくても)。

裏で勝手にスレッドが走っている -> そしてメモリリーク

この件: http://bugs.mysql.com/bug.php?id=68556

本件は日本語情報があまりないのだが、実は MySQL Connector/J は裏で "AbandonedConnectionCleanupThread" なるスレッドを勝手に走らせている。このスレッドは(ぱっとソースを見た限り)放置された Connection を close してくれる。

この親切機能の何が問題であるかというと、tomcat 環境で

  1. 勝手に AbandonedConnectionCleanupThread が起動する (WebApp クラスローダーで)
  2. アプリの war なりをデプロイし直す (別の WebApp クラスローダーが作られる)
  3. 1 で起動した AbandonedConnectionCleanupThread は元気に走り続ける
  4. 古い WebApp クラスローダーは GC されない (1 のスレッドから参照されるため)
  5. デプロイ解除(配備解除)前のアプリの全ての static オブジェクトも GC されない (クラスローダーから参照)
  6. 激しくメモリリーク

という事態を招くためである。

表面的に見える事象としては、

  • アプリの再デプロイ後すぐに or しばらくして OutOfMemoryError
  • war を配備解除するたびにメモリが減っている
  • 大量のオブジェクトがリークしている

という厄介な状態であり、しかも上記の理由でクラスローダーまるごとリークしているため、真犯人の Connector/J 以外のオブジェクトが目立ってしまい、原因究明もしづらい (特にヒープのダンプだけ見ていると原因究明がとてもむずかしい)。

tomcat への war デプロイ後に「とりあえず tomcat 再起動しないとメモリが足りなくなる」現象が起きている場合、本件を疑う価値があるであろう。

なお、MySQL 公式のバグチケットは Tomcat 7 の JreMemoryLeakPreventionListener を使えということで close されているが*5、この解決策を使うと MySQL JDBC ドライバが Common クラスローダーでロードされるため、tomcat 上の全アプリが同じ Connector/J を使うことになる (Connector/J の持っている static 変数もアプリをまたいで共有することになる)。

公式のバグチケット: http://bugs.mysql.com/bug.php?id=68556

そのため、JreMemoryLeakPreventionListener で使うということは、tomcat 上の Connector/J を使う全アプリが運命共同体になることを意味する*6

それを許容できる、あるいはそもそも tomcat 上に war が 1 つしかないなら良いが、そうでないならば JreMemoryLeakPreventionListener を使うのではなく、

class MyServlet extends Servlet {
    @Override public void destroy(){
        // このアプリの classloader で走っている MySQL Connector/J のスレッドを停止させる
        // これによってこのアプリの classloader がまるごとメモリリークすることを防ぐ
        // 注: これを使う場合はこのアプリ自身で Connector/J をロードしていること、tomcat 全体で Connector/J をロードしている場合ここで止めてはいけない
        // http://saiya-moebius.hatenablog.com/entry/2014/08/20/230445
        AbandonedConnectionCleanupThread.shutdown();
    }
}

するべきであろう *7

*1:http://qiita.com/satococoa/items/e3396d9d75b9cf7e6214

*2:しかも C3PO はコネクションの生存判定の実装が賢くないので自力で頑張らないといけない部分があったりもする

*3:切断し忘れているコネクションや TCP レベルで断してしまったゴミコネクションが生き残る時間が長くなる副作用アリ

*4:MySQL Server 5.6 + Connector/J 5.1

*5:いまいち解決になってない気がするが・・・

*6:先述のサーバーサイド プリペアドステートメントの件では Connector/J の static 状態がどこかおかしくなるようで、全滅させられたことが・・・

*7:AbandonedConnectionCleanupThread#shutdown は古い Connector/J には存在しないが...

Objects#requireNonNull による null チェックのススメ

コンストラクタメソッドを書く際、引数が null になっていないかのチェックを書くのがめんどくさくて省略してしまうことはないだろうか? ...少なくとも私はめんどくさくて多くの場合省略してしまっていた。

しかし、Java 7 以降に入った Objects クラスなるものを使うと楽に書けるということを知ったので共有までに。

Usage

Objects (Java Platform SE 7)

公式の説明を見ればわかる通り大変シンプル。

Objects.requireNonNull メソッドは引数が null なら NullPointerException を投げ、そうでないなら引数をそのまま返す。

class Hoge {
    private final String huga;

    public Hoge(String huga){
        this.huga = Objects.requireNonNull(huga);
    }
}

これは Objects を使わない以下のコードと等価である:

class Hoge {
    private final String huga;

    public Hoge(String huga){
        if(huga == null) throw new NullPointerException();
        this.huga = huga;
    }
}

実際に Eclipse 上などで入力してみると、"Objects", ".", "requireNonNull" の 3 トークンだけで済む前者の方が、後者の if 文パターンよりも大分少ない手間で入力できる。

また、null チェックのためだけのコードが縦に長くならない点は可読性に良い (1 メソッドが長くなればなるほど読みづらい)。

加えて、requireNonNull はメソッドなので、文中のどこにでも挟めるのも地味に良い点である

public void doSomething(String huga){
    this.hogehoge(Objects.requireNonNull(piyo));     // メソッド呼び出し前に null チェックされる
}

ところで、null チェックって必要?

基本的に、エラーの原因とエラーの発生側の距離(コード上の場所や、実行タイミング)は近ければ近いほどよい。

距離が近くない場合、エラーが出てからその原因を特定するまでに時間を要してしまう。

Null チェックについてで言えば、フィールドに値を持つ場合、「フィールドに null を入れてしまった側」と、「そのフィールドの値を実際に使って NPE になってしまった側」のコード上の場所も実行タイミングも離れる。

それゆえに、NPE が発生してから、コードを辿ったり、あるいはもう一回プログラムを再実行してデバッガで追いかけなおすという手間*1が発生し開発効率が下がる。

よって、null チェックをしておくことは後々に自分や他人のためになる。

Objects#requireNonNull によって少ない負担で null チェックが出来るようになったことで、上記の null チェックのメリットがより得やすくなった(手間に対してメリットがペイするようになった)ので、これを機に null チェックする習慣をつけることを検討してはいかがだろうか。

*1:大概コードを追いにくいようなシロモノに限って作りもレガシーなので、プログラムの再実行のためにファイルやら何やらを整えなおすのに手間がかかり、実行してからエラーになるまで時間がかかり、例外トレースも汚くて...ということが多くつらい

SimpleDateFormat の罠まとめ & 対策コード例

公式ドキュメント:SimpleDateFormat (Java Platform SE 8)

便利かつ頻繁に利用される SimpleDateFormat クラスだが、実際のところ罠が多い。

知らずに罠を踏んでいる事例を身の回りで何度も目にした上に、罠を網羅的にまとめた記事が少ないことに気がついたので、書いてみた。

まとめ

  1. マルチスレッドで使うと壊れる : 下手なことはせずインスタンスは毎回作るべし
  2. 実在しない日時が通る : setLenient(false) すべし
  3. 文字の不足や過剰があっても通る : format 結果と突き合わせるべし

全対策を盛り込んだコード

License: 以下 DateTimeUtil クラスのライセンスは Public Domain とする

import java.util.Date;
import java.text.ParseException;
import java.text.SimpleDateFormat;

class DateTimeUtil {
    /** (Thread safe) {@link SimpleDateFormat} を用いて日時文字列をパースする。<br />
    * SimpleDateFormat とは異なり、実在しない日時(ex. "2014/02/29")や
    * 桁数の不一致(ex. "yyyy/MM/dd" で "14/02/28")は許容しないし、先頭一致ではなく全体一致である。<br />
    * 参考: http://saiya-moebius.hatenablog.com/entry/2014/08/17/165322
    * 
    * @param format {@link SimpleDateFormat} 形式のフォーマット文字列
    * @param source パース対象の文字列
    * @return パース結果の日時
    * @throws ParseException    与えられた文字列をパースできなかった。
    */
    public static Date parseExact(String format, String source) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat(format);
        sdf.setLenient(false);
        Date result = sdf.parse(source);
        if(sdf.format(result).equalsIgnoreCase(source)) return result;
        throw new ParseException("Unparseable date: \"" + source + "\"", 0);
    }
}

マルチスレッドで使うと壊れる

Advice: 下手なことはせずインスタンスは毎回作るべし

日付フォーマットは同期化されません。スレッドごとに別のフォーマット・インスタンスを作成することをお薦めします。 SimpleDateFormat (Java Platform SE 8)

この公式ドキュメントの記載は脅しでもなんでもなく、SimpleDateFormat はパース中の内部状態をフィールド変数に持っている。

そのため、マルチスレッド動作するプログラムでインスタンスを使いまわすと高確率で死ぬ。

つい static final フィールドで持ちたくなる誘惑に駆られるが、インスタンスを毎回 new する習慣をつけたほうが良い。

なお、近年の JVMJIT, GC の最適化*1は賢いため、下手に synchronized で同期化したり ThreadLocal で使いまわすのはクヌース先生のいうところの「早すぎる最適化」になりがちであるし逆効果であることも多い*2。そういうことは本当に必要になってから考えよう。

実在しない日時が通る

Advice : setLenient(false) すべし

非厳密性 Calendarは、カレンダ・フィールドを解釈する際、厳密および非厳密の2つのモードを使用します。非厳密モードの場合、Calendarはそれ自身が生成する値よりも広範なカレンダ・フィールド値を受け入れます。Calendarが、get()の値を返すためにカレンダ・フィールド値を再計算する際、すべてのカレンダ・フィールドが正規化されます。たとえば、非厳密なGregorianCalendarは、MONTH == JANUARY、DAY_OF_MONTH == 32を2月1日として解釈します。 Calendar (Java Platform SE 8)

SImpleDateFormat はデフォルトでは setLenient(true) な状態(非厳密)であり、以下の JUnit 例が示す通り実在しない日付は自動的に繰り上げ・繰り下げが行われる。

@Test()
public void testLenient_OutOfRange() throws ParseException {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
    Assert.assertEquals("2014/03/01", sdf.format(sdf.parse("2014/02/29")));
}

多くの場合これは意図している挙動ではないので、SimpleDateFormat を使う場合は setLenient(false) にして使う習慣にするほうが良い。

@Test(expected = ParseException.class)
public void testNonLenient_OutOfRange() throws ParseException {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
    sdf.setLenient(false);
    sdf.parse("2014/02/29"); // 29 日は存在しない, ParseException
}

桁数不足や末尾が余計でも通る

Advice : ParsePosition を見るか format 結果と突き合わせるべし

Notice : これらの挙動は setLenient(false) しても止められない

この件については公式の記述を読むより下記コードを見た方がわかりやすいだろう:

@Test
public void testNonLenient_ExOrMissingChars() throws ParseException {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
    sdf.setLenient(false);
    Assert.assertEquals("2014/03/01", sdf.format(sdf.parse("2014/03/01ほげ"))); // 余計な文字
    Assert.assertEquals("2014/03/12", sdf.format(sdf.parse("2014/03/012")));  // 余計な文字
    Assert.assertEquals("2014/03/01", sdf.format(sdf.parse("2014/3/1"))); // 桁数不一致
    Assert.assertEquals("0014/03/01", sdf.format(sdf.parse("14/3/1")));   // 桁数不一致
}

setLenient(false) しているにもかかわらず、"yyyy/MM/dd" を書く側としては期待していない文字列が通ってしまっている。

特に後者の "14/3/1" が西暦 14 年になってしまうのは痛い。

公式ドキュメントでも実は説明されている

public Date parse(String source) throws ParseException 指定された文字列の先頭からテキストを解析して日付を生成します。メソッドは指定された文字列のテキスト全体に使用されない場合もあります。 DateFormat (Java Platform SE 8)

公式ドキュメントの言い回しはいまいち非直感的だが、要するに「末尾に余計な文字が付いていても無視するぞ」という意味である。

数値: フォーマット時に、パターン文字の数は最小桁数です。これより短い数値は、この桁数までゼロ埋めされます。解析には、2つの隣接するフィールドを区切る必要がないかぎり、パターン文字の数は無視されます。 SimpleDateFormat (Java Platform SE 8)

この公式ドキュメントも分かりづらいが、例えば "2014/03/01" であれば "/" で区切りが明確なので、"2014/3/012" のように桁数が余計でも "2014/3/1" のように足りない部分があっても許容されますよ、という意味である。

ParsePosition なるものもあるが...

このような問題への対策として ParsePosition なるものを利用することを対策としている記述もネット上に存在する。

ParsePosition オブジェクトを parse メソッドに渡すことで、parse 対象文字列の何文字目までがパースされたかを知ることができるので、パースされた文字数が文字列の長さと一致するのかを見る、という対策である。

しかしながらも、上記の通り日付の各部分の文字数が「足りなくても過剰でも」通ってしまう性質上、parse された文字数のチェックだけでは不十分である (例えば年が短くて日が長すぎるとか)。

また、ParsePosition オブジェクトを渡す場合 ParseException が発生しなくなってしまい、代わりに ParsePosition#getErrorIndex() を毎回調べなければいけない。これは、多くの場合エラーのチェック漏れバグの原因となる。

そのため、ParsePosition を用いるよりも、素直に parse で得られた Date を format して元の文字列と一致するかをチェックした方が早い。

*1:Escape Analysis によってインスタンスをスタックに割り当てる、スレッド固有のヒープに割り当てる、など

*2:かえって同期化のコストが大きくなったり ThreadLocal のクリーンナップが問題になったりすることも多い