This content is not available in your language... So if you don't understand the language... well, at least you can appreciate the pictures of the post, right?

Hoje eu decidi usar o Script Engine do Kotlin na Loritta, afinal, usar o Nashorn estava meio chato e eu 💖 Kotlin, então eu decidi tentar usar.

Criei uma pequena classe de testes no meu projeto, rodei ele dentro do IntelliJ IDEA, nenhum erro! Agora vamos tentar rodar em produção... huh? unresolved reference? mas... como? Se essa classe não existivesse você nem poderia ter sido executado!

javax.script.ScriptException: error: unresolved reference: mrpowergamerbr
fun loritta(context: com.mrpowergamerbr.loritta.commands.CommandContext) {
                         ^

É, por algum motivo o Script Engine do Kotlin não consegue encontrar as minhas classes, ou qualquer outra classe na verdade, mas... porque? Elas existem durante o runtime porque, se não existissem, o aplicativo nem iria rodar!

Após pensar e procurar no Google, eu encontrei esta issue no YouTrack mas ela é relacionada a fat JARs, e o meu aplicativo em produção não usa fat JARs, mas decidi investigar...

E após várias tentativas, eu consegui fazer funcionar! Mas porque não estava funcionando? Bem...

Hoje eu decidi parar de compilar fat JARs, antes a minha JAR era um good boye, pesando apenas ~10MBs... mas já que o meu aplicativo estava crescendo com mais funcionalidades, o tamanho da JAR continuou a crescer devido a novas dependências e novos códigos adicionados... aí a JAR estava pesando ~70MBs e o good boye já não era mais um good boye, agora era um fatty boye

fatty boye

Então eu decidi tirar isso, parar de compilar o fat JAR diminuiu o tempo de compilação em 10s (e ainda diminuiu uns 20s na hora de copiar a JAR para o meu dedicado!, já que agora eu só copio as dependências quando preciso atualizar elas!), fiz que todas as dependências fossem exportadas para a pasta libs/ e fiz que o Maven adicionasse o Class-Path para o MANIFEST.MF e tudo funcionou bem! ...exceto o Script Engine do Kotlin.

O motivo de não funcionar é porque, por algum motivo, o Script Engine do Kotlin não estava nem pegando as minhas próprias classes da JAR! Mas espere, tem um jeito de arrumar... e a correção surgiu naquela issue do YouTrack que eu passei antes, mesmo se você não está usando fat JARs!

Você pode adicionar todas as dependências manualmente utilizando o -Dkotlin.script.classpath=jar1:jar2:jar3:jar4 no seu script de inicialização...

java -Dkotlin.script.classpath=libs/dependency.jar:libs/dependency2.jar:your-app.jar -jar your-app.jar

Mas aí você tem que atualizar o script de inicialização toda hora que você atualizar/remover/adicionar dependências, e as vezes você pode até esquecer de atualizar aquela property, causando problemas em seus scripts... então vamos atualizar a property pelo código!

val path = this::class.java.protectionDomain.codeSource.location.path
val jar = JarFile(path)
val mf = jar.manifest
val mattr = mf.mainAttributes
// Yes, you SHOULD USE Attributes.Name.CLASS_PATH! Don't try using "Class-Path", it won't work!
val manifestClassPath = mattr[Attributes.Name.CLASS_PATH] as String

// The format within the Class-Path attribute is different than the one expected by the property, so let's fix it!
// By the way, don't forget to append your original JAR at the end of the string!
val propClassPath = manifestClassPath.replace(" ", ":") + ":Loritta-0.0.1-SNAPSHOT.jar"

// Now we set it to our own classpath
System.setProperty("kotlin.script.classpath", propClassPath)

Sim, eu sei, você também poderia atualizar aquela property usando bash scripts ou qualquer outra coisa, mas este é o jeito que eu resolvi fazer (e funciona, e agora que você sabe como arrumar, você pode fazer a sua própria solução (que talvez até seja melhor que a minha!)).

E depois de fazer isto, você agora pode fazer evaluation do seu código (mesmo se ele dependa de classes externas) sem ter nenhum problema!

javaScript = "fun test() { println(\"Hello World!\") }"

val engine = ScriptEngineManager().getEngineByName("kotlin")
engine.eval(javaScript)
val invocable = engine as Invocable
invocable.invokeFunction("test")

Antes da mudança

/assets/img/kotlin-classpath/before.png

Depois da mudança

/assets/img/kotlin-classpath/after.png

Isto é provavelmente um problema que quase ninguém irá ter? Sim, mas mesmo assim, talvez isto será útil para outra pessoa. 😄

E é isto! Resolvi postar isto para caso alguém tenha o mesmo problema. (E para evitar isto)