Kotlin 中惯用的日志记录方式
Idiomatic way of logging in Kotlin
Kotlin 没有 Java 中使用的相同静态字段概念。在 Java 中,普遍接受的日志记录方式是:
public class Foo {
private static final Logger LOG = LoggerFactory.getLogger(Foo.class);
}
问题在 Kotlin 中执行日志记录的惯用方式是什么?
我没听说过这方面的成语。
越简单越好,所以我会用顶级的属性
val logger = Logger.getLogger("package_name")
这种做法在 Python 中很有效,尽管 Kotlin 和 Python 可能看起来不同,但我相信它们在“精神”(说成语)上非常相似。
通常来说,这就是伴随对象的用途:替换静态内容。
作为日志记录实现的一个很好的例子,我想提一下 Anko,它使用一个特殊的接口 AnkoLogger
,需要日志记录的 class 应该实现这个接口。在界面内部,有代码为 class 生成日志标记。然后通过扩展函数完成日志记录,这些函数可以在接口实现中调用,无需前缀甚至创建记录器实例。
我不认为这是 惯用的 ,但这似乎是一个很好的方法,因为它需要最少的代码,只需将接口添加到 class 声明中,并且您会为不同的 classes.
使用不同的标签进行记录
下面的代码基本上是 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")
}
这样的事情对你有用吗?
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 代码中,您会发现以下其中一种模式。使用 属性 Delegates 的方法利用了 Kotlin 的强大功能来生成最小的代码。
注意:此处的代码适用于 java.util.Logging
但同样的理论适用于任何日志库
类静态(常见,相当于问题中的Java代码)
如果您不相信日志系统中哈希查找的性能,您可以通过使用一个伴随对象来获得与您的 Java 代码类似的行为,该对象可以包含一个实例并且感觉像静态的你.
class MyClass {
companion object {
val LOG = Logger.getLogger(MyClass::class.java.name)
}
fun foo() {
LOG.warning("Hello from MyClass")
}
}
创建输出:
Dec 26, 2015 11:28:32 AM org.Whosebug.kotlin.test.MyClass
foo
INFO: Hello from MyClass
这里有更多关于伴生对象的信息:Companion Objects ... 另请注意,在上面的示例中,MyClass::class.java
为记录器获取类型 Class<MyClass>
的实例,而 this.javaClass
将获得 Class<MyClass.Companion>
.
类型的实例
每个 Class 实例(常见)
但是,确实没有理由避免在实例级别调用和获取记录器。您提到的惯用 Java 方式已经过时并且基于对性能的恐惧,而每个 class 的记录器已经被地球上几乎所有合理的日志记录系统缓存。只需创建一个成员来保存记录器对象。
class MyClass {
val LOG = Logger.getLogger(this.javaClass.name)
fun foo() {
LOG.warning("Hello from MyClass")
}
}
创建输出:
Dec 26, 2015 11:28:44 AM org.Whosebug.kotlin.test.MyClass foo
INFO: Hello from MyClass
您可以对每个实例和每个 class 变体进行性能测试,看看大多数应用程序是否存在实际差异。
属性代表(普通,最优雅)
@Jire 在另一个答案中建议的另一种方法是创建一个 属性 委托,然后您可以使用它在您想要的任何其他 class 中统一执行逻辑.有一种更简单的方法可以做到这一点,因为 Kotlin 已经提供了一个 Lazy
委托,我们可以将它包装在一个函数中。这里的一个技巧是,如果我们想知道当前使用委托的 class 的类型,我们将其作为任何 class:
的扩展函数
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"
此代码还确保如果您在伴随对象中使用它,记录器名称将与您在 class 本身上使用时的名称相同。现在您可以简单地:
class Something {
val LOG by logger()
fun foo() {
LOG.info("Hello from Something")
}
}
for per class instance,或者如果您希望每个 class 一个实例更静态:
class SomethingElse {
companion object {
val LOG by logger()
}
fun foo() {
LOG.info("Hello from SomethingElse")
}
}
在这两个 classes 上调用 foo()
的输出将是:
Dec 26, 2015 11:30:55 AM org.Whosebug.kotlin.test.Something foo
INFO: Hello from Something
Dec 26, 2015 11:30:55 AM org.Whosebug.kotlin.test.SomethingElse foo
INFO: Hello from SomethingElse
扩展函数(在这种情况下不常见,因为 Any 命名空间的“污染”)
Kotlin 有一些隐藏的技巧,可以让您将一些代码变得更小。您可以在 classes 上创建扩展函数,从而为它们提供额外的功能。上面评论中的一个建议是使用记录器功能扩展 Any
。每当有人在 class 中的 IDE 中使用代码完成时,这都会产生噪音。但是扩展 Any
或其他一些标记接口有一个秘密的好处:您可以暗示您正在扩展自己的 class 并因此检测到您所在的 class 。嗯?为了减少混淆,这里是代码:
// extend any class with the ability to get a logger
fun <T: Any> T.logger(): Logger {
return Logger.getLogger(unwrapCompanionClass(this.javaClass).name)
}
现在在 class(或伴随对象)中,我可以简单地自己调用此扩展 class:
class SomethingDifferent {
val LOG = logger()
fun foo() {
LOG.info("Hello from SomethingDifferent")
}
}
生成输出:
Dec 26, 2015 11:29:12 AM org.Whosebug.kotlin.test.SomethingDifferent foo
INFO: Hello from SomethingDifferent
基本上,代码被视为对扩展 Something.logger()
的调用。问题是以下情况也可能对其他 classes 造成“污染”:
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:
中使用这些变体中的任何一个
class MarkedClass: Loggable {
val LOG = logger()
}
生成输出:
Dec 26, 2015 11:41:01 AM org.Whosebug.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()
}
当然,抽象基 class 也可以做同样的事情,可以选择接口和实现该接口的抽象 class 允许灵活性和统一性:
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 个记录器实例将生成相同的记录器名称,并输出:
Dec 26, 2015 11:39:00 AM org.Whosebug.kotlin.test.MixedBagOfTricks foo
INFO: Hello from MixedBagOfTricks
注意: unwrapCompanionClass()
方法确保我们不会生成以伴随对象命名的记录器,而是以封闭的 class 命名。这是当前推荐的查找包含伴随对象的 class 的方法。使用 removeSuffix()
从名称中删除“$Companion” 不起作用,因为可以为伴随对象指定自定义名称。
KISS:对于 Java 迁移到 Kotlin 的团队
如果您不介意在记录器的每个实例化中提供 class 名称(就像 java),您可以通过在某处将其定义为顶级函数来保持简单在您的项目中:
import org.slf4j.LoggerFactory
inline fun <reified T:Any> logger() = LoggerFactory.getLogger(T::class.java)
这使用了 Kotlin reified type parameter。
现在,您可以按如下方式使用它:
class SomeClass {
// or within a companion object for one-instance-per-class
val log = logger<SomeClass>()
...
}
这种方法非常简单并且接近于 java 等价物,但只是添加了一些语法糖。
下一步:扩展或委托
我个人更喜欢更进一步,使用扩展或委托方法。 @JaysonMinard 的回答很好地总结了这一点,但这里是 TL;DR for the "Delegate" approach with the log4j2 API (UPDATE: no need to write this不再手动编写代码,因为它已作为 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 Logging API module, which is now an official part of Log4j2 (disclaimer: I am the primary author). You can download this directly from Apache, or via Maven Central。
Usage 基本上如上所述,但是该模块支持基于接口的记录器访问,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()
其次,您可以定义一个提供记录器及其混合实现的接口。
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"}
}
}
我也写了一篇博客 post 将其与 AnkoLogger
进行比较:Logging in 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 project - kotlin-logging ) 库允许您编写如下日志代码:
class FooWithLogging {
companion object: KLogging()
fun bar() {
logger.info{"Item $item"}
}
}
静态日志
或者您也可以使用这个用 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")
木材
如果您已经使用过 Jake Wharton 的 Timber
日志库,请检查 timberkt
。
This library builds on Timber with an API that's easier to use from Kotlin. Instead of using formatting parameters, you pass a lambda that is only evaluated if the message is logged.
代码示例:
// Standard timber
Timber.d("%d %s", intVar + 3, stringFun())
// Kotlin extensions
Timber.d { "${intVar + 3} ${stringFun()}" }
// or
d { "${intVar + 3} ${stringFun()}" }
同时检查:Logging in 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()
}
}
这里已经有很多很好的答案,但它们都涉及向 class 添加记录器,但是您将如何做才能在顶级函数中进行记录?
这种方法通用且简单,可以在 classes、伴随对象和顶级函数中很好地工作:
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")
}
}
这仍然是 WIP(即将完成)所以我想分享一下:
https://github.com/leandronunes85/log-format-enforcer#kotlin-soon-to-come-in-version-14
这个库的主要目标是在整个项目中强制执行特定的日志样式。通过让它生成 Kotlin 代码,我试图解决这个问题中提到的一些问题。关于最初的问题,我通常倾向于简单地做:
private val LOG = LogFormatEnforcer.loggerFor<Foo>()
class Foo {
}
您可以简单地构建自己的 "library" 实用程序。您不需要为此任务使用大型库,这会使您的项目变得更重、更复杂。
例如,您可以使用 Kotlin 反射来获取任何 class 属性.
的名称、类型和值
首先,确保你已经在你的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 上登录,在 iOS 上登录 os_log,在 JavaScript 上登录控制台。
- 高性能。禁用时只有 1 个布尔检查。我喜欢放入大量日志记录,并希望在发布时将其全部关闭,并且不想为拥有大量日志记录付出太多开销。此外,当日志记录打开时,它需要非常高效。
- 可扩展。需要能够添加其他记录器,例如记录到 Crashlytics 等。
- 每个记录器可以在不同的级别记录。例如,您可能只希望将信息及以上内容发送到 Crashlytics 和在生产中禁用的所有其他记录器。
使用:
val log = logging()
log.i { "some message" }
Kotlin 没有 Java 中使用的相同静态字段概念。在 Java 中,普遍接受的日志记录方式是:
public class Foo {
private static final Logger LOG = LoggerFactory.getLogger(Foo.class);
}
问题在 Kotlin 中执行日志记录的惯用方式是什么?
我没听说过这方面的成语。 越简单越好,所以我会用顶级的属性
val logger = Logger.getLogger("package_name")
这种做法在 Python 中很有效,尽管 Kotlin 和 Python 可能看起来不同,但我相信它们在“精神”(说成语)上非常相似。
通常来说,这就是伴随对象的用途:替换静态内容。
作为日志记录实现的一个很好的例子,我想提一下 Anko,它使用一个特殊的接口 AnkoLogger
,需要日志记录的 class 应该实现这个接口。在界面内部,有代码为 class 生成日志标记。然后通过扩展函数完成日志记录,这些函数可以在接口实现中调用,无需前缀甚至创建记录器实例。
我不认为这是 惯用的 ,但这似乎是一个很好的方法,因为它需要最少的代码,只需将接口添加到 class 声明中,并且您会为不同的 classes.
使用不同的标签进行记录下面的代码基本上是 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")
}
这样的事情对你有用吗?
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 代码中,您会发现以下其中一种模式。使用 属性 Delegates 的方法利用了 Kotlin 的强大功能来生成最小的代码。
注意:此处的代码适用于 java.util.Logging
但同样的理论适用于任何日志库
类静态(常见,相当于问题中的Java代码)
如果您不相信日志系统中哈希查找的性能,您可以通过使用一个伴随对象来获得与您的 Java 代码类似的行为,该对象可以包含一个实例并且感觉像静态的你.
class MyClass {
companion object {
val LOG = Logger.getLogger(MyClass::class.java.name)
}
fun foo() {
LOG.warning("Hello from MyClass")
}
}
创建输出:
Dec 26, 2015 11:28:32 AM
org.Whosebug.kotlin.test.MyClass
foo INFO: Hello from MyClass
这里有更多关于伴生对象的信息:Companion Objects ... 另请注意,在上面的示例中,MyClass::class.java
为记录器获取类型 Class<MyClass>
的实例,而 this.javaClass
将获得 Class<MyClass.Companion>
.
每个 Class 实例(常见)
但是,确实没有理由避免在实例级别调用和获取记录器。您提到的惯用 Java 方式已经过时并且基于对性能的恐惧,而每个 class 的记录器已经被地球上几乎所有合理的日志记录系统缓存。只需创建一个成员来保存记录器对象。
class MyClass {
val LOG = Logger.getLogger(this.javaClass.name)
fun foo() {
LOG.warning("Hello from MyClass")
}
}
创建输出:
Dec 26, 2015 11:28:44 AM org.Whosebug.kotlin.test.MyClass foo INFO: Hello from MyClass
您可以对每个实例和每个 class 变体进行性能测试,看看大多数应用程序是否存在实际差异。
属性代表(普通,最优雅)
@Jire 在另一个答案中建议的另一种方法是创建一个 属性 委托,然后您可以使用它在您想要的任何其他 class 中统一执行逻辑.有一种更简单的方法可以做到这一点,因为 Kotlin 已经提供了一个 Lazy
委托,我们可以将它包装在一个函数中。这里的一个技巧是,如果我们想知道当前使用委托的 class 的类型,我们将其作为任何 class:
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"
此代码还确保如果您在伴随对象中使用它,记录器名称将与您在 class 本身上使用时的名称相同。现在您可以简单地:
class Something {
val LOG by logger()
fun foo() {
LOG.info("Hello from Something")
}
}
for per class instance,或者如果您希望每个 class 一个实例更静态:
class SomethingElse {
companion object {
val LOG by logger()
}
fun foo() {
LOG.info("Hello from SomethingElse")
}
}
在这两个 classes 上调用 foo()
的输出将是:
Dec 26, 2015 11:30:55 AM org.Whosebug.kotlin.test.Something foo INFO: Hello from Something
Dec 26, 2015 11:30:55 AM org.Whosebug.kotlin.test.SomethingElse foo INFO: Hello from SomethingElse
扩展函数(在这种情况下不常见,因为 Any 命名空间的“污染”)
Kotlin 有一些隐藏的技巧,可以让您将一些代码变得更小。您可以在 classes 上创建扩展函数,从而为它们提供额外的功能。上面评论中的一个建议是使用记录器功能扩展 Any
。每当有人在 class 中的 IDE 中使用代码完成时,这都会产生噪音。但是扩展 Any
或其他一些标记接口有一个秘密的好处:您可以暗示您正在扩展自己的 class 并因此检测到您所在的 class 。嗯?为了减少混淆,这里是代码:
// extend any class with the ability to get a logger
fun <T: Any> T.logger(): Logger {
return Logger.getLogger(unwrapCompanionClass(this.javaClass).name)
}
现在在 class(或伴随对象)中,我可以简单地自己调用此扩展 class:
class SomethingDifferent {
val LOG = logger()
fun foo() {
LOG.info("Hello from SomethingDifferent")
}
}
生成输出:
Dec 26, 2015 11:29:12 AM org.Whosebug.kotlin.test.SomethingDifferent foo INFO: Hello from SomethingDifferent
基本上,代码被视为对扩展 Something.logger()
的调用。问题是以下情况也可能对其他 classes 造成“污染”:
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:
中使用这些变体中的任何一个class MarkedClass: Loggable {
val LOG = logger()
}
生成输出:
Dec 26, 2015 11:41:01 AM org.Whosebug.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()
}
当然,抽象基 class 也可以做同样的事情,可以选择接口和实现该接口的抽象 class 允许灵活性和统一性:
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 个记录器实例将生成相同的记录器名称,并输出:
Dec 26, 2015 11:39:00 AM org.Whosebug.kotlin.test.MixedBagOfTricks foo INFO: Hello from MixedBagOfTricks
注意: unwrapCompanionClass()
方法确保我们不会生成以伴随对象命名的记录器,而是以封闭的 class 命名。这是当前推荐的查找包含伴随对象的 class 的方法。使用 removeSuffix()
从名称中删除“$Companion” 不起作用,因为可以为伴随对象指定自定义名称。
KISS:对于 Java 迁移到 Kotlin 的团队
如果您不介意在记录器的每个实例化中提供 class 名称(就像 java),您可以通过在某处将其定义为顶级函数来保持简单在您的项目中:
import org.slf4j.LoggerFactory
inline fun <reified T:Any> logger() = LoggerFactory.getLogger(T::class.java)
这使用了 Kotlin reified type parameter。
现在,您可以按如下方式使用它:
class SomeClass {
// or within a companion object for one-instance-per-class
val log = logger<SomeClass>()
...
}
这种方法非常简单并且接近于 java 等价物,但只是添加了一些语法糖。
下一步:扩展或委托
我个人更喜欢更进一步,使用扩展或委托方法。 @JaysonMinard 的回答很好地总结了这一点,但这里是 TL;DR for the "Delegate" approach with the log4j2 API (UPDATE: no need to write this不再手动编写代码,因为它已作为 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 Logging API module, which is now an official part of Log4j2 (disclaimer: I am the primary author). You can download this directly from Apache, or via Maven Central。
Usage 基本上如上所述,但是该模块支持基于接口的记录器访问,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()
其次,您可以定义一个提供记录器及其混合实现的接口。
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"}
}
}
我也写了一篇博客 post 将其与 AnkoLogger
进行比较:Logging in 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 project - kotlin-logging ) 库允许您编写如下日志代码:
class FooWithLogging {
companion object: KLogging()
fun bar() {
logger.info{"Item $item"}
}
}
静态日志
或者您也可以使用这个用 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")
木材
如果您已经使用过 Jake Wharton 的 Timber
日志库,请检查 timberkt
。
This library builds on Timber with an API that's easier to use from Kotlin. Instead of using formatting parameters, you pass a lambda that is only evaluated if the message is logged.
代码示例:
// Standard timber
Timber.d("%d %s", intVar + 3, stringFun())
// Kotlin extensions
Timber.d { "${intVar + 3} ${stringFun()}" }
// or
d { "${intVar + 3} ${stringFun()}" }
同时检查:Logging in 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()
}
}
这里已经有很多很好的答案,但它们都涉及向 class 添加记录器,但是您将如何做才能在顶级函数中进行记录?
这种方法通用且简单,可以在 classes、伴随对象和顶级函数中很好地工作:
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")
}
}
这仍然是 WIP(即将完成)所以我想分享一下: https://github.com/leandronunes85/log-format-enforcer#kotlin-soon-to-come-in-version-14
这个库的主要目标是在整个项目中强制执行特定的日志样式。通过让它生成 Kotlin 代码,我试图解决这个问题中提到的一些问题。关于最初的问题,我通常倾向于简单地做:
private val LOG = LogFormatEnforcer.loggerFor<Foo>()
class Foo {
}
您可以简单地构建自己的 "library" 实用程序。您不需要为此任务使用大型库,这会使您的项目变得更重、更复杂。
例如,您可以使用 Kotlin 反射来获取任何 class 属性.
的名称、类型和值首先,确保你已经在你的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 上登录,在 iOS 上登录 os_log,在 JavaScript 上登录控制台。
- 高性能。禁用时只有 1 个布尔检查。我喜欢放入大量日志记录,并希望在发布时将其全部关闭,并且不想为拥有大量日志记录付出太多开销。此外,当日志记录打开时,它需要非常高效。
- 可扩展。需要能够添加其他记录器,例如记录到 Crashlytics 等。
- 每个记录器可以在不同的级别记录。例如,您可能只希望将信息及以上内容发送到 Crashlytics 和在生产中禁用的所有其他记录器。
使用:
val log = logging()
log.i { "some message" }