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
にあまり処理は書きたくない感もある。
Log4j 2のバージョンアップのやりかた
「Log4j 2に脆弱性があるらしい、バージョンアップしたら治るらしい。」
本日話題のこのテーマで軽く書いておきます。 未完です。
未完公開の言い訳。更新した内容は最後に書いてます。大きな間違いは(今のとこ)ないので、よかった。
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.xml
やbuild.gradle
に書いていないから使っていない」は誤りです。
ログライブラリの構造
Javaのそれなりの規模のアプリケーションは、歴史的経緯もあり、このような構造になっています。
この図ではログ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
のが適切だと思うけど、大勢に影響ないし、画像直すの面倒だから直さない・・・。
ログライブラリの構造のもう少し詳しい情報は以下が参考になると思います。
- Javaのログ出力: 道具と考え方: 宮川拓さんのスライド
- SLF4Jのユーザーマニュアル: の画像がわかりよい
- Log4j 2のFAQのJARの関係: 今回はこっちの画像の方がいいか。
バージョンアップのやり方
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' // ...他にもある }
こんなことになったりします。
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)
脱線: 「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)
ちょっと用事があるので、推敲もしていませんが公開しておきます。あとで追記したり、ガラッと書き換えるかも。
更新履歴
Apache POI 5.1.0出てた
の後続エントリ。
5.1.0でサイズは戻ったです。(少なくとも私が使う範囲は)
依存ツリーも4.1.2の頃とだいたい同じ見た目。 左が5.1.0で右が5.0.0(の前1/3くらい)です。これがまんまjarの数になる。
4.1.2 -> 5.1.0で見るとほとんど差はなくて、log4j2
にしたんだなーとか、commons-io
とかcommons-collections
使うようにしたんだなーとか。
そんなふわっとした見方ができたりします。
- Upgrade Batik dependency to 1.14
- Upgrade BouncyCastle dependency to 1.69 (including adding dependency on bcutil jar)
- Internal logging in POI now uses Apache Log4J 2
この辺か。
とりあえず4.1.2 -> 5.0.0 はちょっと抵抗感じたけど、5.1.0ならそこまででもない感覚。感覚があってるかはテストに任せる。