Ідіоматичний спосіб входу в Котлін


164

У Котліна немає того самого поняття статичних полів, що і в Java. У Java загальноприйнятим способом ведення журналу є:

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

Питання в тому, який ідіоматичний спосіб виконання журналу в Котліні?


1
Не розміщуючи це як відповідь, це дуже далеко від шляху Java, але я розглядав можливість розширення функції для будь-якого для реєстрації. Вам, звичайно, потрібно кешувати лісоруби, але я думаю, що це було б гарним способом зробити це.
mhlz

1
@mhlz Чи не буде ця функція розширення статично вирішена? Як і в, він не застосовуватиметься до всіх об'єктів, лише до типів Any(таким чином, потрібен амплуа)?
Джире

1
@mhlz функція розширення не має сенсу, оскільки у неї не буде стану, щоб тримати реєстратор. Це може бути розширенням для повернення реєстратора, але навіщо це робити кожному відомому класу в системі? Якщо розміщення розширень на будь-який, як правило, пізніше стає неохайним шумом в IDE. @Jire розширення буде стосуватися всіх нащадків Будь-якого, все одно поверне правильне this.javaClassдля кожного. Але я не рекомендую це як рішення.
Джейсон Мінард

Відповіді:


250

У більшості зрілих кодів Котліна ви знайдете одну з цих моделей нижче. Підхід, що використовує власників делегатів, використовує потужність Котліна для створення найменшого коду.

Примітка: код тут є, java.util.Loggingале ця ж теорія застосовується до будь-якої бібліотеки журналів

Статичний (звичайний, еквівалент коду Java у запитанні)

Якщо ви не можете довіряти виконанню цього хеш-пошуку в системі реєстрації, ви можете отримати подібну поведінку до свого Java-коду, використовуючи супутні об’єкти, які можуть містити екземпляр і вважати себе статичним.

class MyClass {
    companion object {
        val LOG = Logger.getLogger(MyClass::class.java.name) 
    }

    fun foo() {
        LOG.warning("Hello from MyClass")
    }
}  

створення результату:

26 грудня 2015 11:28:32 ранку org.stackoverflow.kotlin.test.MyClassINFO: Привіт від MyClass

Детальніше про супутні об’єкти тут: Об'єкти супутника ... Також зверніть увагу, що у наведеному вище прикладі MyClass::class.javaотримується екземпляр типу Class<MyClass>для реєстратора, тоді як this.javaClassотримав би екземпляр типу Class<MyClass.Companion>.

По інстанції класу (загальний)

Але насправді немає причин уникати дзвінків та отримання реєстратора на рівні екземпляра. Ідіоматичний спосіб Яви, який ви згадали, застарів і базується на страхуванні продуктивності, тоді як реєстратор кожного класу вже кешований практично будь-якою розумною системою лісозаготівлі на планеті. Просто створіть член для утримання об'єкта реєстратора.

class MyClass {
  val LOG = Logger.getLogger(this.javaClass.name)

  fun foo() {
        LOG.warning("Hello from MyClass")
  }
} 

створення результату:

26 грудня 2015 р. 11:28:44 org.stackoverflow.kotlin.test.MyClass foo ІНФОРМАЦІЯ: Привіт від MyClass

Ви можете перевірити продуктивність як для варіантів, так і для класів, і побачити, чи є реальна різниця для більшості програм.

Делегати власності (звичайні, найелегантніші)

Інший підхід, який пропонує @Jire в іншій відповіді, полягає у створенні делегата властивості, який ви можете використовувати для того, щоб здійснювати логіку рівномірно в будь-якому іншому класі, який ви хочете. Існує простіший спосіб зробити це, оскільки Котлін вже надає Lazyделегата, ми можемо просто зафіксувати його у функції. Один фокус тут полягає в тому, що якщо ми хочемо знати тип класу, який зараз використовує делегат, ми робимо його функцією розширення для будь-якого класу:

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

Цей код також гарантує, що якщо ви використовуєте його в Companion Object, ім'я реєстратора буде таким самим, як якщо б ви використовували його в самому класі. Тепер ви можете просто:

class Something {
    val LOG by logger()

    fun foo() {
        LOG.info("Hello from Something")
    }
}

для кожного примірника класу або якщо ви хочете, щоб він був більш статичним з одним екземпляром на клас:

class SomethingElse {
    companion object {
        val LOG by logger()

    }

    fun foo() {
        LOG.info("Hello from SomethingElse")
    }
}

І ваш вихід із виклику foo()обох цих класів буде:

26 грудня 2015 р. 11:30:55 org.stackoverflow.kotlin.test.Something foo INFO: Привіт від чогось

26 грудня 2015 р. 11:30:55 org.stackoverflow.kotlin.test.SomethingElse foo INFO: Привіт від SomethingElse

Функції розширення (рідкість у цьому випадку через "забруднення" будь-якого простору імен)

У Котліна є кілька прихованих хитрощів, які дозволяють зробити деякі з цього коду ще меншими. Ви можете створювати функції розширень на класах і, таким чином, надавати їм додаткову функціональність. Одне з пропозицій у коментарях вище було розширити Anyфункцію реєстратора. Це може створювати шум у будь-який час, коли хтось використовує доповнення коду у своєму IDE в будь-якому класі. Але є розширена Anyабо якась інша інтерфейсна маркерна таємна вигода : ви можете сказати, що ви розширюєте власний клас і, отже, виявляєте клас, у якому ви перебуваєте. Так? Щоб бути менш заплутаним, ось код:

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

Тепер у межах класу (або супутнього об’єкта) я можу просто викликати це розширення у своєму власному класі:

class SomethingDifferent {
    val LOG = logger()

    fun foo() {
        LOG.info("Hello from SomethingDifferent")
    }
}

Вихід:

26 грудня 2015 11:29:12 org.stackoverflow.kotlin.test.SomethingDifferent foo INFO: Привіт від SomethingDifferent

В основному код розглядається як заклик до розширення Something.logger(). Проблема полягає в тому, що наступне також може бути правдивим, створюючи "забруднення" для інших класів:

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

Функції розширення в інтерфейсі маркера (не впевнені, наскільки поширена, але загальна модель для "рис")

Щоб зробити розширення більш чистими та зменшити "забруднення", ви можете використовувати інтерфейс маркера для розширення:

interface Loggable {} 

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

Або навіть зробити метод частиною інтерфейсу із реалізацією за замовчуванням:

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

І використовуйте будь-яку з цих варіацій у своєму класі:

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

Вихід:

26 грудня 2015 11:41:01 AM org.stackoverflow.kotlin.test.MarkedClass foo INFO: Привіт від MarkedClass

Якщо ви хочете змусити створити єдине поле для утримання реєстратора, тоді, використовуючи цей інтерфейс, ви могли б легко вимагати від виконавця такого поля, як LOG:

interface Loggable {
    val LOG: Logger  // abstract required field

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

Тепер реалізатор інтерфейсу повинен виглядати так:

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

Звичайно, абстрактний базовий клас може робити те ж саме, маючи можливість як інтерфейсу, так і абстрактного класу, що реалізує цей інтерфейс, дозволяє гнучкість та однаковість:

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!
    }
}

Складаємо все разом (невелика бібліотека-помічник)

Ось невелика бібліотека помічників, щоб зробити будь-який із варіантів вище простим у використанні. У Котліні звичайно розширювати API, щоб зробити їх більше до душі. Або в розширеннях, або у функціях верхнього рівня. Ось суміш, яка дає вам варіанти створення реєстраторів та зразок, що показує всі варіанти:

// Return logger for Java class, if companion object fix the name
fun <T: Any> logger(forClass: Class<T>): Logger {
    return Logger.getLogger(unwrapCompanionClass(forClass).name)
}

// unwrap companion class to enclosing class given a Java Class
fun <T : Any> unwrapCompanionClass(ofClass: Class<T>): Class<*> { 
   return ofClass.enclosingClass?.takeIf { 
      ofClass.enclosingClass.kotlin.companionObject?.java == ofClass 
   } ?: ofClass 
}

// unwrap companion class to enclosing class given a Kotlin Class
fun <T: Any> unwrapCompanionClass(ofClass: KClass<T>): KClass<*> {
   return unwrapCompanionClass(ofClass.java).kotlin
}

// Return logger for Kotlin class
fun <T: Any> logger(forClass: KClass<T>): Logger {
    return logger(forClass.java)
}

// return logger from extended class (or the enclosing class)
fun <T: Any> T.logger(): Logger {
    return logger(this.javaClass)
}

// return a lazy logger property delegate for enclosing class
fun <R : Any> R.lazyLogger(): Lazy<Logger> {
    return lazy { logger(this.javaClass) }
}

// return a logger property delegate for enclosing class
fun <R : Any> R.injectLogger(): Lazy<Logger> {
    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()
}

Виберіть те, що хочете зберегти, і ось усі доступні варіанти:

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")
    }
}

Усі 13 екземплярів реєстраторів, створених у цьому зразку, будуть створювати те саме ім’я реєстратора та виводити:

26 грудня 2015 р. 11:39:00 org.stackoverflow.kotlin.test.MixedBagOfTricks foo INFO: Привіт від MixedBagOfTricks

Примітка . unwrapCompanionClass()Метод гарантує, що ми не генеруємо реєстратор, названий на честь об'єкта-супутника, а клас, що додає. Це поточний рекомендований спосіб пошуку класу, що містить супровідний об’єкт. Зняття " $ Companion " з імені з використанням імені removeSuffix()не працює, оскільки супутнім об'єктам можуть бути призначені власні імена.


Деякі рамки введення залежності використовують делегати, як ви бачите в іншій відповіді тут. Вони виглядають як `val log: Logger by injectLogger ()` і дозволяють вводити систему реєстрації та невідомий коду використання. (Моя рамка для ін'єкцій, що показує це на сайті github.com/kohesive/injekt )
Jayson Minard

10
Дякую за обширну відповідь. Дуже інформативний. Мені особливо подобається впровадження Делегатів власності (загальна, найелегантніша) реалізація.
mchlstckl

6
Я думаю, що відбулася зміна синтаксису kotlin. і розмотувати слід ofClass.enclosingClass.kotlin.objectInstance?.javaClassзамістьofClass.enclosingClass.kotlin.companionObject?.java
oshai

1
ах, неважливо, як зазначено тут kotlinlang.org/docs/reference/reflection.html відбивна баночка поставляється окремо від stdlib, для Gradle нам це потрібно:compile 'org.jetbrains.kotlin:kotlin-reflect:1.0.2'
Hoang Tran

1
Код для створення "Делегатів властивостей" та "Функції розширення" схоже, за винятком типу повернення. Зразок коду для власника Delegate ( public fun <R : Any> R.logger(): Lazy<Logger> { return lazy{Logger.getLogger(unwrapCompanionClass(this.javaClass).name)}}), здається, створює функцію розширення, таку, яка "".logger()зараз є річчю, чи слід так поводитися?
Майк Риландер

32

Погляньте на бібліотеку kotlin-logging .
Це дозволяє вести журнал таким чином:

private val logger = KotlinLogging.logger {}

class Foo {
  logger.info{"wohoooo $wohoooo"}
}

Або так:

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

Я також написав повідомлення в блозі, порівнюючи його з AnkoLogger: Вхід у Котлін та Android: AnkoLogger проти kotlin-logging

Відмова: Я підтримую цю бібліотеку.

Редагувати: Kotlin-logging тепер підтримує багатоплатформну підтримку: https://github.com/MicroUtils/kotlin-logging/wiki/Multiplatform-support


Можу чи я запропонувати вам змінити свою відповідь , щоб показати вихід з logger.info()викликів, як це зробив Джейсон в його загальноприйнятому відповідь.
Пауло Мерсон

7

Як хороший приклад реалізації журналу я хочу зазначити Anko, який використовує спеціальний інтерфейс, AnkoLoggerякий повинен застосовувати клас, який потребує реєстрації. Всередині інтерфейсу є код, який генерує тег журналу для класу. Потім журнал проводиться за допомогою функцій розширення, які можна викликати всередині реалізації interce без префіксів або навіть створення екземпляра реєстратора.

Я не думаю, що це ідіоматично , але це здається хорошим підходом, оскільки він вимагає мінімального коду, просто додаючи інтерфейс до декларації класу, і ви отримуєте журнал з різними тегами для різних класів.


Код нижче - в основному AnkoLogger , спрощений і переписаний для використання Android-агностиків.

По-перше, є інтерфейс, який поводиться як маркерний інтерфейс:

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

Це дозволяє його реалізації використовувати функції розширень для MyLoggerвсередині свого коду, просто викликаючи їх this. А також містить тег реєстрації.

Далі, є загальна точка входу для різних методів ведення журналу:

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

Це буде викликано методами реєстрації. Він отримує тег від MyLoggerреалізації, перевіряє параметри журналу, а потім викликає один з двох обробників, той з Throwableаргументом і той без.

Тоді ви можете визначити скільки завгодно методів ведення журналу таким чином:

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()
            })

Вони визначені один раз як для ведення журналу лише повідомлення, так і для реєстрації Throwableа, це робиться за допомогою додаткового throwableпараметра.

Функції, які передаються як handlerі throwableHandlerможуть бути різними для різних методів ведення журналу, наприклад, вони можуть записати журнал у файл або завантажити його кудись. isLoggingEnabledі LoggingLevelsвони опущені для стислості, але використання їх забезпечує ще більшу гнучкість.


Він дозволяє використовувати таке:

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

Є невеликий недолік: для входу в функції на рівні пакета знадобиться об’єкт реєстратора:

private object MyPackageLog : MyLogger

fun myFun() {
    MyPackageLog.info("Info message")
}

Ця відповідь є специфічною для Android, і в цьому питанні не було зазначено, ні тег Android.
Джейсон Мінард

@JaysonMinard чому це? Цей підхід є загальним призначенням, оскільки, наприклад, унікальний тег реєстрації для кожного класу корисний і для проектів, що не належать до Android.
гаряча клавіша

1
Незрозуміло, що ви говорите «реалізуйте щось подібне до того, що зробив Анко», а натомість більше схоже на «використовувати Анко» ... для чого тоді потрібна бібліотека Android під назвою Anko. Який інтерфейс має функції розширення, які закликають android.util.Logробити журнал. Який був ваш намір? використовувати Анко? Створіть щось подібне, використовуючи Anko як приклад (краще, якщо ви просто покладете запропонований код у рядок і виправите його для не-Android, а не сказати "перенести це на не-Android, ось посилання". Замість цього ви додаєте зразок коду дзвонить Анко)
Джейсон Мінард

1
@JaysonMinard, дякую за ваші коментарі, я переписав пост, щоб він тепер пояснював підхід, а не посилання Anko.
гаряча клавіша

6

KISS: Для команд Java, які мігрують до Котліна

Якщо ви не заперечуєте вказувати ім’я класу для кожної інстанції реєстратора (як і java), ви можете зробити це просто, визначивши це як функцію вищого рівня десь у вашому проекті:

import org.slf4j.LoggerFactory

inline fun <reified T:Any> logger() = LoggerFactory.getLogger(T::class.java)

Для цього використовується параметр типу ретифікованого типу Котліна .

Тепер ви можете використовувати це наступним чином:

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

Такий підхід надзвичайно простий і близький до еквіваленту java, але просто додає певного синтаксичного цукру.

Наступний крок: розширення або делегати

Я особисто вважаю за краще піти на крок далі і використовувати розширення або підхід до делегатів. Це чудово узагальнено у відповіді @ JaysonMinard, але ось TL; DR для підходу "Delegate" за допомогою API log4j2 ( UPDATE : більше не потрібно писати цей код вручну, оскільки він був випущений як офіційний модуль проект log4j2, див. нижче). Оскільки log4j2, на відміну від slf4j, підтримує ведення журналів за допомогою Suppliers, я також додав делегата, щоб зробити ці методи більш простими.

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: () -> String) {
    log.debug(Supplier { supplier.invoke() })
  }

  inline fun debug(t: Throwable, crossinline supplier: () -> String) {
    log.debug(Supplier { supplier.invoke() }, t)
  }

  inline fun info(crossinline supplier: () -> String) {
    log.info(Supplier { supplier.invoke() })
  }

  inline fun info(t: Throwable, crossinline supplier: () -> String) {
    log.info(Supplier { supplier.invoke() }, t)
  }

  inline fun warn(crossinline supplier: () -> String) {
    log.warn(Supplier { supplier.invoke() })
  }

  inline fun warn(t: Throwable, crossinline supplier: () -> String) {
    log.warn(Supplier { supplier.invoke() }, t)
  }

  inline fun error(crossinline supplier: () -> String) {
    log.error(Supplier { supplier.invoke() })
  }

  inline fun error(t: Throwable, crossinline supplier: () -> String) {
    log.error(Supplier { supplier.invoke() }, t)
  }
}

/**
 * A delegate-based lazy logger instantiation. Use: `val log by logger()`.
 */
@Suppress("unused")
inline fun <reified T : Any> T.logger(): Lazy<FunctionalLogger> =
  lazy { FunctionalLogger(LogManager.getLogger(unwrapCompanionClass(T::class.java))) }

// unwrap companion class to enclosing class given a Java Class
fun <T : Any> unwrapCompanionClass(ofClass: Class<T>): Class<*> {
  return if (ofClass.enclosingClass != null && ofClass.enclosingClass.kotlin.companionObject?.java == ofClass) {
    ofClass.enclosingClass
  } else {
    ofClass
  }
}

Log4j2 Kotlin Logging API

Більшість попереднього розділу були безпосередньо адаптовані для створення модуля API журналу Kotlin , який зараз є офіційною частиною Log4j2 (відмова від відповідальності: я є основним автором). Ви можете завантажити це безпосередньо з Apache або через Maven Central .

Використання в основному, як описано вище, але модуль підтримує доступ до реєстратора на основі інтерфейсу, функцію loggerрозширення, включену Anyдля використання там, де thisвизначено, і іменовану функцію реєстратора для використання там, де не thisвизначено (наприклад, функції верхнього рівня).


1
Якщо я маю рацію, ви можете уникнути введення назви класу в першому запропонованому вами рішенні, змінивши підпис методу на T.logger ()
IPat

1
@IPat yup, перше рішення навмисно не робить цього, щоб залишатися поруч із "способом java". Друга частина відповіді стосується випадку розширення T.logger()- див. Нижню частину зразка коду.
Раман

5

Анко

Ви можете використовувати Ankoбібліотеку для цього. У вас буде код, як показано нижче:

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

котлін-лісозаготівля

Kotlin-logging ( проект Github - kotlin-logging ) бібліотека дозволяє писати код журналу, як показано нижче:

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

StaticLog

або ви також можете використовувати цю маленьку написану в бібліотеці Котліна назву, 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")\

Друге рішення може бути кращим, якщо ви хочете визначити вихідний формат для методу ведення журналу, наприклад:

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

або використовувати фільтри, наприклад:

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

timberkt

Якщо ви вже використовували Timberперевірку бібліотеки журналів Джейка Уортона timberkt.

Ця бібліотека побудована на Timber за допомогою API, який простіше використовувати від Kotlin. Замість того, щоб використовувати параметри форматування, ви передаєте лямбда, яка оцінюється лише у випадку, якщо повідомлення зареєстровано.

Приклад коду:

// Standard timber
Timber.d("%d %s", intVar + 3, stringFun())

// Kotlin extensions
Timber.d { "${intVar + 3} ${stringFun()}" }
// or
d { "${intVar + 3} ${stringFun()}" }

Перевірте також: Вхід у Котлін та Android: AnkoLogger vs kotlin-logging

Сподіваюся, це допоможе


4

Чи буде щось подібне працювати для вас?

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() }
}

1
Ця відповідь потребує додаткового пояснення, якщо людина, яка запитує, не розуміє супутніх об’єктів, вони, ймовірно, не дісталися до делегатів, і тому не знають, що це робить. Плюс цього дуже мало заощаджень на коді за допомогою цієї моделі. І я сумніваюсь, що кешування в супутниковому об’єкті насправді є підвищенням продуктивності, ніж у обмеженій системі з невеликим процесором, таким як Android.
Джейсон Мінард

1
Цей код наведено вище - це створення класу, який виступає в якості делегата (див. Kotlinlang.org/docs/reference/delegated-properties.html ), який є першим класом. LoggerDelegate Потім він створює функцію верхнього рівня, яка створює простіше створити екземпляр делегата (не набагато простіше, але небагато). І цю функцію слід змінити inline. Тоді він використовує делегат, щоб забезпечити реєстратор, коли бажається. Але він надає один для супутника, Foo.Companionа не для класу, Fooтому, можливо, не за призначенням.
Джейсон Мінард

@JaysonMinard Я згоден, але я залишу відповідь майбутнім глядачам, які хочуть "швидкого виправлення" або прикладу того, як застосувати це до власних проектів. Я не розумію, чому ця logger()функція повинна бути, inlineякщо немає лямбда. IntelliJ пропонує вбудовування в цьому випадку не потрібно: i.imgur.com/YQH3NB1.png
Jire

1
Я включив вашу відповідь у свою, і спростив її, видаливши спеціальний клас делегата і Lazyзамість цього застосувавши обгортку . З хитрістю дізнатися, до якого класу він відноситься.
Джейсон Мінард

1

Я не чув жодної ідіоми з цього приводу. Чим простіше, тим краще, тим я використовував би властивість верхнього рівня

val logger = Logger.getLogger("package_name")

Ця практика добре допомагає в Python, і настільки ж різні, як Котлін і Python можуть здаватися, я вважаю, що вони досить схожі в "дусі" (кажучи про ідіоми).


Верхній рівень також відомий як пакетний рівень.
Caelum

Змінна верхнього рівня - це як сказати "використовувати глобальні змінні", і я думаю, це було б застосовано лише в тому випадку, якщо у вас були інші функції верхнього рівня, які потребували використання реєстратора. У цей момент, можливо, буде краще перейти в реєстратор до будь-якої функції утиліти, яка хоче ввійти.
Джейсон Мінард

1
@JaysonMinard Я думаю, що передача реєстратора в якості параметра буде анти-шаблоном, тому що ваш журнал ніколи не повинен впливати на ваш API, зовнішній чи внутрішній
voddan

Добре, тоді поверніться до моєї точки зору: для ведення журналу рівня класу кладуть реєстратор у клас, а не функцію верхнього рівня.
Джейсон Мінард

1
@voddan принаймні наведіть повний приклад того, який тип реєстратора ви створюєте. val log = what?!? ... створення реєстратора за іменем? Ігноруючи той факт, запитання показало, що він хоче створити реєстратор для конкретного класуLoggerFactory.getLogger(Foo.class);
Jayson Minard

1

А як щодо функції розширення класу натомість? Таким чином ви закінчите:

public fun KClass.logger(): Logger = LoggerFactory.getLogger(this.java)

class SomeClass {
    val LOG = SomeClass::class.logger()
}

Примітка - я взагалі цього не перевіряв, тому це може бути не зовсім правильно.


1

По-перше, ви можете додати функції розширення для створення реєстратора.

inline fun <reified T : Any> getLogger() = LoggerFactory.getLogger(T::class.java)
fun <T : Any> T.getLogger() = LoggerFactory.getLogger(javaClass)

Тоді ви зможете створити реєстратор, використовуючи наступний код.

private val logger1 = getLogger<SomeClass>()
private val logger2 = getLogger()

По-друге, ви можете визначити інтерфейс, який забезпечує реєстратор та його змішану реалізацію.

interface LoggerAware {
  val logger: Logger
}

class LoggerAwareMixin(containerClass: Class<*>) : LoggerAware {
  override val logger: Logger = LoggerFactory.getLogger(containerClass)
}

inline fun <reified T : Any> loggerAware() = LoggerAwareMixin(T::class.java)

Цей інтерфейс можна використовувати наступним чином.

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


1

Тут вже є багато чудових відповідей, але всі вони стосуються додавання реєстратора до класу, але як би ви це зробили для входу в функції вищого рівня?

Цей підхід є загальним та досить простим, щоб добре працювати в обох класах, супутніх об'єктах та функціях верхнього рівня:

package nieldw.test

import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import org.junit.jupiter.api.Test

fun logger(lambda: () -> Unit): Lazy<Logger> = lazy { LogManager.getLogger(getClassName(lambda.javaClass)) }
private fun <T : Any> getClassName(clazz: Class<T>): String = clazz.name.replace(Regex("""\$.*$"""), "")

val topLog by logger { }

class TopLevelLoggingTest {
    val classLog by logger { }

    @Test
    fun `What is the javaClass?`() {
        topLog.info("THIS IS IT")
        classLog.info("THIS IS IT")
    }
}

0

Ось для чого, в основному, супутні об’єкти: заміна статичних матеріалів.


Об'єкт-супутник не є статичним, це сингтон, який може містити членів, які можуть стати статичними, якщо використовувати JvmStaticанотацію. І в майбутньому може бути більше одного дозволеного. Плюс ця відповідь не дуже корисна без додаткової інформації чи зразка.
Джейсон Мінард

Я не сказав, що це статика. Я сказав, що це за заміну статики. І чому було б більше одного дозволеного? Це не має сенсу. Нарешті, я поспішав, і думав, що вказівка ​​в правильному напрямку буде досить корисною.
Яків Циммерман

1
Супутній об’єкт не для заміни статики, але він також може робити його елементи статичними. Котлін певний час підтримував більше, ніж на супутнику, і дозволяє їм мати інші імена. Після того, як ви почнете називати їх, вони діють менш, ніж статика. І в майбутньому залишається відкритим мати більше одного названого супутника. Наприклад, може бути Factoryі іншеHelpers
Джейсон Мінард

0

Приклад Slf4j, те ж саме для інших. Це навіть працює для створення реєстратора рівня пакетів

/**  
  * Get logger by current class name.  
  */ 

fun getLogger(c: () -> Unit): Logger = 
        LoggerFactory.getLogger(c.javaClass.enclosingClass)

Використання:

val logger = getLogger { }

0
fun <R : Any> R.logger(): Lazy<Logger> = 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()
    }
}

0

Це все ще WIP (майже закінчено), тому я хотів би поділитися ним: https://github.com/leandronunes85/log-format-enforcer#kotlin-soon-to-come-in-version-14

Основна мета цієї бібліотеки - наведення певного стилю журналу в рамках проекту. Створюючи код Котліна, я намагаюся вирішити деякі проблеми, згадані в цьому питанні. Що стосується оригінального питання, що я зазвичай схильний робити:

private val LOG = LogFormatEnforcer.loggerFor<Foo>()
class Foo {

}

0

Ви можете просто створити власну "бібліотеку" утиліт. Для цієї задачі вам не потрібна велика бібліотека, яка зробить ваш проект важчим і складнішим.

Наприклад, ви можете використовувати Kotlin Reflection, щоб отримати ім'я, тип і значення будь-якого класового властивості.

Перш за все, переконайтеся, що метазалежність встановлена ​​у вашому build.gradle:

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
}

Після цього ви можете просто скопіювати та вставити цей код у свій проект:

import kotlin.reflect.full.declaredMemberProperties

class LogUtil {
    companion object {
        /**
         * Receives an [instance] of a class.
         * @return the name and value of any member property.
         */
        fun classToString(instance: Any): String {
            val sb = StringBuilder()

            val clazz = instance.javaClass.kotlin
            clazz.declaredMemberProperties.forEach {
                sb.append("${it.name}: (${it.returnType}) ${it.get(instance)}, ")
            }

            return marshalObj(sb)
        }

        private fun marshalObj(sb: StringBuilder): String {
            sb.insert(0, "{ ")
            sb.setLength(sb.length - 2)
            sb.append(" }")

            return sb.toString()
        }
    }
}

Приклад використання:

data class Actor(val id: Int, val name: String) {
    override fun toString(): String {
        return classToString(this)
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.