Back To The Future: Server-Side Web Pages With Kotlin (Pt. 2)
Take a dive into experimental Kotlin libraries to create server-side web pages, now with the Kotlin Scripting library as well.
Join the DZone community and get the full member experience.
Join For FreeRecap: Server-Side Web Pages With Kotlin
In the first article, server-side web pages with Kotlin part 1, a brief history of web development was outlined: namely, the four main stages being static HTML page delivery; server-side programmatic creation of web pages; HTML templating engines, again server-side; and finally, client-side programmatic creation of web pages. While contemporary web development is mostly focused on the last of the four stages (i.e., creating web pages on the client side), there still exist good cases for rendering web pages on the server side of the web application; furthermore, new technologies like kotlinx.html – a library by the authors of Kotlin for generating HTML code via a domain-specific language (DSL) – provide additional options for server-side web development. To give an example, the following two approaches produce the same homepage for the Spring Boot-powered website of a hypothetical bookstore:
Templating Engine (Thymeleaf)
The basic workflow for rendering a webpage with a template engine like Thymeleaf is to create an HTML template page in the resources/templates
folder of the project, in this case home.html
:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<title>Bookstore - Home</title>
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<script th:src="@{/js/bootstrap.min.js}"></script>
</head>
<body>
<main class="flex-shrink-0">
<div th:insert="~{fragments/general.html :: header(pageName = 'Home')}"></div>
<div>
<h2>Welcome to the Test Bookstore</h2>
<h3>Our Pages:</h3>
<ul>
<li><a th:href="@{/authors}">Authors</a></li>
<li><a th:href="@{/books}">Books</a></li>
</ul>
</div>
</main>
<div th:insert="~{fragments/general.html :: footer}"></div>
</body>
</html>
Next, a web controller endpoint function needs to be created and return a string value that corresponds to the name of the template file in the resources folder (only without the .html
file extension):
@Controller
class HomeController {
@GetMapping(value= ["/", "/home"])
fun home() = "home"
}
kotlinx.html
While there are again two basic steps for rendering a web page using the kotlinx.html library, the difference here is that the HTML generation code can be placed directly within the class structure of the web application. First, create a class that generates the HTML code in string form:
@Service
class HomepageRenderer {
fun render(): String {
return writePage {
head {
title("Bookstore - Home")
link(href = "/css/bootstrap.min.css", rel = "stylesheet")
script(src = "/js/bootstrap.min.js") {}
}
body {
main(classes = "flex-shrink-0") {
header("Home")
div {
h2 { +"Welcome to the Test Bookstore" }
h3 { +"Our Pages:" }
ul {
li { a(href = "/authors") { +"Authors" } }
li { a(href = "/books") { +"Books" } }
}
}
}
footer()
}
}
}
}
Next, the web controller will return the string generated by the renderer class as the response body of the relevant web endpoint function:
@Controller
class HomeController(private val homepageRenderer: HomepageRenderer) {
@GetMapping(value = ["/", "/home"], produces = [MediaType.TEXT_HTML_VALUE])
@ResponseBody
fun home() = homepageRenderer.render()
}
Comparison
As mentioned in the previous article, this approach brings some appreciable benefits compared to the traditional approach of templating engines, such as having all relevant code in one location within the project and leveraging Kotlin’s typing system as well as its other features. However, there are also downsides to this approach, one of which is that the very embedding of the code within the (compiled) class structure means that no “hot reloading” is possible: any changes to the HTML-rendering code will require the server to restart compared to simply being able to refresh the target webpage when using a templating engine like Thymeleaf. As this concluding article will examine, this issue has a potential solution in the form of the Kotlin Scripting library.
Kotlin Scripting: An Introduction
As the name suggests, Kotlin Scripting is a library that allows a developer to write Kotlin code and have it execute not after having been compiled for the targeted platform, but rather as a script that has been read in and interpreted by the executing host. The potential upsides of this are obvious: leveraging the power of Kotlin’s typing system and other features while being able to execute the Kotlin code without having to re-compile the files after any refactorings. While the functionality is officially still in an “experimental” state, it already has a very prominent early adopter: Gradle’s Kotlin DSL, with which one can write Gradle build scripts in place of using the traditional Groovy-based script files. Moreover, a third-party library is available for enhancing the experience of developing and executing Kotlin script files, especially for command-line applications.
Adaptation Steps
For this exercise – which will again be based on the hypothetical bookstore website as used in the first article – it will be sufficient to apply a relatively simple “scripting host” that will load Kotlin scripts and execute them within a Spring Boot web application. Note that a tutorial for how to set up a basic script-executing application is available on Kotlin’s website – and it is upon this tutorial that the code setup in this article is based – but it contains some parts that are unnecessary (e.g., dynamically loading dependencies from Maven) as well as parts that are missing (e.g., passing arguments to a script), so a further explanation in the steps below will be provided.
Step 1: Dependencies
First, the following dependencies will need to be added to the project:
org.jetbrains.kotlin:kotlin-scripting-common
org.jetbrains.kotlin:kotlin-scripting-jvm
org.jetbrains.kotlin:kotlin-scripting-jvm-host
Note that these dependencies all follow the same naming conventions as the “mainstream” Kotlin dependencies – and are bundled in the same version releases as well – so one can use the kotlin()
dependency-handler helper function in Gradle (e.g., implementation(kotlin(“scripting-common”))
) as well as omit the package version if one uses the Kotlin Gradle plugin.
Step 2: Script Compilation Configuration
Next, an object needs to be defined that contains the configuration for how the Kotlin scripts will be loaded:
object HtmlScriptCompilationConfiguration : ScriptCompilationConfiguration(
{
jvm {
jvmTarget("19")
dependenciesFromCurrentContext("main", "kotlinx-html-jvm-0.8.0")
}
}
)
As seen above, the object needs to contain two configuration declarations:
- The version of the JVM that will be used to compile the Kotlin scripts – this needs to match the version of the JVM that executes the script host (i.e., the Spring Boot web application).
- Any dependencies that are to be passed into the context that loads and executes the Kotlin scripts. “
main
” is obligatory for importing the core Kotlin libraries; “kotlinx-html-jvm-0.8.0
” is for the kotlinx.html code that was introduced in the previous article.
Step 3: Script Placeholder Definition
With the script compilation configuration object defined, we can now define the abstract class that will serve as a placeholder for the scripts to be loaded and executed:
@KotlinScript(
fileExtension = "html.kts",
compilationConfiguration = HtmlScriptCompilationConfiguration::class
)
abstract class HtmlScript(@Suppress("unused") val model: Map<String, Any?>)
As the code demonstrates, it is necessary to pass in the file extension that will identify the script files that will use the previously defined compilation configuration. Furthermore, the abstract class’s constructor serves as the entry point for any variables that need to be passed into the script during execution; in this case, the parameter model
has been defined to serve in a similar manner to how the similarly-named model
object works for Thymeleaf’s HTML template files.
Step 4: Script Executor
After defining the script placeholder, it is now possible to define the code that will load and execute the scripts:
@Service
class ScriptExecutor {
private val logger = LoggerFactory.getLogger(ScriptExecutor::class.java)
private val compilationConfiguration = createJvmCompilationConfigurationFromTemplate<HtmlScript>()
private val scriptingHost = BasicJvmScriptingHost()
fun executeScript(scriptName: String, arguments: Map<String, Any?> = emptyMap()): String {
val file = File(Thread.currentThread().contextClassLoader.getResource(scriptName)!!.toURI())
val evaluationConfiguration = ScriptEvaluationConfiguration { constructorArgs(arguments) }
val response = scriptingHost.eval(file.toScriptSource(), compilationConfiguration, evaluationConfiguration)
response.reports.asSequence()
.filter { it.severity == ScriptDiagnostic.Severity.ERROR }
.forEach { logger.error("An error occurred while rendering {}: {}", scriptName, it.message) }
return (response.valueOrNull()?.returnValue as? ResultValue.Value)?.value as? String ?: ""
}
}
A couple of things of note to mention here:
- Evaluating a script in Kotlin requires two configurations: one for compilation, and one for evaluation. The former was defined in a previous step, whereas the latter needs to be generated for every script execution, as it is here that any arguments get passed into the script via the
constructorArgs()
function call (in our case, onto themodel
parameter defined in the previous step). - The script execution host will not throw any executions encountered when executing a script (e.g., syntax errors). Instead, it will aggregate all “reports” gathered during the script evaluation and return them in a parameter named as such in the
response
object. Thus, it is necessary to create a reporting mechanism (in this case, thelogger
object) after the fact to inform the developer and/or user if an exception has occurred. - There is no type definition for what a “successful” script execution should return; as such, the return value needs to be cast to the appropriate type before being returned out of the function.
Step 5: Script Definition
Now, the actual HTML-rendering script can be defined. Following on the examples from the previous article, we will be creating the script that renders the “view all authors” page of the website:
val authors = model[AUTHORS] as List<Author>
writePage {
head {
title("Bookstore - View Authors")
link(href = "/css/bootstrap.min.css", rel = "stylesheet")
script(src = "/js/bootstrap.min.js") {}
script(src = "/js/util.js") {}
}
body {
header("Authors")
div {
id = "content"
h2 { +"Our Books' Authors" }
ul {
authors.forEach { author ->
li {
form(method = FormMethod.post, action = "/authors/${author.id}/delete") {
style = "margin-block-end: 1em;"
onSubmit = "return confirmDelete('author', \"${author.name}\")"
a(href = "/authors/${author.id}") {
+author.name
style = "margin-right: 0.25em;"
}
button(type = ButtonType.submit, classes = "btn btn-danger") { +"Delete" }
}
}
}
}
a(classes = "btn btn-primary", href = "/authors/add") { +"Add Author" }
}
footer()
}
}
The items to note:
- IDE support for Kotlin scripting is limited, and it will currently (e.g., with IDEA 2022.3.2) not recognize arguments passed into the script like the
model
object. As a consequence, the IDE will probably mark the file as erroneous, even though this is not actually the case. - It is recommended to simulate the package structure within which the scripts should be placed. In this case, the above file is located in
resources/com/severett/thymeleafcomparison/kotlinscripting/scripting
and thus is marked as residing in the packagecom.severett.thymeleafcomparison.kotlinscripting.scripting.
This allows it to access the functions in the common.kt file like theheader()
andfooter()
functions that generate the boilerplate header and footer sections of the HTML code, respectively.
Step 6: Web Controller
The final step is to create the web controller that dictates which scripts should be executed to generate the HTML code for the web requests. The end result is similar to the approach for kotlinx.html – i.e., returning a response body of text/html – with the difference being what mechanism is actually called to generate the response body, in this case, the script executor defined above:
@Controller
@RequestMapping("/authors")
class AuthorController(private val authorService: AuthorService, private val scriptExecutor: ScriptExecutor) {
@GetMapping(produces = [TEXT_HTML])
@ResponseBody
fun getAll(): String {
return scriptExecutor.executeScript(
"$SCRIPT_LOCATION/viewauthors.html.kts",
mapOf(AUTHORS to authorService.getAll())
)
}
@GetMapping(value = ["/{id}"], produces = [TEXT_HTML])
@ResponseBody
fun get(@PathVariable id: Int): String {
return scriptExecutor.executeScript(
"$SCRIPT_LOCATION/viewauthor.html.kts",
mapOf(AUTHOR to authorService.get(id))
)
}
@GetMapping(value = ["/add"], produces = [TEXT_HTML])
@ResponseBody
fun add() = scriptExecutor.executeScript("$SCRIPT_LOCATION/addauthor.html.kts")
@PostMapping(value = ["/save"], produces = [TEXT_HTML])
@ResponseBody
fun save(
@Valid authorForm: AuthorForm,
bindingResult: BindingResult,
httpServletResponse: HttpServletResponse
): String {
return if (!bindingResult.hasErrors()) {
authorService.save(authorForm)
httpServletResponse.sendRedirect("/authors")
""
} else {
scriptExecutor.executeScript(
"$SCRIPT_LOCATION/addauthor.html.kts",
mapOf(ERRORS to bindingResult.allErrors.toFieldErrorsMap())
)
}
}
@PostMapping(value = ["/{id}/delete"])
fun delete(@PathVariable id: Int, httpServletResponse: HttpServletResponse) {
authorService.delete(id)
httpServletResponse.sendRedirect("/authors")
}
}
Note that SCRIPT_LOCATION
is “com/severett/thymeleafcomparison/kotlinscripting/scripting
” and is used as a common prefix for all script paths in the application.
Analysis
As a result, we now have a web application that combines the language features of Kotlin and the ability for quick reloading of web page-generating code as is available for templating engines like Thymeleaf. Mission accomplished, right? Unfortunately, no.
- Configuring the scripting host requires a lot of dependencies, far more than either the Thymeleaf or kotlinx.html applications. Running the
bootJar
Gradle task for the Kotlin Scripting application produces a JAR file that is 87.24 megabytes in size – quite larger than either the Thymeleaf (26.94 megabytes) or kotlinx.html (26.26 megabytes) applications. - Moreover, this approach re-introduces one of the drawbacks of using a templating engine compared to kotlinx.html: the website code has gone back to being split between two different locations, and having to track between the two will increase the cognitive load of the developer, especially given the incomplete support that Kotlin Scripting enjoys in IDEs compared to more mature technologies like Thymeleaf.
- Finally, the execution time is quite bad compared to the two approaches from the previous article:
As in, over a second to load one relatively small webpage – up to 26 times slower than either the Thymeleaf- or kotlinx.html-based applications! Any potential gains of hot reloading are going to be eaten up almost immediately by this approach.
Conclusion
In the end, this is an interesting exercise in exploring the capabilities of Kotlin Scripting and how it could be integrated into a Spring Boot web application, but the current technical limitations and relative lack of documentation do not make it an attractive option for web development, at least in the Spring Boot ecosystem at this point. Still, knowledge of more tools available is always a good thing: even if this use case ultimately never proves to be viable for Kotlin Scripting, it’s possible that one may come across a different scenario in the future where it will indeed come in handy. Furthermore, the authors of Kotlin have a strong incentive to invest in improving the performance of the library, as any speed-ups in the code execution will translate to the Kotlin DSL for Gradle also becoming a more attractive tool for developers. As with most (relatively) new and experimental technology, time will tell whether Kotlin Scripting becomes a better option in the future; as for now.
Published at DZone with permission of Severn Everett. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments