Gradle+SpringBootでLog4j 2のバージョン更新(DependencyManagementPlugin不使用)
Log4j 2のバージョンアップのやりかた からの派生。 特化した内容なので別エントリにします。
条件
- 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使ったりしないっけ?(最後は杞憂だけど。) configurations
のresolutionStrategy
なんて初めて見た←
仕切り直し。
spring-boot-dependencies
でもplatform
を使ってるので、こちらもplatform
でやればいいです。Log4j 2は log4j-bom
があります。
強制したい時はenforcedPlatformとかもありますが、今回はバージョンアップなのでplatform
でもよいです。
ということで、最終的なbuild.gradle
のdependencies
はこうなります。
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-core
の 2.14.0
を明示しても、spring-boot-dependencies
の2.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-core
とlog4j-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-bom
はspring-boot-dependencies
のpom.xml
読んでたら見つけました。
複数のBOMを読ませたら競合バージョン解決が行われ、基本的には新しい方が使われます。Mavenの深さとどちらがいいかは好みが分かれるところ。
BOMがなければconfigurations
とかで対象ライブラリ判定して適用する感じになります。
スクリプト書けるんでそれなりに柔軟な判定は可能、でもbuild.gradle
にあまり処理は書きたくない感もある。