Kotlinの慣用的なロギング

1前書き

このチュートリアルでは、典型的なhttps://www.baeldung.com/category/kotlin/[Kotlin]プログラミングスタイルに適したいくつかのロギングイディオムを見ていきます。

2ロギング熟語

ロギングはプログラミングの至る所で必要とされています。どうやら単純なアイデア(印刷物を印刷するだけ!)ですが、それを行うには多くの方法があります。

実際、すべての言語、オペレーティングシステム、および環境には、独自の慣用的な、場合によっては固有のロギングソリューションがあります。多くの場合、実際には複数あります。

ここでは、コトリンの伐採ストーリーに焦点を当てます。

また、Kotlinの高度な機能を詳しく調べたり、その微妙な違いを探ったりするための前提条件として、ロギングも使用します。

3セットアップ

  • コード例にはhttps://www.slf4j.org/[ SLF4J ]ライブラリを使用しますが、https://www.baeldung.com/log4j2-appenders-layouts-に同じパターンと解決策が適用されますフィルタ[ Log4J ]、https://docs.oracle.com/javase/8/docs/technotes/guides/logging/overview.html[ JUL ]、およびその他のロギングライブラリ。

それで、https://search.maven.org/classic/#search%7C1%7Cg%3A%22org.slf4j%22%20AND%20a%3A%22slf4j-api%22[SLF4J APIを含めることから始めましょう。]およびhttps://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22ch.qos.logback%22[Logback]依存関係は、当社のpomにあります。

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.2.3</version>
</dependency>

それでは、ロギングが4つの異なるアプローチに対してどのように見えるかを見てみましょう。

  • 物件

  • コンパニオンオブジェクト

  • 拡張方法

  • 委任物件

** 4物件としてのロガー+

**

私たちが試すかもしれない最初の事はそれが必要なところはどこでもロガープロパティを宣言することです:

class Property {
    private val logger = LoggerFactory.getLogger(javaClass)

   //...
}

ここでは、定義しているクラス名からロガーの名前を動的に計算するために javaClass を使用しました。このスニペットを好きなところにコピーして貼り付けることができます。

その後、宣言クラスの任意のメソッドでロガーを使用できます。

fun log(s: String) {
    logger.info(s)
}

ロガーを private として宣言することを選択しました。サブクラスを含む他のクラスがそれにアクセスして自分のクラスに代わってログオンすることを望まないためです。

  • もちろん、これはプログラマーにとっては強力な規則ではなく単なるヒント** です。同じ名前のロガーを入手するのは簡単だからです。

4.1. いくつかの入力を保存する

関数への getLogger 呼び出しを因数分解することで、コードを少し短くすることができます。

fun getLogger(forClass: Class<** >): Logger =
  LoggerFactory.getLogger(forClass)

そしてこれをユーティリティクラスに入れることで、以下のサンプルを通して __LoggerFactory.getLogger(javaClass)の代わりに getLogger(javaClass) __getLogを呼び出すことができます。

5コンパニオンオブジェクトのロガー

最後の例はその単純さにおいて強力ですが、それは最も効率的ではありません。

まず、各クラスインスタンスでロガーへの参照を保持するにはメモリがかかります。次に、ロガーがキャッシュされていても、ロガーを持つすべてのオブジェクトインスタンスに対してキャッシュルックアップが発生します。

コンパニオンオブジェクトがそれ以上うまくないかどうかを確認しましょう。

5.1. 最初の試み

Javaでは、ロガーを static として宣言することは、上記の懸念に対処するパターンです。

ただし、Kotlinでは、静的プロパティはありません。

しかし、https://www.baeldung.com/kotlin-objects : でそれらをエミュレートすることができます

class LoggerInCompanionObject {
    companion object {
        private val loggerWithExplicitClass
          = getLogger(LoggerInCompanionObject::class.java)
    }

   //...
}

セクション4.1の getLogger 便利関数を再利用したことに注目してください。私達は記事を通してそれを参照し続けるでしょう。

したがって、上記のコードでは、クラスのどのメソッドでも、以前とまったく同じようにロガーを使用できます。

fun log(s: String) {
    loggerWithExplicitClass.info(s)
}

5.2. javaClass はどうなりましたか?

残念ながら、上記のアプローチには欠点があります。囲んでいるクラスを直接参照しているからです。

LoggerInCompanionObject::class.java

コピー&ペーストのしやすさを失いました。

  • しかし、____javaClassを以前のように使用しないのはなぜですか?** 実際には、できません。持っていれば、コンパニオンオブジェクトのクラスにちなんで名付けられたロガーを誤って取得していたでしょう。

----//Incorrect!
class LoggerInCompanionObject {
    companion object {
        private val loggerWithWrongClass = getLogger(javaClass)
    }
}//...
loggerWithWrongClass.info("test")
----

上記は多少間違ったロガー名を出力します。 $ Companion ビットを見てください。

21:46:36.377[main]INFO
com.baeldung.kotlin.logging.LoggerInCompanionObject$Companion - test

実際、 IntelliJ IDEAは、ロガーの宣言に警告を付けます。 これは、コンパニオンオブジェクト内の javaClass への参照は、おそらく望んでいるものではないと認識しているためです。

5.3. リフレクションを使ったクラス名の導出

それでも、すべてが失われるわけではありません。

クラス名を自動的に導き出して、コードをコピーして貼り付けることができるようにする方法はありますが、そうするには追加のリフレクションが必要です。

まず、https://search.maven.org/classic/#search%7C1%7Cg%3A%22org.jetbrains.kotlin%22%20AND%20a%3A%22kotlin-reflect%22[pomの kotlin-reflect ]依存関係:

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-reflect</artifactId>
    <version>1.2.51</version>
</dependency>

そうすれば、ロギング用の正しいクラス名を動的に取得できます。

companion object {
    @Suppress("JAVA__CLASS__ON__COMPANION")
    private val logger = getLogger(javaClass.enclosingClass)
}//...
logger.info("I feel good!")

正しい出力が得られます。

10:00:32.840[main]INFO
com.baeldung.kotlin.logging.LoggerInCompanionObject - I feel good!
  • enclosingClass ** を使用する理由は、最終的にはコンパニオンオブジェクトが内部クラスのインスタンスになるためです。したがって、 __enclosingClassは外部クラス、またはこの場合は LoggerInCompanionObject__を参照します。

また、IntelliJ IDEAが javaClass に与える警告を抑制しても差し支えありません。これ以降、正しい処理を行っています。

5.4. @ JvmStatic

コンパニオンオブジェクトのプロパティは静的フィールドに似ていますが、コンパニオンオブジェクトはシングルトンに似ています。

Kotlinのコンパニオンオブジェクトは、少なくともJVM上で実行している場合は、https://www.baeldung.com/kotlin-objects[コンパニオンオブジェクトを静的フィールドに変換する]という特別な機能を持っています。

@JvmStatic
private val logger = getLogger(javaClass.enclosingClass)

5.5. すべてを一緒に入れて

3つの改善点すべてをまとめてみましょう。まとめると、これらの改良により私たちのロギング構造はコピーペースト可能で静的になります。

class LoggerInCompanionObject {
    companion object {
        @Suppress("JAVA__CLASS__ON__COMPANION")
        @JvmStatic
        private val logger = getLogger(javaClass.enclosingClass)
    }

    fun log(s: String) {
        logger.info(s)
    }
}

6. 拡張メソッドからのロガー

面白くて効率的ですが、コンパニオンオブジェクトを使うのは冗長です。

ワンライナーとして始まったのは、コードベース全体にコピーアンドペーストするための複数行です。

また、コンパニオンオブジェクトを使用すると、追加の内部クラスが生成されます。 Javaでの単純な静的ロガー宣言と比べて、コンパニオンオブジェクトを使用する方が重いです。

それでは、 拡張メソッド を使ったアプローチを試してみましょう。

6.1. 最初の試み

  • 基本的な考え方は、__Loggerを返す拡張メソッドを定義することです。したがって、それを必要とするすべてのクラスは、そのメソッドを呼び出して正しいインスタンスを取得できます。

これはクラスパスのどこにでも定義できます。

fun <T : Any> T.logger(): Logger = getLogger(javaClass)

拡張メソッドは基本的にそれらが適用されるクラスにコピーされます。そのため、 __javaClass __againを直接参照することができます。

そして今、すべてのクラスは logger メソッドを持つようになります。

class LoggerAsExtensionOnAny {//implied ": Any"
    fun log(s: String) {
        logger().info(s)
    }
}
  • この方法はコンパニオンオブジェクトよりも簡潔ですが、まず最初に問題を解決したいと思うかもしれません。

6.2. Any タイプの汚染

最初の拡張方法の大きな欠点は Any 型が汚染されることです。

我々はこれをあらゆるタイプに適用すると定義したので、やや侵襲的になります。

"foo".logger().info("uh-oh!")//Sample output://13:19:07.826[main]INFO java.lang.String - uh-oh!

Any logger() を定義することで、このメソッドを使用して言語内のすべての型を polluted しました。**

これは必ずしも問題ではありません。他のクラスが独自の logger メソッドを持つことを妨げるものではありません。

ただし、余分なノイズを除いて、カプセル化も破られます。

タイプは互いにログに記録できますが、これは必要ありません。

そして __logger __willは現在、ほぼすべてのIDEコード提案にポップアップします。

** 6.3. マーカーインタフェースの拡張方法

  • マーカーを使って拡張メソッドの範囲を狭めることができます。

interface Logging

このインタフェースを定義したので、この拡張メソッドがこのインタフェースを実装する型にのみ適用されることを示すことができます。

fun <T : Logging> T.logger(): Logger = getLogger(javaClass)

そして今、型を Logging を実装するように変更すれば、以前と同じように logger を使用できます。

class LoggerAsExtensionOnMarkerInterface : Logging {
    fun log(s: String) {
        logger().info(s)
    }
}

** 6.4. 具体化された型パラメータ+

**

最後の2つの例では、リフレクションを使って __javaClass __を取得し、ロガーに識別名を付けます。

ただし、実行時にリフレクションを呼び出すことを避けて、 T typeパラメータからそのような情報を抽出することもできます。これを実現するには、関数を __inline __およびhttps://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters[typeパラメータを指定します]として宣言します。

inline fun <reified T : Logging> T.logger(): Logger =
  getLogger(T::class.java)

これは継承に関してコードの意味論を変えることに注意してください。これについてはセクション8で詳しく説明します。

** 6.5. ロガープロパティと組み合わせる

**

拡張メソッドのいいところは、最初のアプローチと組み合わせることができるということです。

val logger = logger()

** 6.6. コンパニオンオブジェクトと組み合わせる

**

しかし、** 私たちの拡張メソッドをコンパニオンオブジェクトの中で使用したいのであれば、ストーリーはより複雑になります。

companion object : Logging {
    val logger = logger()
}

以前と同じ問題が __javaClass __asにあったからです。

com.baeldung.kotlin.logging.LoggerAsExtensionOnMarkerInterface$Companion

これを説明するために、まずクラスをより確実に取得するメソッドを定義しましょう。

inline fun <T : Any> getClassForLogging(javaClass: Class<T>): Class<** > {
    return javaClass.enclosingClass?.takeIf {
        it.kotlin.companionObject?.java == javaClass
    } ?: javaClass
}

ここで、 getClassForLogging __enclosingClassを返します。 if javaClass__はコンパニオンオブジェクトを表します。

そして今度は拡張メソッドを更新することができます。

inline fun <reified T : Logging> T.logger(): Logger
  = getLogger(getClassForLogging(T::class.java))

このように、ロガーがプロパティとして含まれているかコンパニオンオブジェクトとして含まれているかにかかわらず、実際には同じ拡張メソッドを使用できます。

** 7. 代理プロパティとしてのロガー

最後に、https://www.baeldung.com/kotlin-delegated-properties[del] egated https://www.baeldung.comをご覧ください。/kotlin-delegated-properties[プロパティ]。

このアプローチのいいところは、マーカーインタフェースを必要とせずに名前空間の汚染を回避できることです。

class LoggerDelegate<in R : Any> : ReadOnlyProperty<R, Logger> {
    override fun getValue(thisRef: R, property: KProperty<** >)
     = getLogger(getClassForLogging(thisRef.javaClass))
}

それからプロパティでそれを使うことができます:

private val logger by LoggerDelegate()

getClassForLogging のため、これはコンパニオンオブジェクトに対しても機能します。

companion object {
    val logger by LoggerDelegate()
}

そして委譲されたプロパティは強力ですが、 getValue はプロパティが読み込まれるたびに再計算されることに注意してください

また、デリゲートプロパティが機能するにはリフレクションを使用する必要があることにも注意してください。

8継承についての注意事項

クラスごとに1つのロガーを持つのは非常に一般的です。そのため、通常はロガーも private として宣言しています。

ただし、サブクラスでスーパークラスのロガーを参照したい場合があります

そして私たちのユースケースに応じて、上記の4つのアプローチは異なった振る舞いをします。

一般に、リフレクションやその他の動的機能を使用する場合は、実行時にオブジェクトの実際のクラスを取得します。

しかし、クラスまたは具体化された型パラメータを名前で静的に参照すると、値はコンパイル時に固定されます。

たとえば、委譲プロパティでは、プロパティが読み込まれるたびにロガーインスタンスが動的に取得されるため、使用されているクラスの名前が使用されます。

open class LoggerAsPropertyDelegate {
    protected val logger by LoggerDelegate()
   //...
}

class DelegateSubclass : LoggerAsPropertyDelegate() {
    fun show() {
        logger.info("look!")
    }
}

出力を見てみましょう。

09:23:33.093[main]INFO
com.baeldung.kotlin.logging.DelegateSubclass - look!

logger はスーパークラスで宣言されていますが、サブクラスの名前を出力します。

ロガーがプロパティとして宣言され、 javaClass を使用してインスタンス化された場合も同様です。

そしてtypeパラメータを具体化しない限り、** 拡張メソッドもこの振る舞いをします。

逆に、** 具体化された総称、明示的なクラス名、およびコンパニオンオブジェクトを使用すると、ロガーの名前は型階層全体で同じままになります。

9結論

この記事では、ロガーの宣言とインスタンス化のタスクに適用できるいくつかのKotlinの手法について説明しました。

単純なものから始めて、Kotlinのコンパニオンオブジェクト、拡張メソッド、および委任されたプロパティを見て、効率を改善し定型句を減らすための一連の試みで、徐々に複雑さを増しました。

いつものように、これらの例は完全にhttps://github.com/eugenp/tutorials/tree/master/core-kotlin[GitHubで利用可能]で利用可能です。