Lazily Initializing Implicit Logger in Every Class in Kotlin
A Guide to Simplifying Logging in Kotlin
How Kotlin(JVM) logger declaration works
If you're familiar with the Kotlin programming language on the JVM platform, you may have encountered the need for logging in your projects. Typically, this requires declaring a logger for each class in your code, using a library such as slf4j. While this approach is functional, it can become tedious and repetitive, especially as the number of classes in your project increases. In this blog, we'll explore an alternative method for class-level logging in Kotlin JVM that leverages implicit loggers, streamlining the process and reducing the boilerplate code you need to write.
A typical logger declaration using slf4j in Kotlin JVM may look like the following code sample:
import org.slf4j.LoggerFactory
class Test {
val log = LoggerFactory.getLogger(Test::class.java)
fun doSomething() {
log.info("doing things")
}
}
It's worth noting that if you choose to use a different logging framework, such as log4j2 or kotlin-logging, the code for declaring a logger may look different. For instance, here's a code sample for declaring a logger using kotlin-logging:
private val logger = KotlinLogging.logger {}
The syntax may vary based on the specific logging framework you choose, but the overall process of declaring a logger for each class remains the same.
How Lombok does this with less boilerplate in Java
If you're familiar with Java, you may have encountered Lombok, a library that reduces boilerplate code in your Java projects. One aspect of Lombok that can be particularly useful for logging is its ability to simplify the declaration of class-level loggers. With Lombok, a logger declaration can be reduced to just a single annotation, as demonstrated in this code sample:
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ExampleClass {
public static void main(String... args) {
log.error("Something is wrong here");
}
}
In this example, the @Slf4j
annotation generates a logger for the class using the slf4j framework. This greatly reduces the amount of code you need to write for logging, making it a convenient and efficient solution for logging in Java projects. While Kotlin does not have a direct equivalent to Lombok, there is another approache to streamlining logging that we'll explore in this blog.
Can we do something better in Kotlin?
As it turned out, we can.
Fortunately, Kotlin provides features that allow us to improve upon the standard method of declaring loggers for each class. One such feature is reified type parameters, which enables the creation of inline extension variables with preserved type information. With this in mind, we can create an inline extension variable that acts as an implicit logger for a class. The reified generics allow us to fetch class details to pass to the logger creation, and by storing the created loggers in a hashmap, we can avoid redundant recreations and ensure constant lookup times.
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import kotlin.reflect.KClass
val loggerMap = hashMapOf<KClass<*>, Logger>()
inline val <reified T> T.log: Logger
get() = loggerMap.getOrPut(T::class) {
LoggerFactory.getLogger(
if (T::class.isCompanion) T::class.java.enclosingClass else T::class.java)
}
Once this code is in classpath, declaring a logger for a class becomes incredibly simple. That is, you don't have to declare anything at all, just go ahead and use the log
variable and Kotlin compiler will create one for you the first time it is used.
class Test {
fun doSomething() {
log.info("doing things")
}
}
This reduces the amount of code required and makes the logging process more efficient and less prone to error. You can even put it in a library and use it for multiple applications.
Whether you're a seasoned developer or just starting out with Kotlin, implementing an implicit logger is a great way to streamline your logging process and save time in your development workflow. So, give it a try and see how it can simplify your projects.