Running a Kotlin DSL from an external file
This article will explain how to run a Kotlin DSL from an external file in a Spring Boot application running in your IDE or from a JAR file.
Kotlin DSL
Kotlin is a programming language with resources to create Domain-Specific languages.
In this article, we are going to use a very simple DSL written in Kotlin that returns an uppercase version of a message. This is the final example of the content of a file called my-dsl.kts
import com.johnowl.runkotlindslfromexternalfile.dsl.*
myDsl {
message = "Hello World!"
}
Explaining how to create a DSL is out of the scope of this article, but this is the implementation of this simple and useless DSL:
package com.johnowl.runkotlindslfromexternalfile.dsl
data class MyDsl(
var message: String = "Unknown"
)
fun myDsl(block: MyDsl.() -> Unit): MyDsl =
MyDsl().apply(block)
This DSL implementation populates a data class. This is the processor for the DSL:
@Service
class MyDslProcessor {
fun process(dsl: MyDsl): String {
return dsl.message.uppercase()
}
}
The processor receives the MyDSL
data class and returns the uppercase message. We have the @Service
annotation because we are using the Spring Framework. Now we can use our DSL in a Spring Controller, for example:
@RestController
class MyDslController(
private val processor: MyDslProcessor
) {
@GetMapping("/dsl/v1")
fun processV1(): String {
val myDsl = myDsl { // (1)
message = "Hello World!"
}
return processor.process(myDsl) // (2)
}
}
In the line commented with(1)
, we can see that we are using our DSL, and in the one with(2)
, we are returning the result.
This is nice, but what if we need to store the content of our DSL in external .kts
files?
How to run a Kotlin DSL from an external file
First, you need to add the dependency below to your project:
Maven:
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-scripting-jsr223</artifactId>
<version>${kotlin.version}</version>
</dependency>
Gradle:
implementation 'org.jetbrains.kotlin:kotlin-scripting-jsr223'
Now, you can use this dependency to create your parser as follows:
@Service
class MyDslParser {
fun parse(filename: String): MyDsl {
val fileContent = File("src/main/resources/$filename").readText()
val scriptEngine = ScriptEngineManager().getEngineByExtension("kts")
val parsedDsl = scriptEngine.eval(fileContent)
if (parsedDsl !is MyDsl) {
throw Exception("Script does not return a MyDsl")
}
return parsedDsl
}
}
This is going to evaluate any Kotlin code inside the file; be careful and use only trusted files.
Create a file called my-dsl.kts
in the resources folder of your Spring Boot application with the content:
import com.johnowl.runkotlindslfromexternalfile.dsl.*
myDsl {
message = "Hello World from the external file!"
}
Add a new mapping in your controller:
@GetMapping("/dsl/v2")
fun process(): String {
val dsl = MyDslParser().parse("my-dsl.kts")
return processor.process(dsl)
}
This is going to return the uppercase version of the text "Hello World from the external file!".
This runs fine in Intellij, but what if you create a jar
file to run in production? The application will start normally, but when you call the /dsl/v2
endpoint a runtime error similar to the one below will happen:
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: javax.script.ScriptException: ERROR Unable to initialize repl compiler:
DEBUG Using JDK home inferred from java.home: /path/to/java/home
ERROR Unable to find kotlin stdlib, please specify it explicitly via "kotlin.java.stdlib.jar" property: java.lang.Exception: Unable to find kotlin stdlib, please specify it explicitly via "kotlin.java.stdlib.jar" property: java.lang.IllegalStateException: Unable to initialize repl compiler:
DEBUG Using JDK home inferred from java.home: /path/to/java/home
ERROR Unable to find kotlin stdlib, please specify it explicitly via "kotlin.java.stdlib.jar" property: java.lang.Exception: Unable to find kotlin stdlib, please specify it explicitly via "kotlin.java.stdlib.jar" property] with root cause
This is not what you want. See how to solve it in the next chapter.
How to run a Kotlin DSL from an external file in a Spring Boot JAR
You need to tell the Spring Boot plugin to unpack some dependencies because some libraries are trying to read files from a specific place, and without unpacking, it doesn't work.
For Maven, add the requiresUnpack
to the Spring Boot Maven plugin configuration in the pom.xml
file:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<requiresUnpack>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-compiler-embeddable</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-scripting-jsr223</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</dependency>
</requiresUnpack>
</configuration>
</plugin>
For Gradle, add the requiresUnpack
configuration at the end of the build.gradle
file:
tasks.named("bootJar") {
requiresUnpack '**/kotlin-scripting-jsr223-*.jar', '**/kotlin-stdlib-*.jar', '**/kotlin-compiler-embeddable-*.jar'
}
It was tested and works with Spring Boot 3.2.0 and Kotlin 1.9.2.
Conclusion
We saw it’s possible to run a Kotlin DSL from an external file, and if you work with Spring Boot, with some extra configuration, it's still possible.
This is something that my team learned recently in a project we are working on. So, I have decided to share because it can also be someone else’s question.
Enjoy the possibilities of writing DSLs in Kotlin!
Do you think you have what it takes to be one of us?
At WAES, we are always looking for the best developers and data engineers to help Dutch companies succeed. If you are interested in becoming a part of our team and moving to The Netherlands, look at our open positions here.
WAES publication
Our content creators constantly create new articles about software development, lifestyle, and WAES. So make sure to follow us on Medium to learn more.
Also, make sure to follow us on our social media:
LinkedIn — Instagram — Twitter — YouTube