SLF4J でログを吐く Java ライブラリは slf4j-api に compile スコープで依存するべき

eller さんが、ライブラリの作者はSLF4J のバインディングのみならず API も compile スコープの依存先から除くべきという趣旨のことを書いていました。前半はその通りだけど後半は違うのでは?と思い、そのようにコメントしたのですが、もう少し詳細を書いておきます。

SLF4J を使うライブラリについて、僕の考えは次のとおりです。

  1. 実装のバインディング (Logback とか slf4j-log4j12 とか) に compile スコープで依存してはいけない
  2. slf4j-api に compile スコープで依存するべき

1 は eller さんと同意見ですが、 2 については相違しています。このように考える理由は次の通りです。

slf4j-api に依存することで、「このライブラリは SLF4J を使うよ」ということが明確になる

これは字義通りです。

slf4j-api に依存しておけば、アプリはとりあえず動く

SLF4J は、アプリに対してファサード API (slf4j-api) を提供して、実際の出力先は LogbackLog4j 等のバインディングに任せる作りになっています。つまり通常、アプリは slf4j-apiバインディングの両方をクラスパスに含むのですが、バインディングが存在しなくても、とりあえず動くことは動くようになっています。つまり、ライブラリが compile スコープで slf4j-api への依存性を持っていれば、そのライブラリに依存するアプリは、とりあえず動きます。

例として Ehcache を挙げます。 ehcache-core の pom.xml を見ると、 compile スコープで slf4j-api に依存しています。 *1

それでは、 ehcache-core と JUnit だけに依存したプロジェクトを突貫で作って、 Ehcache のメソッドを叩くテストを書いて動かしてみます。すると、次のような警告メッセージが表示されますが、テスト自体は成功します。

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

これは SLF4J が、「バインディングが見つからないからログ吐けないよ。文句があるならこっちのドキュメントを見てバインディングを突っ込んでね」と言っています。

もし Ehcache が slfj-api に依存していなかったとしたら、次のような例外が発生して、テストは失敗するはずです。

java.lang.NoClassDefFoundError: org/slf4j/LoggerFactory
        at net.sf.ehcache.CacheManager.<clinit>(CacheManager.java:131)
        at com.example.App.<init>(App.java:7)
...

SLF4J を知っている人なら、「死ね」とか口走りながら slf4j-api を依存先に突っ込んで先に進むところですが、慣れてない人は大変です。 SLF4J の名前を冠した JAR は山ほどあるので (ログ実装ごとにバインディングがある) 、どれを突っ込めばいいのかすぐには分かりません。この点でも、ライブラリは slf4j-api を依存先に含んでいる方が親切設計です。

アプリの側でのバージョンの上書きは容易

eller さんは、アプリ側で SLF4J の特定のバージョンを使いたい際、 SLF4J に推移的に依存しているライブラリごとに exclusion を書く必要がある、ということを問題にされています。しかし実際には、アプリの pom.xml で明示的に特定バージョンの slf4j-api への dependency を書けば、その設定が優先されるため、 exclusion を書いて回る必要はありません。

たとえば次のように、アプリが Ehcache のみに依存しているとします。

<!-- 前略 -->
 
  <dependencies> 
    <dependency> 
      <groupId>net.sf.ehcache</groupId> 
      <artifactId>ehcache-core</artifactId> 
      <version>2.6.5</version> 
    </dependency> 
  </dependencies> 

<!-- 後略 -->

この時、「mvn dependency:tree」を実行すると、次のような依存関係ツリーが得られます。

com.example:sample:jar:1.0-SNAPSHOT
\- net.sf.ehcache:ehcache-core:jar:2.6.5:compile
   \- org.slf4j:slf4j-api:jar:1.6.1:compile

アプリ → ehcache-core 2.6.5 → slf4j-api 1.6.1 という依存関係が分かります。

もしアプリの側で SLF4J のバージョン 1.7.3 を使う必要があれば、次のように pom.xml 中で明示的に dependency を定義します。 exclusion を指定する必要はありません。 *2

<!-- 前略 -->
 
  <dependencies> 
    <dependency> 
      <groupId>net.sf.ehcache</groupId> 
      <artifactId>ehcache-core</artifactId> 
      <version>2.6.5</version> 
    </dependency> 
    <dependency> 
      <groupId>org.slf4j</groupId> 
      <artifactId>slf4j-api</artifactId> 
      <version>1.7.3</version> 
    </dependency> 
  </dependencies>

<!-- 後略 -->

すると、依存関係は次のようになります。

com.example:sample:jar:1.0-SNAPSHOT
+- net.sf.ehcache:ehcache-core:jar:2.6.5:compile
\- org.slf4j:slf4j-api:jar:1.7.3:compile

アプリの pom.xml で指定した slf4j-api 1.7.3 が使われており、 ehcache-core が指定している 1.6.1 は抑制されています。これは、 Maven がアプリに近い所で指定されたバージョンを優先するからです。

何かの理由で古いバージョンを使いたい場合も、同様にアプリの pom.xmldependency を定義すれば OK です。

<!-- 前略 -->
 
  <dependencies> 
    <dependency> 
      <groupId>net.sf.ehcache</groupId> 
      <artifactId>ehcache-core</artifactId> 
      <version>2.6.5</version> 
    </dependency> 
    <dependency> 
      <groupId>org.slf4j</groupId> 
      <artifactId>slf4j-api</artifactId> 
      <version>1.6.0</version> 
    </dependency> 
  </dependencies>

<!-- 後略 -->

依存関係は次のようになります。

com.example:sample:jar:1.0-SNAPSHOT
+- net.sf.ehcache:ehcache-core:jar:2.6.5:compile
\- org.slf4j:slf4j-api:jar:1.6.0:compile

結論

結局のところ、親切設計の観点から、 SLF4J を使うライブラリは次のようにするべきです。

  • compile スコープで slf4j-api に依存し、 SLF4J の LoggerFactory や Logger を使ってログを吐く。 LogbackLog4j を直接触ることはしない
  • test スコープで LogbackLog4j + slf4j-log4j12 に依存する。その上で Logback, Log4j などログ実装ごとの設定ファイルを配置して、ログが吐かれるようにする

アプリは全部 compile スコープでいいと思います。

*1:test スコープで slf4j-jdk14 (java.util.logging へのバインディング) に依存していますが、使う側のアプリに影響しないので問題ありません

*2:実際のアプリではここで、バインディングへの依存も同時に指定するべきです