Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

Javaライブラリを配布する際のログ周りにおける配慮と実践

2020-07-22更新: 以下の投稿で情報をアップデートしています。 https://blog.kengo-toda.jp/entry/2020/07/21/223136


いつも購読させていただいている id:teppeis さんのブックマークに以下のエントリが流れてきて、なるほどこいつはたしかに厄介だと思いました。

ただSLF4Jが最も先進的かつ著名なインタフェースである以上、配布側としてはSLF4Jを使いつつ問題を解決したいところです。他のインタフェースを使ったりオレオレ実装を使ったりしてしまうと、それこそユーザの自由度を奪ってしまう形になります。

実際、SLF4Jを配布パッケージに含めないという簡単な解決法がありますので、簡単に紹介します。悲劇を繰り返さないためにライブラリ開発者がすべきことは何かを考える上で、参考にしていただければ幸いです。

配布パッケージの依存先からSLF4Jを取り除く

なぜ依存先からSLF4Jを取り除くべきか

SLF4Jを利用したライブラリを開発する際に注意すべきは、以下の2点です。

もし配布パッケージにSLF4JのAPIが含まれていたり、配布パッケージがSLF4JのAPIcompileスコープで依存していたりすると、ユーザがいちいちexclusionで依存を断ち切らなければなりません。
ライブラリは複数組み合わせて使われるものですから、1つひとつexclusionを書いていたらキリがありません。SLF4Jを依存先から取り除いておくことが、特にエンタープライズ製品のような大きなプロダクトでは喜ばれるでしょう。

配布パッケージの依存先からSLF4Jを取り除くには

repackageやcompileスコープでの依存はやめ、providedスコープでの依存を使いましょう。テスト実行時にバインディングを使いたいなら、testスコープで依存するようにします。

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.3</version>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-simple</artifactId>
  <version>1.7.3</version>
  <scope>test</scope>
</dependency>

ただこの方法だとユーザがSLF4Jへ明示的に依存しなければならないため、面倒に感じるユーザがいるかもしれません。そうした人への配慮が必要なら、classifierの利用を検討しましょう。同じバージョンで異なる種類のパッケージ(SLF4J付き、SLF4J無し など)を配布することができます。

SLF4Jへの依存を除くべきもう1つの理由

ちなみにSLF4Jの1.7系では可変長引数を使うようにインタフェースの変更が行われましたが、新インタフェースに対する呼び出しは旧インタフェースへのそれとバイトコード的に等価なため、1.6のAPIでコンパイルしたクラスを1.7のAPIと組み合わせて使うことができます。1.7でコンパイルしたクラスを1.6と動かすことも可能です。

つまりライブラリをどのバージョンのAPIコンパイルしようと、ユーザには関係がありません。ユーザの"バージョンを選ぶ自由"を損ないません。

しかし1.6のAPIと1.7のバインディングを組み合わせることはできません。1.7のAPIと1.6のバインディングもダメです。つまり、APIのバージョンとバインディングのバージョンは常に一致させる必要があります。

1.6に依存するライブラリと1.7に依存するライブラリを同時に使うと、クラスローダの状況によってはエラーが生じてしまうかもしれません。ユーザはわざわざexclusionを書くか、ライブラリが依存しているバージョンを使うかしなければなりません。

この問題を起こさないためにも、ライブラリはSLF4Jを含むべきではなく、ユーザに依存関係を明記させる方が好ましいと言えます。

以上で依存先からSLF4Jを取り除く方法に関する話は終わりです。 やり方としては非常にシンプルですが、ライブラリ実装時はなかなか気づきにくい問題だと思います。最後に関連する他の問題に触れておきます。

SLF4J以外のインタフェースに依存しないために

これは先の記事で触れられていた問題とは無関係ですが、「Javaライブラリを配布する際のログ周りにおける配慮と実践」の1つとして触れておきます。

ユーザの"SLF4Jのバージョンとログ出力先を選択する自由"を確保ことが肝心なわけですが、このためにはLoggerインタフェースを必ず介してログを出力すると決め、それを統一する必要があります。しかし複数人数で開発していると、意外と漏れが出てしまいます。

具体的には、System.outSystem.errの利用をやめ、またこれらを間接的に呼び出すThrowable#printStackTrace()の呼び出しも排除するべきなのですが、どちらも開発やデバッグに便利なためいつの間にかコードに紛れ込んできてしまいます。

// bad code
System.err.println("user (" + userName + ") requests to remove it, but it is not admin user.");
exception.printStackTrace();

// it should be
logger.warn("user ({}) requests to remove it, but it is not admin user.", userName, exception);

こうした改善すべきコードはPMDのSystemPrintlnルールAvoidPrintStackTraceルールを使って確認することができます。CIに組み込んでおきましょう。