Kotlinでの慣用的なロギング
1. 序章
このチュートリアルでは、一般的なKotlinプログラミングスタイルに適合するいくつかのロギングイディオムを見ていきます。
2. ロギングイディオム
ロギングはプログラミングにおいて遍在する必要性です。 明らかに単純なアイデアですが(印刷するだけです!)、それを行う方法はたくさんあります。
実際、すべての言語、オペレーティングシステム、および環境には、独自の慣用的な、場合によっては慣用的なロギングソリューションがあります。 多くの場合、実際には、複数あります。
ここでは、Kotlinのロギングストーリーに焦点を当てます。
また、いくつかの高度なKotlin機能に飛び込み、それらのニュアンスを探求するための口実としてロギングを使用します。
3. 設定
コード例では、 SLF4J ライブラリを使用しますが、同じパターンとソリューションが Log4J 、 JUL 、およびその他のログライブラリに適用されます。
それでは、 SLF4JAPIとLogbackの依存関係をpomに含めることから始めましょう。
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</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)
}
ロガーをプライベートとして宣言することを選択しました。これは、サブクラスを含む他のクラスがロガーにアクセスして、クラスに代わってログオンすることを望まないためです。
もちろん、これは、同じ名前のロガーを簡単に入手できるため、強力に適用されるルールではなく、プログラマーにとってのヒントにすぎません。
4.1. 入力を節約する
関数へのgetLogger呼び出しを因数分解することで、コードを少し短くすることができます。
fun getLogger(forClass: Class<*>): Logger =
LoggerFactory.getLogger(forClass)
そして、これをユーティリティクラスに配置することで、以下のサンプル全体でLoggerFactory.getLogger(javaClass)の代わりにgetLogger(javaClass)を呼び出すことができるようになりました。
5. コンパニオンオブジェクトのロガー
最後の例はその単純さにおいて強力ですが、最も効率的ではありません。
まず、各クラスインスタンスでロガーへの参照を保持するには、メモリが必要です。 次に、ロガーがキャッシュされている場合でも、ロガーを持つすべてのオブジェクトインスタンスに対してキャッシュルックアップが発生します。
コンパニオンオブジェクトがうまくいくかどうかを見てみましょう。
5.1. 最初の試み
Javaでは、ロガーを static として宣言することは、上記の懸念に対処するパターンです。
ただし、Kotlinには静的プロパティがありません。
ただし、コンパニオンオブジェクト :を使用してそれらをエミュレートできます。
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. リフレクションによるクラス名の導出
それでも、すべてが失われるわけではありません。
クラス名を自動的に導出し、コードをコピーして貼り付ける機能を復元する方法はありますが、そのためには追加のリフレクションが必要です。
まず、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。
また、IntelliJIDEAがjavaClass で発する警告を抑制しても問題ありません。これは、正しいことを行っているためです。
5.4. @JvmStatic
コンパニオンオブジェクトのプロパティlookは静的フィールドに似ていますが、コンパニオンオブジェクトはシングルトンに似ています。
Kotlinコンパニオンオブジェクトには特別な機能がありますが、少なくともJVMで実行している場合は、コンパニオンオブジェクトを静的フィールドに変換します。
@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を再度直接参照するだけです。
そして今、すべてのクラスは、次のタイプで定義されているかのように、メソッドloggerを持ちます。
class LoggerAsExtensionOnAny { // implied ": Any"
fun log(s: String) {
logger().info(s)
}
}
このアプローチはコンパニオンオブジェクトよりも簡潔ですが、最初にいくつかの問題を解決したい場合があります。
6.2. 任意のタイプの汚染
最初の拡張方法の重大な欠点は、Anyタイプを汚染することです。
あらゆるタイプに適用するものとして定義したため、最終的には少し侵襲的です。
"foo".logger().info("uh-oh!")
// Sample output:
// 13:19:07.826 [main] INFO java.lang.String - uh-oh!
Anyでlogger()を定義することにより、このメソッドを使用して、言語内のすべてのタイプを汚染しました。
これは必ずしも問題ではありません。 他のクラスが独自のロガーメソッドを持つことを妨げることはありません。
ただし、余分なノイズは別として、カプセル化を破ります。 タイプは相互にログを記録できるようになりましたが、これは望ましくありません。
また、ロガーは、ほぼすべての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 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 でも以前と同じ問題が発生するため、次のようになります。
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 は、 javaClass がコンパニオンオブジェクトを参照している場合、enclosingClassを返します。
そして今、私たちは再び私たちの拡張メソッドを更新することができます:
inline fun <reified T : Logging> T.logger(): Logger
= getLogger(getClassForLogging(T::class.java))
このように、ロガーがプロパティまたはコンパニオンオブジェクトとして含まれているかどうかに関係なく、実際に同じ拡張メソッドを使用できます。
7. 委任されたプロパティとしてのロガー
このアプローチの優れている点は、マーカーインターフェイスを必要とせずに名前空間の汚染を回避できることです:
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コンパニオンオブジェクト、拡張メソッド、および委任されたプロパティを調べて、徐々に複雑さを増していきました。
いつものように、これらの例はGitHubで完全に利用できます。