Manière idiomatique de se connecter à Kotlin

Kotlin n’a pas la même notion de champs statiques que ceux utilisés en Java. En Java, la méthode de journalisation généralement acceptée est la suivante:

public class Foo { private static final Logger LOG = LoggerFactory.getLogger(Foo.class); } 

La question est la suivante: quelle est la manière idiomatique d’exécuter une journalisation dans Kotlin?

Dans la majorité du code Kotlin mature, vous trouverez l’un de ces modèles ci-dessous. L’approche utilisant les delegates de propriété profite de la puissance de Kotlin pour produire le code le plus petit.

Note: le code est ici pour java.util.Logging mais la même théorie s’applique à toute bibliothèque de journalisation

Static-like (commun, équivalent de votre code Java dans la question)

Si vous ne pouvez pas vous fier aux performances de cette recherche de hachage dans le système de journalisation, vous pouvez obtenir un comportement similaire à votre code Java en utilisant un object compagnon pouvant contenir une instance et vous sembler statique.

 class MyClass { companion object { val LOG = Logger.getLogger(MyClass::class.java.name) } fun foo() { LOG.warning("Hello from MyClass") } } 

création de sortie:

26 déc. 2015 11:28:32 org.stackoverflow.kotlin.test.MyClass foo INFO: Bonjour de MyClass

Plus d’informations sur les objects compagnons ici: Objets compagnons … Notez également que dans l’exemple ci-dessus, MyClass::class.java obtient l’instance de type Class pour le this.javaClass , alors que this.javaClass obtient l’instance de type Class .

Par instance d’une classe (commune)

Mais, il n’ya vraiment aucune raison d’éviter d’appeler et d’obtenir un enregistreur au niveau de l’instance. La manière idiomatique de Java que vous avez mentionnée est obsolète et basée sur la crainte de la performance, alors que l’enregistreur par classe est déjà mis en cache par presque tous les systèmes de journalisation raisonnables de la planète. Créez simplement un membre pour contenir l’object enregistreur.

 class MyClass { val LOG = Logger.getLogger(this.javaClass.name) fun foo() { LOG.warning("Hello from MyClass") } } 

création de sortie:

Dec 26, 2015 11:28:44 org.stackoverflow.kotlin.test.MyClass foo INFO: Bonjour de MyClass

Vous pouvez tester les performances à la fois par instance et par classe et voir s’il existe une différence réaliste pour la plupart des applications.

Délégués de propriété (communs, plus élégants)

Une autre approche, suggérée par @Jire dans une autre réponse, consiste à créer un délégué de propriété, que vous pouvez ensuite utiliser pour exécuter la logique de manière uniforme dans toute autre classe souhaitée. Il y a un moyen plus simple de faire cela puisque Kotlin fournit déjà un délégué Lazy , nous pouvons simplement l’envelopper dans une fonction. Une astuce ici est que si nous voulons connaître le type de la classe qui utilise actuellement le délégué, nous en faisons une fonction d’extension sur n’importe quelle classe:

 fun  R.logger(): Lazy { return lazy { Logger.getLogger(unwrapCompanionClass(this.javaClass).name) } } // see code for unwrapCompanionClass() below in "Putting it all Together section" 

Ce code garantit également que si vous l’utilisez dans un object compagnon, le nom du consignateur sera le même que si vous l’utilisiez sur la classe elle-même. Maintenant, vous pouvez simplement:

 class Something { val LOG by logger() fun foo() { LOG.info("Hello from Something") } } 

pour chaque instance de classe, ou si vous voulez qu’il soit plus statique avec une instance par classe:

 class SomethingElse { companion object { val LOG by logger() } fun foo() { LOG.info("Hello from SomethingElse") } } 

Et votre sortie d’appeler foo() sur ces deux classes serait:

Dec 26, 2015 11:30:55 org.stackoverflow.kotlin.test.Quelque chose de louche INFO: Bonjour de quelque chose

Dec 26, 2015 11:30:55 org.stackoverflow.kotlin.test.SomethingElse foo INFO: Bonjour de SomethingElse

Fonctions d’extension (rare dans ce cas en raison de la “pollution” de n’importe quel espace de noms)

Kotlin a quelques astuces cachées qui vous permettent de réduire une partie de ce code. Vous pouvez créer des fonctions d’extension sur les classes et leur donner ainsi des fonctionnalités supplémentaires. Une suggestion dans les commentaires ci-dessus était d’étendre Any avec une fonction de journalisation. Cela peut créer du bruit chaque fois que quelqu’un utilise l’achèvement du code dans son IDE sur n’importe quelle classe. Mais il y a un avantage secret à étendre Any ou une autre interface de marqueur: vous pouvez impliquer que vous étendez votre propre classe et donc détectez la classe dans laquelle vous vous trouvez. Hein? Pour être moins déroutant, voici le code:

 // extend any class with the ability to get a logger fun  T.logger(): Logger { return Logger.getLogger(unwrapCompanionClass(this.javaClass).name) } 

Maintenant, dans une classe (ou un object compagnon), je peux simplement appeler cette extension sur ma propre classe:

 class SomethingDifferent { val LOG = logger() fun foo() { LOG.info("Hello from SomethingDifferent") } } 

Production de sortie:

Dec 26, 2015 11:29:12 org.stackoverflow.kotlin.test.Quelque choseDifférent foo INFO: Bonjour de SomethingDifferent

Fondamentalement, le code est considéré comme un appel à l’extension Something.logger() . Le problème est que ce qui suit pourrait également être une source de “pollution” sur d’autres classes:

 val LOG1 = "".logger() val LOG2 = Date().logger() val LOG3 = 123.logger() 

Fonctions d’extension sur l’interface de marqueur (pas sûr de la fréquence, mais modèle commun pour les “traits”)

Pour rendre l’utilisation d’extensions plus propres et réduire la “pollution”, vous pouvez utiliser une interface de marqueur pour étendre:

 interface Loggable {} fun Loggable.logger(): Logger { return Logger.getLogger(unwrapCompanionClass(this.javaClass).name) } 

Ou même faire de la méthode une partie de l’interface avec une implémentation par défaut:

 interface Loggable { public fun logger(): Logger { return Logger.getLogger(unwrapCompanionClass(this.javaClass).name) } } 

Et utilisez l’une de ces variantes dans votre classe:

 class MarkedClass: Loggable { val LOG = logger() } 

Production de sortie:

Dec 26, 2015 11:41:01 org.stackoverflow.kotlin.test.MarkedClass foo INFO: Bonjour de MarkedClass

Si vous voulez forcer la création d’un champ uniforme pour contenir l’enregistreur, alors, en utilisant cette interface, vous pourriez facilement demander à l’implémenteur d’avoir un champ tel que LOG :

 interface Loggable { val LOG: Logger // abstract required field public fun logger(): Logger { return Logger.getLogger(unwrapCompanionClass(this.javaClass).name) } } 

Maintenant, l’implémenteur de l’interface doit ressembler à ceci:

 class MarkedClass: Loggable { override val LOG: Logger = logger() } 

Bien sûr, une classe de base abstraite peut faire la même chose, car l’option à la fois de l’interface et d’une classe abstraite implémentant cette interface permet une flexibilité et une uniformité:

 abstract class WithLogging: Loggable { override val LOG: Logger = logger() } // using the logging from the base class class MyClass1: WithLogging() { // ... already has logging! } // providing own logging compatible with marker interface class MyClass2: ImportantBaseClass(), Loggable { // ... has logging that we can understand, but doesn't change my hierarchy override val LOG: Logger = logger() } // providing logging from the base class via a companion object so our class hierarchy is not affected class MyClass3: ImportantBaseClass() { companion object : WithLogging() { // we have the LOG property now! } } 

Tout rassembler (une petite bibliothèque d’aide)

Voici une petite bibliothèque d’aide pour faciliter l’utilisation des options ci-dessus. Il est courant dans Kotlin d’étendre les API pour les rendre plus à votre goût. Soit dans les fonctions d’extension ou de haut niveau. Voici un mélange pour vous donner des options sur la façon de créer des enregistreurs et un exemple montrant toutes les variantes:

 // Return logger for Java class, if companion object fix the name fun  logger(forClass: Class): Logger { return Logger.getLogger(unwrapCompanionClass(forClass).name) } // unwrap companion class to enclosing class given a Java Class fun  unwrapCompanionClass(ofClass: Class): Class< *> { return ofClass.enclosingClass?.takeIf { ofClass.enclosingClass.kotlin.companionObject?.java == ofClass } ?: ofClass } // unwrap companion class to enclosing class given a Kotlin Class fun  unwrapCompanionClass(ofClass: KClass): KClass< *> { return unwrapCompanionClass(ofClass.java).kotlin } // Return logger for Kotlin class fun  logger(forClass: KClass): Logger { return logger(forClass.java) } // return logger from extended class (or the enclosing class) fun  T.logger(): Logger { return logger(this.javaClass) } // return a lazy logger property delegate for enclosing class fun  R.lazyLogger(): Lazy { return lazy { logger(this.javaClass) } } // return a logger property delegate for enclosing class fun  R.injectLogger(): Lazy { return lazyOf(logger(this.javaClass)) } // marker interface and related extension (remove extension for Any.logger() in favour of this) interface Loggable {} fun Loggable.logger(): Logger = logger(this.javaClass) // abstract base class to provide logging, intended for companion objects more than classes but works for either abstract class WithLogging: Loggable { val LOG = logger() } 

Choisissez celui que vous souhaitez conserver, et voici toutes les options utilisées:

 class MixedBagOfTricks { companion object { val LOG1 by lazyLogger() // lazy delegate, 1 instance per class val LOG2 by injectLogger() // immediate, 1 instance per class val LOG3 = logger() // immediate, 1 instance per class val LOG4 = logger(this.javaClass) // immediate, 1 instance per class } val LOG5 by lazyLogger() // lazy delegate, 1 per instance of class val LOG6 by injectLogger() // immediate, 1 per instance of class val LOG7 = logger() // immediate, 1 per instance of class val LOG8 = logger(this.javaClass) // immediate, 1 instance per class } val LOG9 = logger(MixedBagOfTricks::class) // top level variable in package // or alternative for marker interface in class class MixedBagOfTricks : Loggable { val LOG10 = logger() } // or alternative for marker interface in companion object of class class MixedBagOfTricks { companion object : Loggable { val LOG11 = logger() } } // or alternative for abstract base class for companion object of class class MixedBagOfTricks { companion object: WithLogging() {} // instance 12 fun foo() { LOG.info("Hello from MixedBagOfTricks") } } // or alternative for abstract base class for our actual class class MixedBagOfTricks : WithLogging() { // instance 13 fun foo() { LOG.info("Hello from MixedBagOfTricks") } } 

Les 13 instances des enregistreurs créés dans cet exemple produiront le même nom de journal et produiront:

Dec 26, 2015 11:39:00 org.stackoverflow.kotlin.test.MixedBagOfTricks foo INFO: Bonjour de MixedBagOfTricks

Remarque: La méthode unwrapCompanionClass() garantit que nous ne générons pas de journal nommé après l’object compagnon, mais plutôt la classe englobante. C’est la méthode recommandée pour trouver la classe contenant l’object compagnon. Supprimer ” $ Companion ” du nom en utilisant removeSuffix() ne fonctionne pas, car les objects compagnon peuvent recevoir des noms personnalisés.

Jetez un coup d’œil à la bibliothèque de kotlin-logging .
Cela permet de se connecter comme ça:

 private val logger = KotlinLogging.logger {} class Foo { logger.info{"wohoooo $wohoooo"} } 

Ou comme ça:

 class FooWithLogging { companion object: KLogging() fun bar() { logger.info{"wohoooo $wohoooo"} } } 

J’ai également écrit un article de blog en le comparant à AnkoLogger : AnkoLogger à Kotlin et Android: AnkoLogger vs Kotlin-logging

Disclaimer: Je suis le responsable de cette bibliothèque.

Edit: kotlin-logging a maintenant un support multiplateforme: https://github.com/MicroUtils/kotlin-logging/wiki/Multiplatform-support

Comme un bon exemple d’implémentation de la journalisation, je voudrais mentionner Anko qui utilise une interface spéciale AnkoLogger qu’une classe qui doit enregistrer doit implémenter. À l’intérieur de l’interface, il y a du code qui génère une balise de journalisation pour la classe. La journalisation se fait ensuite via des fonctions d’extension qui peuvent être appelées dans l’implémentation interace sans préfixes ni même création de l’instance de journalisation.

Je ne pense pas que cela soit idiomatique , mais cela semble être une bonne approche car elle nécessite un code minimum, en ajoutant simplement l’interface à une déclaration de classe, et vous obtenez une journalisation avec différentes balises pour différentes classes.


Le code ci-dessous est fondamentalement AnkoLogger , simplifié et réécrit pour une utilisation indépendante de Android.

Tout d’abord, il y a une interface qui se comporte comme une interface de marqueur:

 interface MyLogger { val tag: Ssortingng get() = javaClass.simpleName } 

Il permet à son implémentation d’utiliser les fonctions d’extension de MyLogger dans son code, en les appelant uniquement à this . Et il contient également une balise de journalisation.

Ensuite, il existe un point d’entrée général pour différentes méthodes de journalisation:

 private inline fun log(logger: MyLogger, message: Any?, throwable: Throwable?, level: Int, handler: (Ssortingng, Ssortingng) -> Unit, throwableHandler: (Ssortingng, Ssortingng, Throwable) -> Unit ) { val tag = logger.tag if (isLoggingEnabled(tag, level)) { val messageSsortingng = message?.toSsortingng() ?: "null" if (throwable != null) throwableHandler(tag, messageSsortingng, throwable) else handler(tag, messageSsortingng) } } 

Il sera appelé par les méthodes de journalisation. Il obtient une balise de l’implémentation MyLogger , vérifie les parameters de journalisation, puis appelle l’un des deux gestionnaires, l’un avec l’argument Throwable et l’autre sans.

Ensuite, vous pouvez définir autant de méthodes de journalisation que vous le souhaitez, de cette manière:

 fun MyLogger.info(message: Any?, throwable: Throwable? = null) = log(this, message, throwable, LoggingLevels.INFO, { tag, message -> println("INFO: $tag # $message") }, { tag, message, thr -> println("INFO: $tag # $message # $throwable"); thr.printStackTrace() }) 

Celles-ci sont définies une fois pour la simple journalisation d’un message et la journalisation d’un Throwable , ceci avec un paramètre throwable .

Les fonctions transmises en tant que handler et throwableHandler peuvent être différentes pour différentes méthodes de journalisation. Par exemple, elles peuvent écrire le journal dans un fichier ou le télécharger quelque part. isLoggingEnabled et LoggingLevels sont omis pour des raisons de brièveté, mais leur utilisation offre encore plus de flexibilité.


Il permet l’utilisation suivante:

 class MyClass : MyLogger { fun myFun() { info("Info message") } } 

Il y a un petit inconvénient: un object de journalisation sera nécessaire pour se connecter aux fonctions au niveau du package:

 private object MyPackageLog : MyLogger fun myFun() { MyPackageLog.info("Info message") } 

Est-ce que quelque chose comme ça fonctionnerait pour vous?

 class LoggerDelegate { private var logger: Logger? = null operator fun getValue(thisRef: Any?, property: KProperty< *>): Logger { if (logger == null) logger = Logger.getLogger(thisRef!!.javaClass.name) return logger!! } } fun logger() = LoggerDelegate() class Foo { // (by the way, everything in Kotlin is public by default) companion object { val logger by logger() } } 

Anko

Vous pouvez utiliser la bibliothèque Anko pour le faire. Vous auriez le code comme ci-dessous:

 class MyActivity : Activity(), AnkoLogger { private fun someMethod() { info("This is my first app and it's awesome") debug(1234) warn("Warning") } } 

kotlin-journalisation

La bibliothèque kotlin-logging ( projet Github – kotlin-logging ) vous permet d’écrire un code de consignation comme ci-dessous:

 class FooWithLogging { companion object: KLogging() fun bar() { logger.info{"Item $item"} } } 

StaticLog

ou vous pouvez aussi utiliser ce petit écrit dans la bibliothèque de Kotlin appelée StaticLog alors votre code ressemblerait à StaticLog :

 Log.info("This is an info message") Log.debug("This is a debug message") Log.warn("This is a warning message","WithACustomTag") Log.error("This is an error message with an additional Exception for output", "AndACustomTag", exception ) Log.logLevel = LogLevel.WARN Log.info("This message will not be shown")\ 

La deuxième solution pourrait être préférable si vous souhaitez définir un format de sortie pour la méthode de journalisation, comme par exemple:

 Log.newFormat { line(date("yyyy-MM-dd HH:mm:ss"), space, level, text("/"), tag, space(2), message, space(2), occurrence) } 

ou utilisez des filtres, par exemple:

 Log.filterTag = "filterTag" Log.info("This log will be filtered out", "otherTag") Log.info("This log has the right tag", "filterTag") 

timberkt

Si vous aviez déjà utilisé la bibliothèque de journalisation Timber Jake Wharton, vérifiez timberkt .

Cette bibliothèque repose sur Timber avec une API plus facile à utiliser depuis Kotlin. Au lieu d’utiliser des parameters de mise en forme, vous passez un lambda qui n’est évalué que si le message est consigné.

Exemple de code:

 // Standard timber Timber.d("%d %s", intVar + 3, ssortingngFun()) // Kotlin extensions Timber.d { "${intVar + 3} ${ssortingngFun()}" } // or d { "${intVar + 3} ${ssortingngFun()}" } 

Vérifiez aussi: Connexion à Kotlin & Android: AnkoLogger vs kotlin-logging

J’espère que ça aidera

KISS: pour les équipes Java migrant vers Kotlin

Si cela ne vous dérange pas de fournir le nom de la classe sur chaque instanciation de l’enregistreur (tout comme java), vous pouvez restr simple en le définissant comme une fonction de niveau supérieur quelque part dans votre projet:

 import org.slf4j.LoggerFactory inline fun  logger() = LoggerFactory.getLogger(T::class.java) 

Cela utilise un paramètre de type réifié Kotlin.

Maintenant, vous pouvez utiliser ceci comme suit:

 class SomeClass { // or within a companion object for one-instance-per-class val log = logger() ... } 

Cette approche est super simple et proche de l’équivalent java, mais ajoute simplement du sucre syntaxique.

Étape suivante: extensions ou delegates

Personnellement, je préfère aller plus loin et utiliser l’approche des extensions ou des delegates. Ceci est bien résumé dans la réponse de @ JaysonMinard, mais voici le TL; DR pour l’approche “Delegate” avec l’API log4j2. Comme log4j2, contrairement à slf4j, prend en charge la journalisation avec le Supplier , j’ai également ajouté un délégué pour simplifier l’utilisation de ces méthodes.

 import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger import org.apache.logging.log4j.util.Supplier import kotlin.reflect.companionObject /** * An adapter to allow cleaner syntax when calling a logger with a Kotlin lambda. Otherwise calling the * method with a lambda logs the lambda itself, and not its evaluation. We specify the Lambda SAM type as a log4j2 `Supplier` * to avoid this. Since we are using the log4j2 api here, this does not evaluate the lambda if the level * is not enabled. */ class FunctionalLogger(val log: Logger): Logger by log { inline fun debug(crossinline supplier: () -> Ssortingng) { log.debug(Supplier { supplier.invoke() }) } inline fun debug(t: Throwable, crossinline supplier: () -> Ssortingng) { log.debug(Supplier { supplier.invoke() }, t) } inline fun info(crossinline supplier: () -> Ssortingng) { log.info(Supplier { supplier.invoke() }) } inline fun info(t: Throwable, crossinline supplier: () -> Ssortingng) { log.info(Supplier { supplier.invoke() }, t) } inline fun warn(crossinline supplier: () -> Ssortingng) { log.warn(Supplier { supplier.invoke() }) } inline fun warn(t: Throwable, crossinline supplier: () -> Ssortingng) { log.warn(Supplier { supplier.invoke() }, t) } inline fun error(crossinline supplier: () -> Ssortingng) { log.error(Supplier { supplier.invoke() }) } inline fun error(t: Throwable, crossinline supplier: () -> Ssortingng) { log.error(Supplier { supplier.invoke() }, t) } } /** * A delegate-based lazy logger instantiation. Use: `val log by logger()`. */ @Suppress("unused") inline fun  T.logger(): Lazy = lazy { FunctionalLogger(LogManager.getLogger(unwrapCompanionClass(T::class.java))) } // unwrap companion class to enclosing class given a Java Class fun  unwrapCompanionClass(ofClass: Class): Class< *> { return if (ofClass.enclosingClass != null && ofClass.enclosingClass.kotlin.companionObject?.java == ofClass) { ofClass.enclosingClass } else { ofClass } } 

Je n’ai entendu parler d’aucun idiome à cet égard. Le plus simple sera le mieux, donc je voudrais utiliser une propriété de niveau supérieur

 val logger = Logger.getLogger("package_name") 

Cette pratique sert bien en Python, et aussi différente que Kotlin et Python puissent apparaître, je crois qu’ils sont assez similaires dans “l’esprit” (en parlant d’idiomes).

Qu’en est-il d’une fonction d’extension sur Class à la place? De cette façon, vous vous retrouvez avec:

 public fun KClass.logger(): Logger = LoggerFactory.getLogger(this.java) class SomeClass { val LOG = SomeClass::class.logger() } 

Remarque – Je n’ai pas testé cela du tout, alors ce n’est peut-être pas tout à fait correct.

Tout d’abord, vous pouvez append des fonctions d’extension pour la création de l’enregistreur.

 inline fun  getLogger() = LoggerFactory.getLogger(T::class.java) fun  T.getLogger() = LoggerFactory.getLogger(javaClass) 

Ensuite, vous pourrez créer un enregistreur en utilisant le code suivant.

 private val logger1 = getLogger() private val logger2 = getLogger() 

Deuxièmement, vous pouvez définir une interface qui fournit un enregistreur et son implémentation en mixin.

 interface LoggerAware { val logger: Logger } class LoggerAwareMixin(containerClass: Class< *>) : LoggerAware { override val logger: Logger = LoggerFactory.getLogger(containerClass) } inline fun  loggerAware() = LoggerAwareMixin(T::class.java) 

Cette interface peut être utilisée de la manière suivante.

 class SomeClass : LoggerAware by loggerAware() { // Now you can use a logger here. } 

C’est à cela que servent les objects compagnons, en général: remplacer des éléments statiques.

Slf4j exemple, même pour les autres. Cela fonctionne même pour créer un enregistreur de niveau de paquet

 /** * Get logger by current class name. */ fun getLogger(c: () -> Unit): Logger = LoggerFactory.getLogger(c.javaClass.enclosingClass) 

Usage:

 val logger = getLogger { } 

créer un object compagnon et marquer les champs appropriés avec l’annotation @JvmStatic

 fun  R.logger(): Lazy = lazy { LoggerFactory.getLogger((if (javaClass.kotlin.isCompanion) javaClass.enclosingClass else javaClass).name) } class Foo { val logger by logger() } class Foo { companion object { val logger by logger() } }