日々常々

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

Gradle+SpringBootでLog4j 2のバージョン更新(DependencyManagementPlugin不使用)

Log4j 2のバージョンアップのやりかた からの派生。 特化した内容なので別エントリにします。

2021-12-13追記: 今気づいたけどSpring公式ブログの[Log4J2 Vulnerability and Spring Boot](https://spring.io/blog/2021/12/10/log4j2-vulnerability-and-spring-boot)見るほうがシンプルでよいかもです。 本稿はバージョンダウンとか選択とかに興味あればって感じ。……`./gradlew dependencyInsight --dependency {}` なんてあったのね。
2021-12-20追記: `2.17.0` 出てますのでコピペしてそのままにせず適宜読み替えてくださいね。

条件

  • Gradle 7.3.1
  • SpringBoot 2.6.1
  • SpringBootで、Gradleで、SpringのDependencyManagementPluginを使わない。
  • Log4j 2のバージョンを一時的に上げたい(永続でなく、下げたいではない)

DependencyManagementPlugin使ってるなら素直に設定して終了です。

準備: SpringBootでlogbackじゃなくLog4j 2を使う

spring-boot-starter-log4j2 があるので、これを入れればOK・・・ではないんですよね。 単に追加するだけだと起動できません。

SLF4J: Class path contains multiple SLF4J bindings.
...
Caused by: org.apache.logging.log4j.LoggingException: log4j-slf4j-impl cannot be present with log4j-to-slf4j
    at org.apache.logging.slf4j.Log4jLoggerFactory.validateContext(Log4jLoggerFactory.java:49)
    at org.apache.logging.slf4j.Log4jLoggerFactory.newLogger(Log4jLoggerFactory.java:39)
    at org.apache.logging.slf4j.Log4jLoggerFactory.newLogger(Log4jLoggerFactory.java:30)
...

こんなログがでます。SLF4Jが複数あるからだめーって。 ちゃんとSpringBootのリファレンスには書いてるので、その通りしましょう。

ということで、Log4j 2を使う最小セットはこうなります。

dependencies {
    implementation platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-log4j2'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    modules {
        module('org.springframework.boot:spring-boot-starter-logging') {
            replacedBy 'org.springframework.boot:spring-boot-starter-log4j2'
        }
    }
}

./gradlew dependencies --configuration runtimeClasspath で一覧。

いい感じですね。……「いい感じ」と言われてもわからないかもなので一応。期待通りConstraintされている、Log4j 2が入っている、Logbackが入っていない、あたりです。あとLog4j 2もちゃんと2.14.1です。

もちろんMavenと同じくexcludeしてもいいです。選べるならお好きなやり方で。選べるだけの知識は必要になりますが。starter1つとかならいいけど、複数ある時に全部からexcludeするの案外めんどいよ。

Log4j 2のバージョンアップ

さあバージョンを上げましょう。 SpringBootのリファレンスに書いてある通り、ですがDependencyManagementPluginを使っていないのでlog4j2.versionプロパティを使用できません。残念。

書かれてることに従ってやるなら、以下の追加です。

dependencies {
    implementation platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-log4j2'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    modules {
        module('org.springframework.boot:spring-boot-starter-logging') {
            replacedBy 'org.springframework.boot:spring-boot-starter-log4j2'
        }
    }

    configurations.all {
        resolutionStrategy.eachDependency { DependencyResolveDetails details ->
            if (details.requested.group == 'org.apache.logging.log4j') {
                details.useVersion '2.15.0'
            }
        }
    }
}

./gradlew dependencies で見てもらったり、IDEの依存ライブラリでバージョン確認。

画像はIntelliJ IDEAさん。ライブラリ一覧を軽く見たい時はコマンドとかGradleウィンドウのDependenciesとかでなく、ProjectウィンドウのExternal Librariesで見ることが多い私です。 タイプしたらハイライトしてくれるしね。

やりたいことはできた。けれど、いくつかの問題があります。

  • build.gradle はシンプルに保ちたい。できればifなんて書きたくない。
  • Log4j 2のライブラリって全部org.apache.logging.log4jでいいんだっけ?他ってなかったっけ?他がこのGroup使ったりしないっけ?(最後は杞憂だけど。)
  • configurationsresolutionStrategyなんて初めて見た←

仕切り直し。 spring-boot-dependenciesでもplatformを使ってるので、こちらもplatformでやればいいです。Log4j 2は log4j-bom があります。

強制したい時はenforcedPlatformとかもありますが、今回はバージョンアップなのでplatformでもよいです。

ということで、最終的なbuild.gradledependenciesはこうなります。

dependencies {
    implementation platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
    implementation platform('org.apache.logging.log4j:log4j-bom:2.15.0')
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-log4j2'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    modules {
        module('org.springframework.boot:spring-boot-starter-logging') {
            replacedBy 'org.springframework.boot:spring-boot-starter-log4j2'
        }
    }
}

オーライ。

バージョン競合時の話

Gradleのバージョン解決は非常に細やかな制御ができるのですが、何もしない場合、競合したら 新しいバージョンが優先 されます。 このため「build.gradleに明示したバージョンが必ずしも使われる訳ではない」という意味です。今回のようなのだと新しいバージョンへの上書きなので問題ないのですが、古いバージョンへの上書きは期待通り動作しません。

仮に今回 2.14.1 -> 2.15.0 ではなく 2.14.0 へのバージョンダウンとかだと、platform ではこの方法ではうまくいきません。 たとえば以下のように log4j-core2.14.0 を明示しても、spring-boot-dependencies2.14.1が使用されます。

dependencies {
    implementation platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
    implementation 'org.apache.logging.log4j:log4j-core:2.14.0'
}

org.apache.logging.log4j:log4j-core:2.15.0 なら (log4j-corelog4j-apiは)2.15.0 が使用されます。 BOMが使えるなら先に挙げた enforcedPlatform でバージョンダウンも強制できますが、バージョンを下げるのは案外面倒ってだけ記憶の片隅に置いておくといいかなと思います。 他の解決方法もほんと色々あるのですが、ここでは割愛します。

Mavenだと<dependencyManagement>してても<dependency>で明示した方が優先されるので、注意が必要なところ。

ちゃんとした話はGradleのリファレンスを参照してください。 こちらでも書かれていますが、Mavenだと近さで選ばれます。

蛇足: SpringのDependencyManagementPluginを使わないわけ

たいそうな理由もないんですが、バージョン解決の仕組みが重なるのが嫌なんです。 「この設定Gradleだっけ、プラグインだっけ」とか、「Gradleのバージョンアップでこの辺変わったけどどう影響するんだろ」とか。

  • GradleのSpringBootPluginからDependencyManagementPluginが分離された
  • GradleにBOMサポートが入った

そこそこ長くGradleとSpringBootを使っているので、この2つのイベントをリアルタイムで見ています。 そしてGradleのバージョンもガンガン上がってる。「ついていくならGradleのほうかな」と、少なくとも依存解決の文脈ではそう思っているので、DependencyManagementPluginの使用は避けています。

別に使ってるのを無理矢理外したりはしないけどね。

あとがき

Gradleむずかしい(Mavenが簡単とは言っていない)。

BOMがあれば、BOMをplatformで使うのがいいと思います。log4j-bomspring-boot-dependenciespom.xml読んでたら見つけました。 複数のBOMを読ませたら競合バージョン解決が行われ、基本的には新しい方が使われます。Mavenの深さとどちらがいいかは好みが分かれるところ。

BOMがなければconfigurationsとかで対象ライブラリ判定して適用する感じになります。 スクリプト書けるんでそれなりに柔軟な判定は可能、でもbuild.gradleにあまり処理は書きたくない感もある。