1. 简介
在本教程中,我们将探讨几种适用于 Kotlin 编程风格的日志惯用写法。
日志是软件开发中不可或缺的一部分。虽然看似简单(只是输出信息而已),但实现方式却多种多样。
每种语言、操作系统和环境都有其惯用甚至特立独行的日志解决方案。Kotlin 也不例外。
我们将以日志为切入点,深入探讨 Kotlin 中的一些高级特性,并分析其细微差别。
2. 日志惯用写法概览
日志的实现方式多种多样,但在 Kotlin 中,我们可以通过以下几种常见方式来实现:
- 类属性方式
- 伴生对象方式
- 扩展方法
- 委托属性
我们将依次分析这些方式的优缺点,帮助你选择最适合项目需求的写法。
3. 环境准备
本教程中我们将使用 SLF4J 作为日志门面,Logback 作为实现。
在 pom.xml
中添加以下依赖:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
如果你打算使用其他日志实现(如 Log4j 或 Java Util Logging),这些方式同样适用。
4. 将 Logger 作为类属性
最直接的方式是将日志记录器作为类的私有属性:
class Property {
private val logger = LoggerFactory.getLogger(javaClass)
fun log(s: String) {
logger.info(s)
}
}
✅ 优点:
- 简洁直观
- 可复制粘贴
❌ 缺点:
- 每个实例都会持有一个 logger,浪费内存
- 每次实例化都会触发一次缓存查找
4.1. 提取通用方法
我们可以将 getLogger
抽取为一个工具方法,减少重复代码:
fun getLogger(forClass: Class<*>): Logger =
LoggerFactory.getLogger(forClass)
这样我们就可以简化日志声明:
private val logger = getLogger(javaClass)
5. 使用伴生对象定义 Logger
Java 中我们通常使用 static
定义 logger,Kotlin 中则可以使用伴生对象模拟静态属性。
5.1. 基本写法
class LoggerInCompanionObject {
companion object {
private val loggerWithExplicitClass =
getLogger(LoggerInCompanionObject::class.java)
}
fun log(s: String) {
loggerWithExplicitClass.info(s)
}
}
⚠️ 注意:
- 使用
LoggerInCompanionObject::class.java
会导致日志名称固定,不利于复制粘贴
5.2. 使用 javaClass 的问题
如果我们直接使用 javaClass
:
private val loggerWithWrongClass = getLogger(javaClass)
输出的日志名称会变成:
com.baeldung.kotlin.logging.LoggerInCompanionObject$Companion
这显然不是我们想要的。
5.3. 通过反射获取外层类
为了自动获取外层类名,我们可以使用 Kotlin 反射:
@Suppress("JAVA_CLASS_ON_COMPANION")
private val logger = getLogger(javaClass.enclosingClass)
需要添加 kotlin-reflect
依赖:
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>1.2.51</version>
</dependency>
5.4. 使用 @JvmStatic
为了让伴生对象更像 Java 的 static
,我们可以加上 @JvmStatic
:
@JvmStatic
private val logger = getLogger(javaClass.enclosingClass)
5.5. 完整示例
class LoggerInCompanionObject {
companion object {
@Suppress("JAVA_CLASS_ON_COMPANION")
@JvmStatic
private val logger = getLogger(javaClass.enclosingClass)
}
fun log(s: String) {
logger.info(s)
}
}
✅ 优点:
- 静态属性,节省内存
- 日志名称正确
- 可复制粘贴
6. 使用扩展方法
扩展方法可以让我们在不修改类的前提下添加日志能力。
6.1. 初步尝试
fun <T : Any> T.logger(): Logger = getLogger(javaClass)
使用方式:
class LoggerAsExtensionOnAny {
fun log(s: String) {
logger().info(s)
}
}
6.2. Any 类型污染问题
上述写法会让所有类型都拥有 logger()
方法,比如:
"foo".logger().info("uh-oh!")
输出:
INFO java.lang.String - uh-oh!
⚠️ 问题:
- 方法污染所有类型
- 破坏封装性
- IDE 自动补全建议过多
6.3. 使用标记接口限制作用域
定义一个空接口:
interface Logging
然后修改扩展方法:
fun <T : Logging> T.logger(): Logger = getLogger(javaClass)
使用时需实现接口:
class LoggerAsExtensionOnMarkerInterface : Logging {
fun log(s: String) {
logger().info(s)
}
}
6.4. 使用 reified 类型参数优化
使用 reified
可以避免运行时反射:
inline fun <reified T : Logging> T.logger(): Logger =
getLogger(T::class.java)
⚠️ 注意:
- 这会改变继承行为(后续详述)
6.5. 与属性结合使用
扩展方法也可以用于初始化属性:
val logger = logger()
6.6. 与伴生对象结合使用
如果想在伴生对象中使用:
companion object : Logging {
val logger = logger()
}
需要处理 javaClass
的问题:
inline fun <T : Any> getClassForLogging(javaClass: Class<T>): Class<*> {
return javaClass.enclosingClass?.takeIf {
it.kotlin.companionObject?.java == javaClass
} ?: javaClass
}
更新扩展方法:
inline fun <reified T : Logging> T.logger(): Logger =
getLogger(getClassForLogging(T::class.java))
7. 使用委托属性
委托属性是一种更高级的方式,可以避免命名空间污染,也不需要标记接口。
7.1. 实现方式
class LoggerDelegate<in R : Any> : ReadOnlyProperty<R, Logger> {
override fun getValue(thisRef: R, property: KProperty<*>) =
getLogger(getClassForLogging(thisRef.javaClass))
}
使用方式:
private val logger by LoggerDelegate()
✅ 优点:
- 不污染 Any
- 支持伴生对象
❌ 缺点:
- 每次访问属性都会重新计算
- 使用反射,性能略低
8. 关于继承的一些注意事项
不同方式在继承时的行为不同:
方式 | 日志名称是否随子类变化 |
---|---|
属性 + javaClass |
✅ |
伴生对象 + T::class.java |
❌ |
扩展方法 + reified |
❌ |
委托属性 + thisRef.javaClass |
✅ |
例如:
open class LoggerAsPropertyDelegate {
protected val logger by LoggerDelegate()
}
class DelegateSubclass : LoggerAsPropertyDelegate() {
fun show() {
logger.info("look!")
}
}
输出:
INFO com.baeldung.kotlin.logging.DelegateSubclass - look!
即使 logger 定义在父类中,输出的仍是子类名称。
9. 总结
在本文中,我们探讨了 Kotlin 中定义和使用日志记录器的几种常见方式:
方式 | 内存效率 | 可复用性 | 日志名称准确性 | 复杂度 |
---|---|---|---|---|
属性 | ❌ | ✅ | ✅ | ⭐ |
伴生对象 | ✅ | ✅ | ✅ | ⭐⭐ |
扩展方法 | ✅ | ✅ | ⚠️ | ⭐⭐⭐ |
委托属性 | ✅ | ✅ | ✅ | ⭐⭐⭐⭐ |
✅ 推荐:
- 如果你追求简洁,使用属性方式
- 如果你追求性能和规范,使用伴生对象 +
@JvmStatic
- 如果你想统一日志行为,使用委托属性或扩展方法
完整代码示例请查看 GitHub。