日々常々

ふつうのプログラマがあたりまえにしたいこと。

ThreadContextやMDCはremoveせずにcloseしよう

新くもなんともない話です。当たり前と思う人には昔から当たり前(昔度合いはおまけ3を参照)。でも知らないことは悪ではないし、そうなんだーってなればいいだけの話。

一連のログに同じ値を付与したい時、 MDC (SLF4J) や ThreadContext (Log4j2) を使うかと思います。 中身は java.lang.ThreadLocal とかだったりするので、サーバーアプリケーションやマルチスレッドでは取り除いてあげないと事故の元です。

// 微妙なやり方
void method() {
    MDC.put("username", "hoge");

    // ... なんか処理

    MDC.remove("username");
}

こんなことしちゃうと例外が発生した場合に remove されなくて事故るので、伝統的な方法では finally でやるかと思います。

// 伝統的なやり方
void method() {
    try {
        MDC.put("username", "hoge");

        // ... なんか処理

    } finally {
        MDC.remove("username");
    }
}

ThreadContext もここまではほぼ同じ。

あと細かいけれど、上記の "username" が二箇所に出るとか、複数の項目を扱うときとか、あんま嬉しくないです。

本題: 後片付けはJavaに任せよう

try-with-resources を使えます。

SLF4Jだとこう。

void methodMDC() {
    try (var ignore = MDC.putCloseable("username", "hoge")) {

        // ... なんか処理

    }
}

Log4j2だと ThreadContext のかわりにCloseableThreadContext です。

void methodThreadContext() {
    try (var ignore = CloseableThreadContext.put("username", "hoge")) {

        // ... なんか処理

    }
}

try-with-resourcesclose することで remove 漏れもないし、キーを複数書かずによくなり、取り違えも無くなります。 putremove を1メソッドに書けない(事前処理と事後処理を別のところで書かなきゃいけないなど)とかでなければ、常にこれらを使うことをお勧めします。

SFL4J は補完で putCloseable メソッドが出てくるので気づきやすいのですが、 Log4j2は org.apache.logging.log4j.CloseableThreadContext と別クラスになるため認知率はガクッと下がる印象です。Log4j2やSFL4J以外を使っていても、同様のものがあると思います。

var を使っているのは素直にやると MDC.MDCCloseableCloseableThreadContext.Instance と長くなるにも関わらず、この変数の型に興味がないためです。こういうとこは var が輝く。

あと変数名を ignore とかにするのは、未使用変数でもこれは警告しないでくれってIDEAへの主張です。

hogeは灰色で警告されるが、ignoredは警告されない

try-with-resources を使用する場合にはよく使うと思います。ignoreでもignoredでもいいし、複数あるとignore1とか連番になったりでダサくなります。個人的には _ とかにしたいんだけども。 IDEの警告はゼロにしとくのがいいです。

脱線: 濫用?

MDCThreadContext はログライブラリのものなんですが、使えるからと言う理由でデータ受け渡しに便利に使用されていたりします。

そんな使い方をするんじゃねぇ、必要であればその情報受け渡しをきっちり設計しようよ……と思ったりもするんですが、 引数で引き回す以外の方法となると、DBなどの永続技術を使うか ThreadLocal などを使用する必要が出てきます。 単に動くだけならどんな方法でも簡単なんですが、適切に設計するのは難しく、下手なことするくらいならこっち使っておく方がマシか……と思ったりも。 ThreadLocal を使うと非同期処理を使う時とかに適切に受け渡す必要があったり(たとえば SpringBootでAsyncを使う時に知っておきたいExecutorのこと (2023-08-24) で書いているように)、複数このようなものが出てくると漏れたりしがちで、あと引きまわしたいスコープも似たり寄ったりになるので、まぁもういいかって思ったり、いややはりちゃんと、となったり。

ObservationContext もだし、今回の例で username と書いたけど認証コンテキストもそんな感じ。 たぶん各々で適切に取り扱う何かを作って、必要なとこでアダプタを作るーとかすれば綺麗なんだろけど、コストかけるとこでもないかなぁ……とかとか。なやましい。 動けばいいねん動けば

おまけ

これらがないときは AutoCloseableでなくてもtry-with-resourcesがしたい (2013-01-05) の方法で足掻いたりしかけて、わかりづらくなるかーってなって、普通に finally でやったりしてました。

// 今は不要
void method() throws Exception {
    MDC.put("username", "hoge");
    try (AutoCloseable ignore = () -> MDC.remove("username")) {

        // ... なんか処理

    }
}

putCloseable あるのにこれやってたら色々考え直した方がいいです。

おまけ2

はてなブログにいつのまにかAIでのタイトル生成機能がついてたので試してみた。

どれもコレジャナイ感。 だけど、AIがつけるタイトルと自分がつけるタイトルが近い文章のほうがいい文章なのかもしれないなぁ、とか思ったりもした。

おまけ3

冒頭で「昔から」って書いたので、どれくらいかなーと調べてみた。

  • SLF4J は 1.7.8 で入っているので、 2014-12-14。
  • Log4j2 は 2.6 で入っているので、2016-05-25。

へー。日付は CentralRepository のタイムスタンプね。