Kotlinの慣用的なロギング

Kotlinでの慣用的なロギング

1. 前書き

このチュートリアルでは、典型的なKotlinプログラミングスタイルに適合するいくつかのロギングイディオムを見ていきます。

2. ロギングイディオム

ロギングは、プログラミングにおけるユビキタスなニーズです。 明らかに単純なアイデア(印刷物だけ!)ですが、それを行う方法はたくさんあります。

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

ここでは、Kotlinのロギングストーリーに焦点を当てます。

また、Kotlinの高度な機能に飛び込み、そのニュアンスを探求するための口実としてロギングを使用します。

3. セットアップ

コード例では、SLF4Jライブラリを使用しますが、同じパターンとソリューションがLog4JJUL、およびその他のロギングライブラリに適用されます。

それでは、pomにSLF4J APILogbackの依存関係を含めることから始めましょう。


    org.slf4j
    slf4j-api
    1.7.25


    ch.qos.logback
    logback-classic
    1.2.3


    ch.qos.logback
    logback-core
    1.2.3

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

  • プロパティ

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

  • 拡張メソッド、および

  • 委任されたプロパティ

4. プロパティとしてのロガー

最初に試すことは、ロガープロパティを必要な場所で宣言することです。

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

    //...
}

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

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

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

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

同じ名前のロガーを簡単に取得できるため、強力に適用されるルールではなく、プログラマー向けのOf course, this is merely a hint

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

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

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

そして、これをユーティリティクラスに配置することにより、we can now simply call getLogger(javaClass) instead of LoggerFactory.getLogger(javaClass) throughout the samples below.

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

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

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

コンパニオンオブジェクトの方がうまくいくかどうか見てみましょう。

5.1. 最初の試み

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

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

しかし、https://www.example.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

コピーと貼り付けのしやすさが失われました。

But why not just use javaClass like before?実際にはできません。 もしそうなら、the companion object’sクラスにちなんで名付けられたロガーを誤って取得していたでしょう:

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

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

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

実際、IntelliJ IDEA marks the declaration of the logger with a warning,は、コンパニオンオブジェクト内のjavaClassへの参照が、おそらく私たちが望むものではないことを認識しているためです。

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

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

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

まず、pomにkotlin-reflectの依存関係があることを確認しましょう。


    org.jetbrains.kotlin
    kotlin-reflect
    1.2.51

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

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.example.kotlin.logging.LoggerInCompanionObject - I feel good!

The reason we use enclosingClass は、最終的にコンパニオンオブジェクトが内部クラスのインスタンスであるという事実に由来するため、enclosingClass は外部クラス、この場合はLoggerInCompanionObjectを参照します。

また、IntelliJ IDEAがjavaClassに与える警告を抑制しても問題ありません。これは、正しいことを行っているためです。

5.4. @JvmStatic

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

Kotlinコンパニオンオブジェクトには、少なくともJVMで実行している場合、converts companion objects to static fieldsという特別な機能があります。

@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の単純な静的ロガー宣言と比較すると、コンパニオンオブジェクトの使用はより重いです。

それでは、extension methodsを使用したアプローチを試してみましょう。

6.1. 最初の試み

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

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

fun  T.logger(): Logger = getLogger(javaClass)

拡張メソッドは基本的に、それらが適用可能なすべてのクラスにコピーされます。したがって、javaClass againを直接参照できます。

そして今、すべてのクラスは、次のタイプで定義されているかのように、メソッドloggerを持ちます。

class LoggerAsExtensionOnAny { // implied ": Any"
    fun log(s: String) {
        logger().info(s)
    }
}

While this approach is more concise than companion objects,最初に問題を解決したい場合があります。

6.2. Anyタイプの汚染

最初の拡張メソッドの重大な欠点は、Anyタイプを汚染することです。

すべてのタイプに適用するものとして定義したため、最終的には少し侵襲的になります。

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

Anylogger()を定義することにより、メソッドを使用する言語のすべてのタイプをpollutedにしました。

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

ただし、余分なノイズは別として、it also breaks encapsulationです。 タイプは相互にログを記録できるようになりましたが、これは望ましくありません。

また、logger は、ほぼすべてのIDEコードの提案でポップアップ表示されるようになりました。

6.3. マーカーインターフェイスの拡張メソッド

We can narrow our extension method’s scope with a markerinterface

interface Logging

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

fun  T.logger(): Logger = getLogger(javaClass)

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

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

6.4. 改良型パラメータ

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

ただし、このような情報をT型パラメーターから抽出して、実行時のリフレクション呼び出しを回避することもできます。 これを実現するために、関数をinline およびreify the type parameterとして宣言します。

inline fun  T.logger(): Logger =
  getLogger(T::class.java)

これにより、継承に関してコードのセマンティクスが変更されることに注意してください。 これについては、セクション8で詳しく説明します。

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

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

val logger = logger()

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

ただし、use our extension method in a companion objectが必要な場合、話はもっと複雑になります。

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

以前と同じ問題がjavaClass asで発生するため:

com.example.kotlin.logging.LoggerAsExtensionOnMarkerInterface$Companion

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

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

ここで、getClassForLoggingenclosingClass ifを返します。javaClassはコンパニオンオブジェクトを参照します。

そして今、私たちは再び拡張メソッドを更新できます:

inline fun  T.logger(): Logger
  = getLogger(getClassForLogging(T::class.java))

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

7. 委任されたプロパティとしてのロガー

最後に、delhttps://www.example.com/kotlin-delegated-properties [egated]propertiesを見てみましょう。

このアプローチの良いところは、we avoid namespace pollution without requiring a marker interface

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

その後、プロパティで使用できます。

private val logger by LoggerDelegate()

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

companion object {
    val logger by LoggerDelegate()
}

委任されたプロパティは強力ですが、getValue is re-computed each time the property is readに注意してください。

また、それが機能するためには、delegate properties must use reflectionであることを覚えておく必要があります。

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.example.kotlin.logging.DelegateSubclass - look!

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

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

そしてextension methods exhibit this behavior, too, unless we reify the type parameter.

逆に、with reified generics, explicit class names and companion objects, a logger’s name stays the same across the type hierarchy.

9. 結論

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

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

いつものように、これらの例は完全なover on GitHubで利用できます。