Kotlin 中惯用的日志记录方式

Kotlin 没有与 Java 中使用的静态字段相同的概念。在Java中,一般接受的日志记录方式是:

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

什么是 Kotlin 执行日志的惯用方式?

57772 次浏览
我还没有听说过这方面的成语。 越简单越好,所以我将使用顶级属性

val logger = Logger.getLogger("package_name")

这种做法在Python中很有用,尽管Kotlin和Python可能看起来不同,但我相信它们在“精神”上是非常相似的。(说到习语)。

一般来说,这就是伴生对象的作用:替换静态的东西。

作为日志实现的一个很好的例子,我想提到,它使用了一个特殊的接口AnkoLogger,需要日志记录的类应该实现这个接口。在接口内部,有为类生成日志标记的代码。然后通过扩展函数完成日志记录,扩展函数可以在接口实现中调用,而不需要使用前缀,甚至不需要创建日志记录器实例。

我不认为这是惯用,但它似乎是一个很好的方法,因为它只需要最少的代码,只需将接口添加到类声明中,并且您可以使用不同的标记为不同的类记录日志。

人力资源> < p > < 下面的代码基本上是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参数完成的。

对于不同的日志记录方法,作为handlerthrowableHandler传递的函数可以是不同的,例如,它们可以将日志写入文件或上传到某个地方。为了简洁起见,省略了isLoggingEnabledLoggingLevels,但是使用它们提供了更大的灵活性。

人力资源> < p > < 它允许以下用法:

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

有一个小缺点:需要logger对象来记录包级函数:

private object MyPackageLog : MyLogger


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

这样的东西对你有用吗?

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

在大多数成熟的Kotlin代码中,您会发现下面这些模式之一。使用房地产的代表的方法利用了Kotlin的强大功能来生成最小的代码。

注意:这里的代码是针对java.util.Logging的,但同样的理论适用于任何日志库

Static-like(常见,相当于问题中的Java代码)

如果您不相信日志系统内哈希查找的性能,可以通过使用一个伴侣对象来获得与Java代码类似的行为,该对象可以容纳一个实例,对您来说就像一个静态对象。

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


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

创建输出:

12月26日上午org.stackoverflow.kotlin.test.MyClass foo INFO: Hello from MyClass

关于伴生对象的更多信息:同伴的对象…还要注意,在上面的示例中,MyClass::class.java为记录器获取类型Class<MyClass>的实例,而this.javaClass将获得类型Class<MyClass.Companion>的实例。

每个类的实例(常见)

但是,确实没有理由避免在实例级调用和获取记录器。您提到的惯用Java方式已经过时,并且基于对性能的担忧,而每个类的记录器已经被地球上几乎任何合理的日志记录系统缓存。只需创建一个成员来保存记录器对象。

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

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

创建输出:

12月26日,2015:28:44 AM org.stackoverflow.kotlin.test.MyClass foo INFO: Hello from MyClass

你可以对每个实例和每个类的变化进行性能测试,看看对于大多数应用程序是否存在实际的差异。

房地产的代表(普通,最优雅)

@Jire在另一个回答中建议的另一种方法是创建一个属性委托,然后您可以使用它在任何其他您想要的类中统一地执行逻辑。由于Kotlin已经提供了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对象中使用它,那么记录器名称将与您在类本身上使用它时相同。现在你可以简单地:

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()的输出将是:

12月26日,2015:30:55 AM org.stackoverflow.kotlin.test.Something foo INFO: Hello from Something

12月26日,2015:30:55 AM org.stackoverflow.kotlin.test.SomethingElse foo Hello from SomethingElse

扩展函数(在这种情况下不常见,因为“污染”;任意命名空间)

Kotlin有一些隐藏的技巧,可以让您将一些代码变得更小。您可以在类上创建扩展函数,从而为它们提供额外的功能。上面评论中的一个建议是用记录器函数扩展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")
}
}

生产输出:

12月26日,2015:11:29 AM org.stackoverflow.kotlin.test.SomethingDifferent foo INFO: Hello from SomethingDifferent

基本上,该代码被视为对扩展Something.logger()的调用。问题是,下面这些也可能是真的——造成“污染”。其他类:

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

标记界面的扩展函数(不确定有多常见,但“trait”的通用模型;)

为了使扩展的使用更干净,减少“污染”,你可以使用一个标记接口来扩展:

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

生产输出:

12月26日,2015:41:01 AM org.stackoverflow.kotlin.test.MarkedClass foo INFO: Hello from 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!
}
}

把它们放在一起(一个小的帮助库)

下面是一个小的帮助器库,可以使上面的任何选项都易于使用。在Kotlin中,扩展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个记录器实例将产生相同的记录器名称,并输出:

12月26日,2015:11:39:00 AM org.stackoverflow.kotlin.test.MixedBagOfTricks foo . INFO: Hello from MixedBagOfTricks

unwrapCompanionClass()方法确保我们不会生成一个以伴随对象而是以外围类命名的记录器。这是当前推荐的查找包含伴生对象的类的方法。剥离“美元的同伴"从名称中使用removeSuffix()是无效的,因为伴随对象可以被赋予自定义名称。

KISS:用于迁移到Kotlin的Java团队

如果你不介意在记录器的每个实例化上提供类名(就像java一样),你可以在项目的某个地方将其定义为顶级函数来保持简单:

import org.slf4j.LoggerFactory


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

这使用了Kotlin 具体化类型参数

现在,你可以这样使用:

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

这种方法超级简单,与java相当,只是在语法上增加了一些糖分。

下一步:扩展或委托

我个人更喜欢更进一步,使用扩展或委托方法。@JaysonMinard的回答很好地总结了这一点,但这里是log4j2 API的“委托”方法的TL;DR (更新:不需要再手动编写此代码,因为它已经作为log4j2项目的官方模块发布,见下文)。由于log4j2与slf4j不同,它支持使用Supplier的日志记录,因此我还添加了一个委托,使这些方法的使用更简单。

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日志API

前一节的大部分内容已经直接改编为Kotlin日志API模块,它现在是Log4j2的正式部分(免责声明:我是主要作者)。你可以下载这个直接从Apache,或者通过Maven中央

使用基本上如上所述,但该模块同时支持基于接口的记录器访问、Any上的logger扩展函数(用于定义this的地方)和命名记录器函数(用于未定义this的地方)。

在Class上使用扩展函数怎么样?这样你就会得到:

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


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

注意——我根本没有测试过这个,所以它可能不太正确。

首先,您可以为日志记录器的创建添加扩展函数。

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

其次,可以定义提供记录器及其mixin实现的接口。

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

查看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: 登录Kotlin &Android: AnkoLogger vs kotlin-logging进行比较

免责声明:我是该库的维护者。

编辑:kotlin-logging现在支持多平台:https://github.com/MicroUtils/kotlin-logging/wiki/Multiplatform-support

Slf4j的例子,其他的也一样。这甚至适用于创建包级记录器

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


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

用法:

val logger = getLogger { }

你可以使用Anko库来做这件事。你会有如下代码:

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

kotlin-logging

kotlin-logging(Github项目- kotlin-logging)库允许你像下面这样编写日志代码:

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

StaticLog

或者你也可以使用这个写在Kotlin库中的名为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

如果你已经使用Jake Wharton的Timber日志库,检查timberkt

这个库构建在Timber上,它的API更容易从Kotlin中使用。您不使用格式化参数,而是传递一个lambda,仅在消息被记录时才计算该lambda。

代码示例:

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


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

检查:登录Kotlin &Android: AnkoLogger vs kotlin-logging

希望对大家有所帮助

创建伴生对象并使用@JvmStatic注释标记适当的字段

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

这里已经有许多很好的答案,但它们都涉及到向类添加记录器,但是如何在顶级函数中进行日志记录呢?

这种方法是通用的,并且足够简单,在两个类、伴生对象和顶级函数中都能很好地工作:

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")
}
}
这仍然是在制品(几乎完成),所以我想分享它: https://github.com/leandronunes85/log-format-enforcer#kotlin-soon-to-come-in-version-14 < / p >

这个库的主要目标是在项目中强制执行特定的日志样式。通过让它生成Kotlin代码,我试图解决这个问题中提到的一些问题。关于最初的问题,我通常倾向于这样做:

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


}

您可以简单地构建自己的实用程序“库”。你不需要一个大的库来完成这个任务,这会让你的项目更重、更复杂。

例如,您可以使用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)
}
}

对于Kotlin Multiplaform日志,我找不到一个具有我所需要的所有功能的库,所以我最终编写了一个。请查看KmLogging。它实现的功能是:

  • 在每个平台上使用特定平台的日志:Android上的Log, iOS上的os_log, JavaScript上的console。
  • 高性能。禁用时只有1个布尔检查。我喜欢加入大量的日志记录,并希望在发布时将其全部关闭,不希望为大量的日志记录支付太多的开销。此外,当打开日志时,它需要非常高性能。
  • 可扩展的。需要能够添加其他记录器,如日志记录到Crashlytics等。
  • 每个记录器可以在不同的级别上进行日志记录。例如,您可能只希望将以上信息转到Crashlytics和所有其他在生产中禁用的日志记录器。

使用方法:

val log = logging()
log.i { "some message" }