Idiomatic Logging em Kotlin

Idiomatic Logging em Kotlin

1. Introdução

Neste tutorial, vamos dar uma olhada em alguns idiomas de registro que se encaixam nos estilos de programaçãoKotlin típicos.

2. Idiomas de registro

O registro é uma necessidade onipresente na programação. Embora aparentemente seja uma idéia simples (apenas imprima coisas!), Há muitas maneiras de fazer isso.

De fato, todo idioma, sistema operacional e ambiente possui sua própria solução de log idiomática e, às vezes, idiossincrática; frequentemente, na verdade, mais de um.

Aqui, vamos nos concentrar na história de registro de Kotlin.

Também usaremos o registro como pretexto para mergulhar em alguns recursos avançados do Kotlin e explorar suas nuances.

3. Configuração

Para os exemplos de código, usaremos a bibliotecaSLF4J, mas os mesmos padrões e soluções se aplicam aLog4J,JUL e outras bibliotecas de registro.

Então, vamos começar incluindo as dependênciasSLF4J APIeLogback em nosso pom:


    org.slf4j
    slf4j-api
    1.7.25


    ch.qos.logback
    logback-classic
    1.2.3


    ch.qos.logback
    logback-core
    1.2.3

Agora, vamos dar uma olhada na aparência de registro para quatro abordagens diferentes:

  • Uma propriedade

  • Um objeto complementar

  • Um método de extensão e

  • Uma propriedade delegada

4. Logger como uma propriedade

A primeira coisa que podemos tentar é declarar uma propriedade do logger sempre que precisar:

class Property {
    private val logger = LoggerFactory.getLogger(javaClass)

    //...
}

Aqui, usamosjavaClass para calcular dinamicamente o nome do logger a partir do nome da classe de definição. Assim, podemos copiar e colar prontamente esse trecho onde quisermos.

Em seguida, podemos usar o logger em qualquer método da classe declarante:

fun log(s: String) {
    logger.info(s)
}

Decidimos declarar o logger comoprivate porque não queremos que outras classes, incluindo subclasses, tenham acesso a ele e façam logon em nome de nossa classe.

Of course, this is merely a hint para programadores, em vez de uma regra fortemente aplicada, uma vez que é fácil obter um registrador com o mesmo nome.

4.1. Salvando um pouco de digitação

Poderíamos encurtar nosso código um pouco fatorando a chamadagetLogger para uma função:

fun getLogger(forClass: Class<*>): Logger =
  LoggerFactory.getLogger(forClass)

E ao colocar isso em uma classe de utilidade,we can now simply call getLogger(javaClass) instead of LoggerFactory.getLogger(javaClass) throughout the samples below.

5. Logger em um objeto companheiro

Embora o último exemplo seja poderoso em sua simplicidade, não é o mais eficiente.

Primeiro, manter uma referência a um criador de logs em cada instância da classe custa memória. Em segundo lugar, embora os loggers sejam armazenados em cache, ainda incorreremos em uma pesquisa de cache para cada instância de objeto que tenha um logger.

Vamos ver se os objetos companheiros se saem melhor.

5.1. Uma primeira tentativa

Em Java, declarar o logger comostatic é um padrão que aborda as preocupações acima.

Em Kotlin, porém, não temos propriedades estáticas.

Mas podemos emulá-los comhttps://www.example.com/kotlin-objects:

class LoggerInCompanionObject {
    companion object {
        private val loggerWithExplicitClass
          = getLogger(LoggerInCompanionObject::class.java)
    }

    //...
}

Observe como reutilizamos a função de conveniênciagetLogger da seção 4.1. Continuaremos nos referindo a ele ao longo do artigo.

Portanto, com o código acima, podemos usar novamente o logger exatamente como antes, em qualquer método da classe:

fun log(s: String) {
    loggerWithExplicitClass.info(s)
}

5.2. O que aconteceu comjavaClass?

Infelizmente, a abordagem acima vem com uma desvantagem. Porque estamos nos referindo diretamente à classe anexa:

LoggerInCompanionObject::class.java

perdemos a facilidade de copiar e colar.

But why not just use javaClass like before? Na verdade, não podemos. Se o tivéssemos, teríamos obtido incorretamente um registrador com o nome da classethe companion object’s:

//Incorrect!
class LoggerInCompanionObject {
    companion object {
        private val loggerWithWrongClass = getLogger(javaClass)
    }
}
//...
loggerWithWrongClass.info("test")

O exemplo acima produziria um nome de logger ligeiramente errado. Dê uma olhada no bit$Companion:

21:46:36.377 [main] INFO
com.example.kotlin.logging.LoggerInCompanionObject$Companion - test

Na verdade,IntelliJ IDEA marks the declaration of the logger with a warning, porque reconhece que a referência ajavaClass em um objeto companheiro provavelmente não é o que queremos.

5.3. Derivando o nome da classe com reflexão

Ainda assim, nem tudo está perdido.

Temos uma maneira de derivar o nome da classe automaticamente e restaurar nossa capacidade de copiar e colar o código, mas precisamos de uma reflexão extra para fazer isso.

Primeiro, vamos garantir que temos a dependênciakotlin-reflect em nosso pom:


    org.jetbrains.kotlin
    kotlin-reflect
    1.2.51

Em seguida, podemos obter dinamicamente o nome de classe correto para o log:

companion object {
    @Suppress("JAVA_CLASS_ON_COMPANION")
    private val logger = getLogger(javaClass.enclosingClass)
}
//...
logger.info("I feel good!")

Agora obteremos a saída correta:

10:00:32.840 [main] INFO
com.example.kotlin.logging.LoggerInCompanionObject - I feel good!

The reason we use enclosingClass resulta do fato de que os objetos companheiros, no final, são instâncias de classes internas, entãoenclosingClass  se refere à classe externa ou, neste caso,LoggerInCompanionObject.

Além disso, não há problema em suprimir o aviso que o IntelliJ IDEA dá emjavaClass, pois agora estamos fazendo a coisa certa com ele.

5.4. @JvmStatic

Enquanto as propriedades dos objetos companheiroslook como campos estáticos, os objetos companheiros são mais como singletons.

Os objetos complementares do Kotlin têm um recurso especial, pelo menos quando executados em uma JVM, queconverts companion objects to static fields:

@JvmStatic
private val logger = getLogger(javaClass.enclosingClass)

5.5. Juntando tudo

Vamos colocar todas as três melhorias juntas. Quando reunidas, essas melhorias tornam nossa construção de registro passível de cópia e estática:

class LoggerInCompanionObject {
    companion object {
        @Suppress("JAVA_CLASS_ON_COMPANION")
        @JvmStatic
        private val logger = getLogger(javaClass.enclosingClass)
    }

    fun log(s: String) {
        logger.info(s)
    }
}

6. Registrador de um método de extensão

Embora interessante e eficiente, o uso de um objeto complementar é detalhado. O que começou como uma linha agora é várias linhas para copiar e colar em toda a base de código.

Além disso, o uso de objetos complementares produz classes internas extras. Comparado com a declaração simples do registrador estático em Java, o uso de objetos complementares é mais pesado.

Então, vamos tentar uma abordagem usandoextension methods.

6.1. Uma primeira tentativa

A ideia básica é definir um método de extensão que retorne umLogger, para que toda classe que precisar dele possa apenas chamar o método e obter a instância correta.

Podemos definir isso em qualquer lugar no caminho de classe:

fun  T.logger(): Logger = getLogger(javaClass)

Os métodos de extensão são basicamente copiados para qualquer classe na qual são aplicáveis; então, podemos simplesmente nos referir diretamente ajavaClass  novamente.

E agora, todas as classes terão o métodologger como se tivesse sido definido no tipo:

class LoggerAsExtensionOnAny { // implied ": Any"
    fun log(s: String) {
        logger().info(s)
    }
}

While this approach is more concise than companion objects, podemos querer resolver alguns problemas com ele primeiro.

6.2. Poluição do tipoAny

Uma desvantagem significativa do nosso primeiro método de extensão é que ele polui o tipoAny.

Como definimos como aplicável a qualquer tipo, acaba sendo um pouco invasivo:

"foo".logger().info("uh-oh!")
// Sample output:
// 13:19:07.826 [main] INFO java.lang.String - uh-oh!

Ao definirlogger() emAny, temospolluted todos os tipos na linguagem com o método.

Isso não é necessariamente um problema. Isso não impede que outras classes tenham seus próprios métodoslogger.

No entanto, além do ruído extra,it also breaks encapsulation. Os tipos agora podem registrar uns para os outros, o que não queremos.

Elogger will agora aparece em quase todas as sugestões de código IDE.

6.3. Método de extensão em uma interface de marcador

We can narrow our extension method’s scope with a markerinterface:

interface Logging

Tendo definido essa interface, podemos indicar que nosso método de extensão se aplica apenas aos tipos que implementam essa interface:

fun  T.logger(): Logger = getLogger(javaClass)

E agora, se mudarmos nosso tipo para implementarLogging, podemos usarlogger como antes:

class LoggerAsExtensionOnMarkerInterface : Logging {
    fun log(s: String) {
        logger().info(s)
    }
}

6.4. Parâmetro de tipo reificado

Nos últimos dois exemplos, usamos reflexão para obter ajavaClass areia para dar um nome distinto ao nosso logger.

No entanto, também podemos extrair essas informações do parâmetro de tipoT, evitando uma chamada de reflexão em tempo de execução. Para conseguir isso, declararemos a função comoinline andreify the type parameter:

inline fun  T.logger(): Logger =
  getLogger(T::class.java)

Observe que isso altera a semântica do código em relação à herança. Discutiremos isso em detalhes na seção 8.

6.5. Combinando com propriedades de logger

Uma coisa boa sobre métodos de extensão é que podemos combiná-lo com nossa primeira abordagem:

val logger = logger()

6.6. Combinando com objetos complementares

Mas a história é mais complexa se quisermosuse our extension method in a companion object:

companion object : Logging {
    val logger = logger()
}

Porque teríamos o mesmo problema comjavaClass as antes:

com.example.kotlin.logging.LoggerAsExtensionOnMarkerInterface$Companion

Para explicar isso, vamos primeiro definir um método que obtém a classe de forma mais robusta:

inline fun  getClassForLogging(javaClass: Class): Class<*> {
    return javaClass.enclosingClass?.takeIf {
        it.kotlin.companionObject?.java == javaClass
    } ?: javaClass
}

Aqui,getClassForLogging retornaenclosingClass ifjavaClass se refere a um objeto companheiro.

E agora podemos novamente atualizar nosso método de extensão:

inline fun  T.logger(): Logger
  = getLogger(getClassForLogging(T::class.java))

Dessa forma, podemos realmente usar o mesmo método de extensão, quer o logger seja incluído como uma propriedade ou um objeto companheiro.

7. Logger como uma propriedade delegada

Por último, vamos dar uma olhada emdelhttps: //www.example.com/kotlin-delegated-properties [egated]properties.

O que é bom sobre essa abordagem é quewe avoid namespace pollution without requiring a marker interface:

class LoggerDelegate : ReadOnlyProperty {
    override fun getValue(thisRef: R, property: KProperty<*>)
     = getLogger(getClassForLogging(thisRef.javaClass))
}

Podemos então usá-lo com uma propriedade:

private val logger by LoggerDelegate()

Por causa degetClassForLogging, isso também funciona para objetos complementares:

companion object {
    val logger by LoggerDelegate()
}

E embora as propriedades delegadas sejam poderosas, observe quegetValue is re-computed each time the property is read.

Além disso, devemos lembrar quedelegate properties must use reflection para que funcione.

8. Algumas notas sobre herança

É muito comum ter um registrador por classe. E é por isso que também geralmente declaramos registradores comoprivate.

No entanto, há momentos em que queremos que nossas subclasses se refiram ao registrador de suas superclasses.

E, dependendo do nosso caso de uso, as quatro abordagens acima se comportarão de maneira diferente.

Em geral, quando usamos reflexão ou outros recursos dinâmicos, escolhemos a classe real do objeto em tempo de execução.

Mas, quando nos referimos estaticamente a uma classe ou a um parâmetro do tipo reificado por nome, o valor será corrigido no momento da compilação.

Por exemplo, com propriedades delegadas, uma vez que a instância do logger é obtida dinamicamente toda vez que a propriedade é lida, ela levará o nome da classe onde é usada:

open class LoggerAsPropertyDelegate {
    protected val logger by LoggerDelegate()
    //...
}

class DelegateSubclass : LoggerAsPropertyDelegate() {
    fun show() {
        logger.info("look!")
    }
}

Vejamos o resultado:

09:23:33.093 [main] INFO
com.example.kotlin.logging.DelegateSubclass - look!

Mesmo quelogger seja declarado na superclasse, ele imprime o nome da subclasse.

O mesmo acontece quando um logger é declarado como uma propriedade e instanciado usandojavaClass.

Eextension methods exhibit this behavior, too, unless we reify the type parameter.

Por outro lado,with reified generics, explicit class names and companion objects, a logger’s name stays the same across the type hierarchy.

9. Conclusões

Neste artigo, vimos várias técnicas Kotlin que podemos aplicar à tarefa de declarar e instanciar registradores.

Começando de maneira simples, aumentamos progressivamente a complexidade em uma série de tentativas para melhorar a eficiência e reduzir o padrão, analisando os objetos complementares do Kotlin, métodos de extensão e propriedades delegadas.

Como sempre, esses exemplos estão disponíveis emover on GitHub completos.