理系学生日記

おまえはいつまで学生気分なのか

Javaアプリから共有ライブラリが読み込めない問題(java.library.path と LD_LIBRARY_PATH)

なんか良くわからないエラー (no [ライブラリ名] in java.library.path みたいなヤツ) が出てきたのだけれど、解決方法が java.library.path の設定ではなく、LD_LIBRARY_PATH の設定だったりした。この手のエラーに苦しめられ続けており、そのあたり、ちょっと掘り下げてみる。

java.library.path

java.library.path がどこで使われているかというと、ClassLoader で使われている。 実際、ClassLoader#findLibrary の JavaDoc には

ネイティブ・ライブラリの絶対パス名を返します。VMは、このメソッドを呼び出して、このクラス・ローダーによってロードされたクラスのネイティブ・ライブラリを検索します。このメソッドがnullを返す場合、VMは「java.library.path」プロパティで指定されたパスに従ってライブラリを検索します。

という記述がある。

具体的に ClassLoader のソースを見てみると、

    // Invoked in the java.lang.Runtime class to implement load and loadLibrary.
    static void loadLibrary(Class<?> fromClass, String name,
                            boolean isAbsolute) {
        ClassLoader loader =
            (fromClass == null) ? null : fromClass.getClassLoader();
        if (sys_paths == null) {
            usr_paths = initializePath("java.library.path");
            sys_paths = initializePath("sun.boot.library.path");
        }

というように、java.library.path を参照していることがわかる。 この java.library.path の値は、実際に読み込むライブラリ名と合成された後、JVM からロードされる。

LD_LIBRARY_PATH

LD_LIBRARY_PATH 自体は、Java の用語ではなく、Linux 系の用語である。 これが出てくる文脈は、ライブラリの動的ロードあるいは動的リンクで、内容としてはこのあたりが詳しい。

動的リンクとは実行時にライブラリがメモリ上にない場合に Linux に当該ライブラリをロードさせること、動的ロードとは、アプリケーションがライブラリ内の関数を動的に呼び出すことを指すらしいが、このときのライブラリの探索パスが LD_LIBRARY_PATH 環境変数になる。

なぜ java.library.path の設定では解消しなかったか

java.library.path はあくまで、JVM から見えるライブラリの探索パスになる。このため、このライブラリが他のライブラリに依存(動的リンクあるいは動的ロードの対象としている)場合、java.library.path の設定では効果を及ぼさず、この動的リンク/動的ロードに失敗してしまう。 こういうケースにおいては、LD_LIBRARY_PATH を設定しないといけない。

maven と git-flow を使用したリリースフロー

ぼく自身、git-flow を利用するのは始めてで、Maven 力も低い。 そういう人間がリリース時にどうしようかとか考えても仕方がないので、先人に頼るかーと思っていたら、ちょうどいいエントリがあった。

Why I Never Use the Maven Release Plugin - DZone Java

このエントリ、基本的には maven-release-plugin を dis るエントリなんだけど、dis だけでなく、じゃぁどうやって「リリース」という作業を実現するのかが述べられているのが良いと思う。 git-flow を利用する maven 環境のエンジニアが遍くこういうフローを使っているかは知らないけれど、ぼくが考えるよりもずっと精度が良いと思うので、メモがてらまとめてみる。

リリースフロー

  1. リリースすることを全員に伝え、必要なリソースを development ブランチに push してもらう
  2. development ブランチから release ブランチを切る
  3. development ブランチの POM のバージョンを、次バージョンに更新し、commit & push しておく。
  4. release ブランチの POM バージョンを CR (Candidate Release) のバージョン (ただし、SNAPSHOT)に更新し commit & push。
  5. release ブランチでテストを実行し、PASS させる。
  6. release ブランチから Candidate Release のビルドを作成する
    1. release ブランチの POM バージョンを CR のバージョン (SNAPSHOT なし) に更新し、commit & push
    2. release ブランチで tag を切る
      1. の POM バージョンで再度更新し、commit & push
    3. 6-2. のタグで checkout
    4. deployment build を実行する
    5. deployment を QA 環境にデプロイする
  7. QA 環境で、テストを PASS させる。ここで出たバグは release ブランチで修正し、development ブランチに定期的に merge する。
    • 実行手順は、バージョンが CR2, CR3 と上がっていく以外は 6. の通り
  8. 最終リリースを作成する
    1. 正式なリリースバージョンで POM のバージョンを更新する
    2. release ブランチのタグを切る
    3. release ブランチを master ブランチにマージする
    4. master ブランチを checkout する
    5. deployment build を実行する
    6. 本番リリースを開始する

git-flow を眺めるだけでは分からなかったけど、リリースフローはなかなか複雑なかんじになる。たいへんそう。 もうちょっと省力化したいけど、そうすると maven-release-plugin の再発明になって、エントリの著者に dis られそう。

モックサーバStubby4jの性能

モックサーバに Stubby4j というのがある。

一緒に働いている方に紹介してもらったんだけど、これすごく使いやすい。 使い方は README にわかりやすく書いてあるからそれを読んでもらえばよい。 この Stubby4J、性能テストにも使いたいことがあるから、どの程度パフォーマンスがでるか試してみた。

結論としては以下のとおり。横軸が concurrency で縦軸が 1 秒あたりに捌けるリクエスト数になる。 裏でブラウジングしながら測定するなど、かなり適当な計測したけど、600 リクエストくらいは余裕で捌けそう、ということが分かった。 f:id:kiririmode:20160217115540p:plain

環境

だいたい以下のような環境で測定。GitHub - kiririmode/stubby4j-performance に一連のスクリプト置いてる。

  • MacBook Pro Mid 2012
  • 2.3 GHz Intel Core i7
  • 16 GB 1600 MHz DDR3
  • Stubby4j v3.3.0
  • ApacheBench v2.3
  • java 8
$ java -version
java version "1.8.0_51"
Java(TM) SE Runtime Environment (build 1.8.0_51-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.51-b03, mixed mode)

ダルかったのは、連続でテストをしてると、一定時間 stubby4j がハング (リクエストを送っているのに、応答を返さなくなる) する点。TCP のソケット数まわりが怪しいけど調べると時間かかりそうだったので、以下のように測定スクリプトのループで都度 Stubby4J の起動・停止を行うようにした。それでも解決しなかったので、ループの最後に sleep 10 をするという荒技してる。

#!/bin/bash

# リクエスト数
req=5000
# Stubby4j の起動待ち時間
time4boot=2
# 結果ファイル名
result_tsv=perf.tsv

# ファイルディスクリプタ数を変更
ulimit -n 8192
# 結果ファイルをクリア
: > $result_tsv

# concurrency でループ
for c in 1 2 5 10 20 50 100; do
    # stubby4j は都度立ち上げる
    java -jar stubby4j-3.3.0.jar -d routes.yaml &
    PID=$!

    # stubby4j の起動時間を確保
    sleep $time4boot

    # 実行
    ab -n $req -c $c -r -q 'http://127.0.0.1:8882/hello-world' | awk -v concurrency="$c" '/across all concurrent requests/ { print concurrency "\t" $4 }' >> $result_tsv

    # stubby4j を kill
    kill $PID

    sleep 10
done

# グラフ作成
gnuplot perf.plt

Java に関する最後の雑誌?Java Magazine の購読

最近はもっぱらジャバジャバしているので、Java の雑誌でも読んで動向をウォッチしとかないとなぁなどと思っていた。 が、JAVA PRESS が死に、JavaWorld が廃刊になったこのご時世、雑誌としてはどうやってウォッチすれば良いのかなぁと思ったら Oracle が Java Magazine を発行していた。

電子書籍になっていて、無料ということで、これはもう読まざるを得ないかんじになってる。 とりあえず 24 号 (2016/01 発行) に目を通したところ、翻訳もこなれているし、解説も分かりやすいので、当面はこれを毎月読んでいれば良さげ。あとは Feed 読みふける。

24 号、特に JCommander の解説(タイトルは JCOMMANDER : もっと手軽にコマンドライン解析)と、JVM のクラスローダの解説 (タイトルは JVM によるライブラリの検索、ロード・実行) が良かった。 JCommander は Commons CLI よりもモダンなインタフェースをしているし、拡張性が高そう。 JVM クラスローダの話は階層型クラスローダの基本なんだけど、こういう基本に立ち返ることができる機会を与えてくれるのは嬉しい。

Maven力の低い自分に送るMaven良い感じドキュメント

Maven 力が低くて、会社の人に迷惑かけている感じある。 Maven、基本的に 公式ドキュメント 読みにくいし全体像つかみにくいし、本読んでもなかなか詳細が分からないし何とかならんものかと思ってた。

Apache Maven 3 クックブック - 理系学生日記

思って探してたら、良いかんじのドキュメントあった。

sonatype のドキュメントだし信頼できると思う。がんばって読んでいきたい。

テスト前提が満たされている場合にのみJUnitのテストケースを実行したい

Java で JUnit のテストコードを書く場合、テスト実行環境がこういう前提を満たした場合にのみ、このテストを実行させるようにしたい、というようなケースが多々あります。 例えば、

  • テスト実行環境の OS に依存する
  • テスト実行環境における特別なソフトウェアのインストール有無に依存する

といったようなケースです。 このように、前提条件が満たされるか否かによってテストの実行是非を制御するようにしたい場合、Assume を利用するのが簡単です。

Assume

Assume クラスは JUnit に付随してくるクラスで、Assert と対を成すようなメソッドを持っています。 下記にいくつか抜粋します。

public static void assumeTrue(boolean b);
public static void assumeFalse(boolean b);
public static void assumeNotNull(Object... objects);
public static <T> void assumeThat(String message, T actual, Matcher<T> matcher);

これらのメソッドは、いずれも「前提条件」を記述するようになっており、前提条件が満たされれば何も起こしません。 一方で、前提条件が満たされなかった場合、AssumptionViolatedException を送出するようになっています。この AssumptionViolatedException は、一般に TestRunner で特別視され、JUnit の TestRunner だと当該例外発生時はそのテストを無視する、という振舞になります。 これを利用すれば、上記のような OS 依存、ソフトウェアインストール状況依存のテストも、問題なく記述できるようになります。

簡単なサンプルとして、Mac 以外の OS でのみ実行するテストを記述すると以下のようになります。

package com.kiririmode.blog;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assume.assumeFalse;

import org.junit.Test;

public class AssumeTest {

    @Test
    public void Mac以外のOSで実行するテスト() {
        // テスト実行環境が Mac でない場合はテストを実施しない
        assumeFalse(isMac());
        assertThat(System.getProperty("os.name"), is("Mac OS X"));
    }

    /**
    * 実行環境が Mac かどうかを判定する
    *
    * @return 実行環境が Mac の場合に true
    */
    private boolean isMac() {
        return System.getProperty("os.name").contains("Mac");
    }
}

テストクラスに記述された全テストケースがそういう環境依存のテストであるならば、@Before あるいは @BeforeClass を付与したメソッドで assume* を使う方が効率的になります。

環境依存テストの別解

上述のような「前提条件」をテストクラス横断で行いたい場合は、都度前提条件を記述するよりは、前提条件をひとつの TestRule として記述した方が省力化が図れます。 このあたりのことは、JUnit 実践入門の 「9.3. カスタムルールの作成」にシャレたサンプルが掲載されているので、参考にすると良いと思います。

JCAとは何であるのか

Java SDK というのは、膨大な機能セットを提供してくれていますが、その中には暗号化に関するものがあり、このうち、暗号化に関する機能についてのフレームワークセットを JCA (Java Cryptography Architecture) と呼びます。 ちょっとこういう機能セットを利用したい状況にあるのですが、イマイチ JCA というものが分かっていないので、調査しまとめてみました。

** JCA の概要 JCA は、デジタル署名やメッセージダイジェスト、証明書とその検証、暗号化と復号化、鍵管理、乱数生成といったセキュリティに纏わる一連の API を含むフレームワークです。 これらの暗号化機能は、歴史的に Sun や SunJSSE 等、様々な実装が提供されてきましたが、JCA ではこれら実装に対して統一的にアクセスできるインタフェースを定義し、それらを、後述する「プロパイダ」からアクセスできるようにしています。

JCA の根本にある設計方針は次の 2 つです。 - 実装の独立性と相互操作性 - アルゴリズムの独立性と拡張性 前述のように、暗号化機能に対する複数の実装があり今後も増えることを念頭に、JCA では、Provider を利用することで各実装が互いに干渉しないようにこれらの実装を利用することができます。また、アルゴリズムの独立性については、個々の機能に対するエンジン(メッセージダイジェストについては MessageDigest クラス、暗号化については Cipher クラス、… というようになっています) が独立して提供されることで実現しています。

抽象論だと分かりづらいのですが、Java 暗号化アーキテクチャ(JCA) リファレンス・ガイドに分かりやすい図があったので、こちらをベースに説明します。

この図は、アプリケーションが JCA を利用してメッセージダイジェストを利用するユースケースを説明しています。 左側の図は、「アプリケーションが特定のメッセージ・ダイジェストの実装に依存せず、MD5 ハッシュを取得したい」というユースケースで、右側の図は「アプリケーションが Provider C の提供するメッセージ・ダイジェストの実装を利用して MD5 ハッシュを取得したい」といったユースケースの図になります。

左側の図では、アプリケーションが MessageDigest.getInstance("MD5") を実行することで、Provider Framework (JCA) に対し「適切な MD5 実装を取得しろ」という要求を出しています。JCA は、事前に定義された優先度に基いて、Provider A, B, C が MD5 実装を提供しているかを調べ、提供する Provider の中で最も優先度の高い Provider B が提供する MD5 実装を返します。 右側の図では、アプリケーションが MessageDigest.getInstance("MD5", "ProviderC") を実行することで、Provider Framework (JCA) に対し「"Provider C" の提供する MD5 実装を取得しろ」という要求を出しています。JCA は、その要求に基き、Provider C が提供する MD5 実装を返します。

このように、JCA では以下の 2 つのコンポーネントを提供しているといって良いと思います。 + 暗号化関連のインタフェースを定義し、適切な実装を返却するフレームワーク + 暗号化関連のインタフェースを実装する暗号化 Provider

** JCA で提供されるエンジン JCA では、以下のようなエンジンを提供しています (抜粋です) + SecureRandom: 安全な乱数/疑似乱数の提供 + MessageDigest: メッセージダイジェストの計算 + Signature: データの署名、および、検証 + Cipher: データの暗号化/復号化 + MAC: メッセージの整合性を保証するためのハッシュ計算 + KeyFactory: プログラムで表現される抽象的な鍵の概念を具体的な鍵の仕様に変換 + SecretKeyFactory: 秘密鍵のみを作成する KeyFactory + KeyPairGenerator: 公開鍵・秘密鍵ペアの生成 + KeyAgreement: 鍵の合意・確立 + KeyStore: 鍵の作成・管理

個々のエンジンについては詳細な使い方はもちろん異なりますが、getInstance メソッドを呼び出して実装を取得する点など、概要的な使い方は似ている部分も多いので、一つのエンジンを使えたら他のエンジンを使用する際も大きな隔絶は無いかなと思います。

** Provider の優先順位

冒頭に記載したように、Provider には優先順位を指定することができます。JCA に対して明示的に Provider を指定しない限り、JCA から取得される暗号化関連機能の実装は、この優先順位によって決定します。 では、優先順位をどこに定義するのか、ですが、この優先順位については静的に設定する方法と動的に設定する方法があります。多く使われているのは静的な設定で、これは、$JAVA_HOME/lib/security/java.security ファイルに指定します。

当該ファイルで security.provider.n (※n は数字) というキーが定義されている箇所があるかと思いますが、この値がプロパイダを示すクラス、n が優先順位になります。(1 始まり。数字が小さいほど、優先順位が高い) このあたりの構成は、むしろ、Java PKCS#11 あたりを参照した方が良いかもしれません。このへんはまた別途エントリを起こそうと思います。

Java プログラマーなら習得しておきたい Java SE 8 実践プログラミング

Java プログラマーなら習得しておきたい Java SE 8 実践プログラミングを読みました。タイトル長いな。

どの言語でもそうですが、特に Java という言語は、その言語仕様とクソのように多い API の使い方をどれだけ知っているかによって、その開発効率が大きく変わってくる言語だと思っています。
そういう意味で、ぼくはこれまでJava (SE) 8 は使っていたものの、全体としては Stream とかラムダくらいしか Java SE 8 の新機能を使っていなくて、かつ、他に新機能ってあったっけ?っていう感じでした。
この本は、そういう

  • Java については一通り開発経験があるものの
  • Java 8 の新機能に関する全体像が曖昧

という、なんとなく動くものは作れるが Java 8 的にそれがベストなのかが分からない、というタイプの人間にはハマってくれる本でした。

一般に Java で言語仕様に関する本というと、

くらいの厚さで嫌になったりするのですが、300 ページにも満たないページ数で要点がきれいにまとめられているところも嬉しい。


まえがきにもありますが、

この本は、「短気な (impatient)」スタイルで書かれています。そのスタイルは、書籍「Scala for the Impatient」で私が最初に採用したものです。
その本では、あるパラダイムが他のパラダイムよりも優れていることについて読者に講義するのではなく、手短に要点を説明したかったのです。必要なときにいつでもすばやく参照できるように、小さな単位で技術情報を解説しました。

というように、

  1. Java 8 で何が導入されたのか
  2. それはどういうユースケースで使われるのか

というような内容が、個々の機能について例題とサンプルコードとともにに記述されていて、タイトルの「実践」に嘘偽りのない本です。本当に簡潔で良かった。Java 6 → Java 7 が大した進化が無かった (ようにぼくには見える) のに対し、Java 7 から Java 8 は非常に多くの機能が追加されていて、コードを書くときもかなりパラダイムを変えざるを得ない*1のだけれど、この本を読んどけば最低限そういうパラダイムに追随できるのではないかと思います。

書いてあること

最初はラムダ式から始まります。ラムダ式とは何なのか、そして、そのラムダ式を Java という後方互換性を重視するプログラミング言語でどう実現しているのかを説明した上で、そのラムダ式が大活躍する Stream API に話が進んでいきます。Stream API も、個々の API については簡潔に、それでいて、どういうときにどうやって使うのかが綺麗に説明されています。
その後、関数型としてのラムダと言えば良いのか、遅延実行から関数の合成、並列化といった具合に話が進みます。ここまでは、どういう使い方であれ Java を使った開発を行う人であれば理解していて損はない、というか理解していないと今後つらいだろうと思われるような内容が続きます。
書いてあることもそうですが、機能の説明順も随分と心を砕いたんじゃないかと思えるくらいに気が効いてます。

ぼく自身は JavaFX には興味がないので読み飛ばしましたが、本当に使いづらかった日付系 API を改善した Date and Time API や、非同期処理のネストを大きく改善する Completable Future、Files/Paths といったユーティリティと、なかなか理解していなかった/存在を知らなかった機能が山積みでした。
それぞれについては、ぼちぼちエントリにまとめていければと思うのですが、とりあえず本当に良い本だったということで。

*1:決して悪い意味ではなく、むしろコードを書くのが楽しくなるような変化だと思う

Loggerはstatic 変数にすべきか、インスタンス変数にすべきか

Logger は static 変数にすべきか、インスタンス変数にすべきかという話があります。

  • static 変数
public class Foo {
  private static Log log = LogFactory.getLog(Foo.class);
  ....
}
  • インスタンス変数
public class Foo {
  private Log log = LogFactory.getLog(Foo.class);
  ....
}

これについては諸説あり、宗教戦争を呼びかねない話ではありますが、ぼくの結論としては、

  1. おまえの作っているアプリケーションがどこで誰に使われる/デプロイされるか分かんないんだったら、とりあえずインスタンス変数にしとけ、な。
  2. おまえしか使わなくて、マジでパフォーマンスをバリバリにチューニングしたいんだったら、static 変数にするのを止めはしません

ということになります。

ロギング Facade 系ライブラリ 二台巨頭の見解

ロギング Facade 系ライブラリの巨頭である SLF4J、Apache Commons Logging それぞれの見解としては、

  • SLF4J
    • メリットとデメリットはまとめましたが、どちらが推奨とかは言いません><
  • Apache Commons Logging
    • 両者でメリットとデメリットがありますが、ほとんどの場合はインスタンス変数の方が良い

ということだと読み取りました。このあたりの議論についてはちょっと後段で触れますが、それぞれ SLF4J FAQhttps://wiki.apache.org/commons/Logging/StaticLog を参照してください。

static Logger の何が悪いのか

前述したとおり、このあたりのメリット、デメリットは [http://www.slf4j.org/faq.html:title=SLF4JのFAQ」にまとめられています。
f:id:kiririmode:20150526164219p:plain
もちろん static Logger にすることで、ロガーを取得する都度必要になるインスタンス生成コストから開放されますし、メモリ効率も良いです。だって、クラスのインスタンス間でロガーは共有されるのですから。
一方で、この「共有」というワードが、デメリットに対して大きく関与してきます。それは、「アプリケーション、および、コンテナ間の共有」です。

このあたりの議論は、むしろ Apache Commons Logging 側のドキュメントに詳しいので参照して頂ければと思うのですが、static 変数として Logger を持つことで対象 Logger がアプリケーション間、および、アプリケーション・コンテナ間で共有されてしまった、以下のようなことが起こり得ると警告されています。

  • To reference an underlying Log object which is part of a hierarchy configured from information at the "container" level, ie not associated with any particular "application"
  • To reference an underlying Log object which is part of a hierarchy configured from information somehow related to the current application
  • To reference a "proxy" object which will determine what the "current application" is each time a log method is invoked, and delegate to the appropriate underlying Log object at that time.
https://wiki.apache.org/commons/Logging/StaticLog

このあたりの内容については、少し補足が必要だと思います。

Java の世界においては、アプリケーションの多くはコンテナ上で動作します。Jetty、Tomcat あたりを想像してください。これらのコンテナは、内部に階層化されたクラスローダを持ち、(基本的には)上位のクラスローダでロード済のクラスは、下位のクラスローダではロードしないという仕組みになっています。*1

ここでもし、static な Logger を持つライブラリ A が、アプリケーション横断のクラスローダ(上の図だと、Common のクラスローダ) でロードされたと仮定しましょう。これにより、static な Logger は、そのコンテナで動作するアプリケーション全てにおいて「ロード済」とみなされます。結果、アプリケーションはすべて同じ Logger を共有し、(例えば)全てのログが同じログファイルに出力されてしまったり、というような悲劇を生みます。

どういう結果を呼ぶかは環境依存です。とある環境では問題がないかもしれませんし、べつの環境では致命的な問題になるかもしれません。このような予測性の欠如は、極めて運用を混乱させるので、安全側、インスタンス変数として Logger を持つという判断をした方が良いんじゃないかなと思います。

*1:Tomcat はちょっと違うけど

SLF4Jとはなにか

SLF4J とは何か。これ、わりと分かりづらい人もいるかと思います。SLF4J の FAQ にそのものズバリな回答があるんですけど、

What is SLF4J?

SLF4J is a simple facade for logging systems allowing the end-user to plug-in the desired logging system at deployment time.


SLF4J って何?

SLF4J っていうのは、デプロイするタイミングで、使いたいロギングシステムをプラグインできるようにする簡易なロギングシステム用 Facade だよ
(意訳)
SLF4J っていうのは、デプロイする時点ではじめて、使いたいロギングのライブラリをバインドできるようにする Facade ライブラリだよ

SLF4J FAQ

ウワッ、何をいっているのかさっぱりわからない、という父兄もいらっしゃることでしょう。これ、知識があればなんとなく理解できるという意味では「バルスのファルシのルシがパージでコクーン」と同じ匂いがします。

Facade とは

プログラミングのコンテキストで出てくる「Facade」というのは、だいたい GoF の Facade パターンを指していると思って間違いないと思います。なんか、wikipedia がわりとまとまっていましたので、そっちを参照してください。

Facade パターンの肝は、ユーザに対してインタフェースを提供しつつサブシステムの実装の詳細を隠して、ユーザを実装から開放することで、SLF4J もその名 (Simple Logging Facade For Java) が示すとおり、Facade として作用します。

SLF4J

はてさて、SLF4J はユーザに対して何の実装を隠すのか、ですが隠す対象は「ロギングシステム」です。

Java には、有名所のロギングシステムとして、log4jlogbackjava.util.logging などがあります。そういう中で、ライブラリ開発者、フレームワーク開発者は、そのライブラリ、あるいはフレームワーク中でログを出力するのに、どのロギングシステムを使えば良いかという選択にさらされていました。

というのも、ライブラリ内部のログ、フレームワーク内部のログと、それらを利用して動作するアプリケーションのロギングシステムは、基本的に同じにしたいというのがユーザの要望だからです。ライブラリ開発者やフレームワーク開発者が勝手にロギングシステムを決めてしまうと、そのロギングシステムの使用をアプリケーション開発者に半ば強制してしまうことになる。

強制してしまうならまだ良い。ライブラリA がロギングシステムA を使用し、ライブラリBがロギングシステムBを使用するようなケースで、ライブラリAとBを利用するアプリケーションCのロギングシステムに何を使ったら良いでしょう、という難題も発生します。これはこまった。そういう中で生まれたのが SLF4J とでも考えれば良い。

SLF4J は、ライブラリ開発者やフレームワーク開発者に対し、「具体的なロギングシステム」を選択させるかわりにその窓口となるロギング用インタフェースを提供することで、「ロギングシステムの選択」という結論の出ないタスクから開放しています。そして、その「ロギングシステムの選択」をアプリケーション開発者の責務にできるような仕組みを持たせています。
このあたりは、公式サイトにある以下の図が分かりやすい。

SLF4J は、その下層にあるロギングシステムのレイヤを、アプリケーションから隠していることが図から見て取れると思います。アプリケーション(ここでは、ライブラリやフレームワークも含みます)は、SLF4J API を叩いておけば良く、その下層のロギングシステムへのブリッジは SLF4J が実施してくれるので、もはやアプリケーションは下層のロギングシステムに何が使われるかを意識することなく、開発を行うことができます。

そして、SLF4J の優れているところは、クラスパスに配置されたロギングシステムの jar に応じて、使うべきロギングシステムを判断してくれることにあります。さきほどの例だと、ライブラリA、ライブラリBの双方が SLF4J を使っていてさえくれれば、アプリケーションCがSLF4Jを通してロギングシステムCを使うことで、ライブラリA、B、アプリケーションCのすべてが、いつのまにかロギングシステムCを使えるようになると。便利ですね。

なお、似たような話に [http://commons.apache.org/proper/commons-logging/:title] があります。