日々常々

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

Log4j 2のバージョンアップのやりかた

Log4j 2に脆弱性があるらしい、バージョンアップしたら治るらしい。」

本日話題のこのテーマで軽く書いておきます。 未完です。

f:id:irof:20211213114828p:plain

未完公開の言い訳。更新した内容は最後に書いてます。大きな間違いは(今のとこ)ないので、よかった。

2021-12-20追記: 2.17.0 出てますのでコピペしてそのままにせず適宜読み替えてくださいね。

とにかくバージョンを上げよう

……リリースできるかは別の話として。 バージョンを上げられないことには話になりません。ということでとにかくあげましょう。

Log4j 2のようなログライブラリは多くのプロダクトで使用されています。 意識する/しないに関わらず、ログライブラリは何かしら関連があると思うべきでしょう。

使用しているかの調べ方

常時依存ライブラリリストを出力するなどして管理しているのであればそれを見ればいいだけの話ですが、そうでなければ、 mvn dependency:tree(使ってるかだけなら mvn dependency:list の方がいいかもだけど) や gradle dependencies を叩いて log4j ワードを見るのが汎用的で取り早いでしょう。 プロダクトがたくさんあるなら、シェルスクリプトとかでやるといいかもですね。

出てこなかったら終了ですが、まず何かしら出てくると思います。

今回の話だと log4j-core が対象っぽい(log4j-apiだけなら影響ないんじゃ?と思ったりはしてますが調べてないです)ので、log4j-coreが無いならあげないのもありかもしれませんが、「Log4j 2は一部使っていますが、log4j-to-slf4jだけなんで上げなくていいんです」みたいな説明はそれはそれで面倒なので、一律「使ってるので更新しました」というのもありかなって。

pom.xmlbuild.gradleに書いていないから使っていない」は誤りです。

ログライブラリの構造

Javaのそれなりの規模のアプリケーションは、歴史的経緯もあり、このような構造になっています。

f:id:irof:20211213111413p:plain

この図ではログAPIが3種類ありますが、必要なだけ用意して、それを一つのロガー(実際にログを書き出すもの)に流し込む構造です。 これにより、異なるログAPIを使用しているライブラリやフレームワークでも共存できるようになっています。

色を塗っているところが依存ライブラリです。 自分達のコードが使用しているのは一番左だけですが、全てなければ実行時エラーになってしまいます。

下半分の色を変えているのは、ログライブラリが環境(アプリケーションサーバー)に提供される場合があるため。 今回のアップデート対象は「ロガー」のところなので、冒頭に挙げた「挙げなくていい」や、デフォルトではログAPIだけLog4j 2を使用しているSpringBootがLog4j 2に変えてなかったら影響ないよと言っているのはこのためです。

Spring Boot users are only affected by this vulnerability if they have switched the default logging system to Log4J2. The log4j-to-slf4j and log4j-api jars that we include in spring-boot-starter-logging cannot be exploited on their own. Only applications using log4j-core and including user input in log messages are vulnerable.

このアナウンスが出たので慌てて上げなくていいところは多そう。

上記の構造のため、本エントリなどの「バージョンアップ」は、providedの場合には通用しません。 アプリケーションサーバー側での対応が必要になると思います。(最近アプリケーションサーバー使わないから詳しくはわからない。)

どうでもいいけどcompile or providedじゃなくruntime or providedのが適切だと思うけど、大勢に影響ないし、画像直すの面倒だから直さない・・・。

ログライブラリの構造のもう少し詳しい情報は以下が参考になると思います。

バージョンアップのやり方

Log4j 2は複数のjarで構成されます。 MavenCentralRepositoryにあるものは https://search.maven.org/search?q=g:org.apache.logging.log4j で見られ、このブログを書いている現在、57個あります。 2.15.0 がリリースされているもので29個。いちいち見ていられません。

こういう複数jarのライブラリはBOMと呼ばれるバージョン管理用の一式が提供されていたりします。BOMはBill of Materials(部品表)のことで、Mavenでも公式に使用されている言葉です。

Log4j 2も log4j-bom があるので、これを使います。

MavenだとdependencyManagementタグです。

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-bom</artifactId>
            <version>2.15.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Gradleならplatformです。

dependencies {
    implementation platform('org.apache.logging.log4j:log4j-bom:2.15.0')
}

BOMを使用するとバージョンがこちらで管理されるので、log4j-api などを直接dependencyに追加する場合などもバージョンを記述しません。 仮に記述しても新しいバージョンが優先される(Gradleの競合解決に依存する)ので注意が必要です。

注意: log4j-apiなどの個別バージョンを指定してはいけない

すべてのLog4j 2ライブラリのバージョンを指定したりその違いを担保できないのであれば、個別に指定してはいけません。

たとえば以下のように記述した場合

dependencies {
    implementation 'org.apache.logging.log4j:log4j-api:2.15.0'
    // ...他にもある
}

こんなことになったりします。

f:id:irof:20211210173025p:plain

Log4j 2のバージョン食い違い。運良く動作すればよいですが、メソッドシグネチャの変更などがあると実行時にメソッドが見つからない例外が出たりします。辛い。

SpringBootでのバージョンアップ

公式アナウンス見た方がいいかも。(書いてること同じだけど)

SpringBootを使う場合、意識しているかはさておき spring-boot-dependencies を使用しているはずです。 spring-boot-dependenciesソースはGradleだったりするんで、pomファイル を見た方がイメージしやすいでしょう。 Gradleを使っててもMavenの知識が必要になってくるのはこういうところ・・・。

バージョンを上書きすると、「SpringBootチームが検証していくれているバージョンの組み合わせと違う組み合わせ」を使うことになります。 もちろん上書きしなくても動かない可能性はあって、自分達の責任範囲ではあるのですが、より動かない確率が高まるのは事実です。 SpringBootに乗っかるのであれば、この辺りのバージョン上書きはしないに越したことはない、と思っていたりします。

2021-12-23にリリースされるSpringBoot 2.6.2 でLog4j 2の2.15.0を使うようになってくれるようなので、もし今回の対応で上書きしても、SpringBootのバージョン上げる時は忘れずに削除するようにしましょう。 でないとLog4j 2が古いバージョンに取り残される可能性が出てきます。

バージョン上書きに関する警告はSpringBootのリファレンスにも書かれています。

Each Spring Boot release is designed and tested against a specific set of third-party dependencies. Overriding versions may cause compatibility issues and should be done with care.

SpringBoot x Maven

Maven<properties> タグでバージョンが列挙されていて、これで使うバージョンを上書きできるようになっています。log4j2.version でテキスト検索してもらえれば log4j-bomが使用されているのがわかるかと思います。 (なんで log4j-to-slf4j も入ってるんだろ・・・何かとかちあってるのかな?他のBOMに入ってるのよりトップレベルで書いてるののほうが優先されたりする。)

なのでSpringBootをMavenで使用している場合などは、propertyを上書きするのが正道です。pom.xmlに以下のように書く。

<properties>
  <log4j2.version>2.15.0</log4j2.version>
</properties>

もしくはmvnコマンドのパラメタでも指定できます。バージョンアップを一時的に試したいとかならパラメタのが楽と言えば楽かもですね。

SpringBoot x Gradle

SpringDependenctManagementプラグインを使用している、要するに apply plugin: 'io.spring.dependency-management' と書いているなら、Mavenと同じプロパティが使用できます。 つまり、 ext['log4j2.version'] = '2.15.0'build.gradle に書いてあげればOKかと思います。

SpringのDependencyManagementPluginを使用していない場合については別に書きました。(2021-12-11T22:40)

irof.hateblo.jp

脱線: 「Mavenリポジトリ」の指すもの

MavenCentralRepositoryは何も設定しなければデフォルトで使用されるリポジトリと思っていただければOKです。なにも修飾せず「Mavenリポジトリ」と言えば、MavenCentralRepositoryを指します。

よりダイレクトに見るならば https://repo1.maven.org/maven2/org/apache/logging/log4j/ です。こちらになければ、リポジトリを明示的に指定するかローカルリポジトリに自身でインストールしない限り、 mvn コマンドで使用できません。 なお https://mvnrepository.com/ はMavenCentralRepositoryを含めたMavenリポジトリを横断的に検索する、個人が運営してくれているサービスです。

irof.hateblo.jp もう少しくわしくかきました。

検証とかリリースとか

2.15.0 に更新することで、なんらかの互換性を失っているかもしれません。最近はあまり踏みませんが。 今回は2.14.1 から 2.14.2でなく2.15.0 なので、そこそこ大きな変更が予想されます。

互換性を失っている場合、直接使用している部分はコンパイルエラーになりますが、大抵はそうではなく、使用しているライブラリが使用しているメソッドが無くなってたりシグネチャが変わっていたりなどです。 こういう時は実行時に NoSuchMethodException とかが出てきます。困る。 でもいちいち調べるの無理なんで、自動テストで担保しましょう。

動作レベルの検証はユニットテストでいけますが、ログライブラリは環境による出力に差があるもの(ユニットテストならコンソールにプレーンなテキストで出すけど、運用環境だとJSONで出したり、ファイルに出したり)なので、ユニットテストだけでは担保できません。 ログを監視する仕組みも含めた環境に乗っけてのテストが必須になってきます。

また、当然のように性能にも差が出てきます。 この辺りのテストも自動化しておくと、「負荷テストぶん回しながらリリースに関わる説明とか調整を行う」ができたりします。

あと本番環境へのリリース後もしばらくは検証とみなせます。 Blue/Greenデプロイなどで切り戻せるようにしておけば、負荷テストを回しながら(致命的な問題があったら戻す)、セキュリティリスクの高い状況を早期に脱するためにリリースをしてしまう。なんて選択肢も出てきます。

申し送り(2021-12-10T18:00)

ちょっと用事があるので、推敲もしていませんが公開しておきます。あとで追記したり、ガラッと書き換えるかも。

更新履歴

  • 2021-12-11T19:30
  • 2021-12-13T11:20
    • ログライブラリの構造のセクション追加
    • BOMの説明追加
    • SpringBootでのバージョンアップに関する記述をMavenの項から引き上げ、リファレンスへのリンクと引用追加
  • 2021-12-14T12:40
    • Log4j 2のjar関連を示すFAQへのリンク追加
    • Gradleの「書いたバージョンが使われる」を「最新が使われる」に修正

Apache POI 5.1.0出てた

irof.hateblo.jp

の後続エントリ。

5.1.0でサイズは戻ったです。(少なくとも私が使う範囲は)

f:id:irof:20211126130808p:plain

依存ツリーも4.1.2の頃とだいたい同じ見た目。 左が5.1.0で右が5.0.0(の前1/3くらい)です。これがまんまjarの数になる。

4.1.2 -> 5.1.0で見るとほとんど差はなくて、log4j2にしたんだなーとか、commons-ioとかcommons-collections使うようにしたんだなーとか。 そんなふわっとした見方ができたりします。

changes

この辺か。

とりあえず4.1.2 -> 5.0.0 はちょっと抵抗感じたけど、5.1.0ならそこまででもない感覚。感覚があってるかはテストに任せる。

JUnitのNestedなMethodSourceの注意点

JUnit5での @MethodSource のおさらい

JUnit5にはパラメタライズドテスト用の @ParameterizedTest があり、様々な方法でパラメーターを与えられます。

その中でもパラメーターにある程度柔軟性が欲しい場合によく使うのが @MethodSource で、テストメソッドのパラメーターを生成できます。

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class MyParameterizedTest {

    @ParameterizedTest
    @MethodSource
    void test(String parameter) throws Exception {
        assertEquals(4, parameter.length());
    }

    static Stream<String> test() {
        return Stream.of(
                "hoge",
                "fuga",
                "piyo"
        );
    }
}

@MethodSourcevalue要素は引数のファクトリメソッド名を指定しますが、テストメソッド名と同じであれば省略可能です。 ファクトリメソッドは引数無しのstaticメソッドで、テストメソッドは引数ありとなるためシグネチャが同じにはなりません。

この例なら @MethodSource じゃなく @ValueSource が適切ですが、そこはお目こぼしください。

少し脱線になりますが、ファクトリメソッドの戻り型では Stream の他に Collection や配列などを使えますが、JUnitとしては「Streamにできるもの」だったりします。(MethodSourceのJavadoc参照) MethodSourceを使うと使われるMethodArgumentProviderでtoStreamしています。で、このtoStreamの中身はStreamだったらそのまま返すなので、「配列かListStreamかどれにすればいいんだろ」とか 無駄に 迷った時は Stream にしておくのがいい感じです。迷わないなら何でもいいので、無理に Stream を使う必要はありません。あと見ての通りStreamcloseしてくれないので、閉じなきゃいけないStreamは使っちゃだめです。

ここまではおさらい。

@MethodSource@Nested で注意が必要だったこと

JUnit5でネストしたテストを @Nestedで書きます。JUnit4のEnclosedではstaticなクラスでネストしましたが、JUnit5ではstatic無しです。 これにより外側のテストクラスのインスタンスフィールドが使用できるなど、色々便利になったのですが、@MethodSourceのファクトリメソッドはstaticになりますので、組み合わせると少々面倒なことになっていました。

class MyParameterizedTest {

    @Nested
    class MyNestedClass {

        @ParameterizedTest
        @MethodSource
        void test(String parameter) throws Exception {
            assertEquals(4, parameter.length());
        }

        static Stream<String> test() {
            return Stream.of(
                    "hoge",
                    "fuga",
                    "piyo"
            );
        }
    }
}

こう書きたいのですが、非staticなクラスでstaticなメソッドを持つことはできなかったので、コンパイルエラーになりました。エラーメッセージは以下。

.../MyParameterizedTest.java:36: エラー: 内部クラスMyParameterizedTest.MyNestedClassの静的宣言が不正です
        static Stream<String> test() {
                              ^
  修飾子'static'は定数および変数の宣言でのみ使用できます

なので以下のいずれかで対応していました。

  1. @Nested なクラスを @TestInstance(TestInstance.Lifecycle.PER_CLASS) とした上で、ファクトリメソッドを非staticメソッドにする。
  2. ファクトリメソッドをstaticで宣言できる場所に移動し、@MethodSourceで指定する。ここだと外に出して MyParameterizedTest#test とFQNで指定する。

前者は検索すると目にしますが、テストインスタンスのライフサイクルはデフォルトの PER_METHOD であってほしいので、やるにしても嫌な気分を背負いながらになります。後者はあまり見ないかな……ライフサイクルは変えなくていいんですが、せっかく省略できるファクトリメソッド名を指定しなきゃいけない上、FQNになるので冗長だし、外に出すとテストメソッドから離れてしまうしで、コーディング上でのやりたくない理由が多いんですよね。痛し痒しです。

どちらもユーザーガイドJavadocをちゃんと読めば書いているんですが、全部読んでられないってのも実際のところかと思います。 あまり公式ドキュメントを読み慣れていない方はこういう機会に「このブログで書いてることって、どこにどういう風に書いているんだろう」と探してみると、他のことも「あの辺に書いてるんじゃないかな」とアタリがつけられるようになるかもしれません。ならないかもですが。

若干絡むか絡まないかの公式ドキュメントの読み方の話。

irof.hateblo.jp

必要「だった」です

本題。

ここまで「必要 だった 」とか「コンパイルエラーに なりました。」とか過去形で書いてて、微妙な日本語になっています。

これはJava17(Java12-16は既にEOLなのでJava17を採用)でJEP 395: Recordsによりstaticメンバの定義が緩和されたためです。 興味のある方はJEP内の "Static members of inner classes" を参照ください。

これにより、上記のコードがコンパイルエラーにならなくなりました。 と言うことで、素直に動きます。「 @Nested@ParameterizedTest@MethodSource を使う時は @TestInstance(TestInstance.Lifecycle.PER_CLASS) にしたうえでファクトリメソッドを非staticにする」なんてことをやる必要はありません。このノウハウが必要なのはJava11を引き続き使用する場合のみです。Java17以降ではノイズになりますので、既存コードにある場合はサクッと消しておくようにしましょう。

いやぁ、ふつうに書けるっていいなぁ!と言うことでタイトルの「注意点」は「注意点がなくなった」です。

……Java17に上げられるならね。どうせユニットテストレベルだとJavaのバージョン違いでの差なんてほとんど出ないし、テストのコンパイルと実行だけ17にしていい……と思ったりしなくはない。テキストブロックもテストでは便利だし(特にJSON書くとき)。ビルドが複雑化するのを考えると微妙だけど。やっていいと思うけど、自分しかビルド環境メンテできない状況だと多分やらない。他の人がやろうとしたら応援する、くらいかなぁ。。。

書いた経緯

「なんか最近要らなくなったんだよなぁ」とふわっとした知識で話したんですが、JUnitのバージョンアップだと思ってて。でJUnitのリリースノートとか追ってみたんだけど見つからなくて。テストコード書いて見ようとしたらJava11だと書けなくて「あーそうだ、非staticには書けないよなー」と。でもJava17だと普通に書けたので、ああ緩和されたんだっけ、と。

Javaの仕様で必要だったフレームワークの使い方の迂回策が、Javaのバージョンアップでそもそも不要になっているわけです。 もちろん元のままでも動作しますが、この手のはしばらくすると理由が失われ「なんでこんなことをしているんだろう」となり、 理由がわからなくなると「消していいかどうかわからない」となると、おまじないとなって残ってしまいます。 過渡期にいる人は理由がわかるうちに対処する役割を担っているんじゃないかな、と思うわけです。

……とかそれっぽいこと書きましたが、単に「せっかく調べたから書いとこ」くらいのノリだったりします。

おまけ: IDEの警告

IntelliJ IDEAはこの辺りもいい感じに警告してくれます。

f:id:irof:20211118203830p:plain
Nestedがstaticになっちゃってるよー

f:id:irof:20211118203914p:plain
ファクトリメソッドがstaticじゃないよー

警告が出ていることに慣れているような方もいるかと思いますが、IDEの警告0を常態化することを強くおすすめします。