De regreso al futuro: Páginas web del lado del servidor con Kotlin (Pt. 2)

Compartir esta publicación

Recap: Páginas web del lado del servidor con Kotlin

En el primer artículo, páginas web del lado del servidor con Kotlin parte 1, se esbozó una breve historia del desarrollo web: a saber, las cuatro etapas principales son la entrega de páginas HTML estáticas; la creación programática de páginas web del lado del servidor; los motores de plantillas HTML, de nuevo del lado del servidor; y, por último, la creación programática de páginas web del lado del cliente. Aunque el desarrollo web contemporáneo se centra sobre todo en la última de las cuatro etapas (es decir, la creación de páginas web en el lado del cliente), siguen existiendo buenos casos para renderizar páginas web en el lado del servidor de la aplicación web; además, nuevas tecnologías como kotlinx.html -una biblioteca de los autores de Kotlin para generar código HTML mediante un lenguaje específico del dominio (DSL)- ofrecen opciones adicionales para el desarrollo web en el lado del servidor. Por poner un ejemplo, los dos enfoques siguientes producen la misma página de inicio para el sitio web impulsado por Spring Boot de una librería hipotética:

Motor de plantillas (Thymeleaf)

El flujo de trabajo básico para renderizar una página web con un motor de plantillas como Thymeleaf es crear una página de plantilla HTML en la carpeta resources/templatesdel proyecto, en este caso 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>

A continuación, es necesario crear una función de punto final de controlador web y devolver un valor de cadena que corresponde al nombre del archivo de plantilla en la carpeta de recursos (sólo que sin la extensión de archivo .html):

@Controller
class HomeController {
   @GetMapping(value= ["/", "/home"])
   fun home() = "home"
}

kotlinx.html

Aunque de nuevo hay dos pasos básicos para renderizar una página web usando la librería kotlinx.html, la diferencia aquí es que el código de generación HTML puede colocarse directamente dentro de la estructura de clases de la aplicación web. Primero, crea una clase que genere el código HTML en forma de cadena:

@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()
           }
       }
   }
}

A continuación, el controlador web devolverá la cadena generada por la clase renderizadora como cuerpo de respuesta de la función de punto final web correspondiente:

@Controller
class HomeController(private val homepageRenderer: HomepageRenderer) {
   @GetMapping(value = ["/", "/home"], produces = [MediaType.TEXT_HTML_VALUE])
   @ResponseBody
   fun home() = homepageRenderer.render()
}

Comparación

Como mencionamos en el artículo anterior, este enfoque aporta algunos beneficios apreciables en comparación con el enfoque tradicional de los motores de plantillas, como tener todo el código relevante en una ubicación dentro del proyecto y aprovechar el sistema de tipado de Kotlin, así como sus otras características. Sin embargo, este enfoque también tiene desventajas, una de las cuales es que la propia incrustación del código dentro de la estructura de clases (compilada) significa que no es posible la «recarga en caliente»: cualquier cambio en el código de renderizado HTML requerirá que el servidor se reinicie, en comparación con la simple posibilidad de actualizar la página web de destino cuando se utiliza un motor de plantillas como Thymeleaf. Como examinaremos en esta segunda parte, este problema tiene una posible solución en la biblioteca Kotlin Scripting.

  Gatsby para el marketing de sitios web

Kotlin Scripting – Introducción

Como su nombre indica, Kotlin Scripting es una biblioteca que permite a un desarrollador escribir código Kotlin y hacer que se ejecute no después de haber sido compilado para la plataforma de destino, sino más bien como un script que ha sido leído e interpretado por el host de ejecución. Las ventajas potenciales son obvias: aprovechar la potencia del sistema de tipado de Kotlin y otras características al tiempo que se puede ejecutar el código Kotlin sin tener que volver a compilar los archivos después de cualquier refactorización. Aunque oficialmente la funcionalidad aún se encuentra en estado «experimental», ya cuenta con un early adopter muy destacado: El DSL Kotlin de Gradle, con el que se pueden escribir scripts de construcción Gradle en lugar de utilizar los tradicionales archivos de script basados en Groovy. Además, hay disponible una biblioteca de terceros para mejorar la experiencia de desarrollo y ejecución de archivos de script Kotlin, especialmente para aplicaciones de línea de comandos.

Pasos de adaptación

Para este ejercicio – que de nuevo se basará en el hipotético sitio web de la librería utilizado en el primer artículo – será suficiente aplicar un «host de scripting» relativamente simple que cargará scripts Kotlin y los ejecutará dentro de una aplicación web Spring Boot. Ten en cuenta que en el sitio web de Kotlin hay disponible un tutorial sobre cómo configurar una aplicación básica de ejecución de scripts -y es en este tutorial en el que se basa la configuración del código de este artículo-, pero contiene algunas partes que son innecesarias (por ejemplo, la carga dinámica de dependencias desde Maven), así como partes que faltan (por ejemplo, el paso de argumentos a un script), por lo que se proporcionará una explicación más detallada en los pasos siguientes.

Paso 1: Dependencias

En primer lugar, habrá que añadir las siguientes dependencias al proyecto:

  • org.jetbrains.kotlin:kotlin-scripting-common
  • org.jetbrains.kotlin:kotlin-scripting-jvm
  • org.jetbrains.kotlin:kotlin-scripting-jvm-host

Ten en cuenta que todas estas dependencias siguen las mismas convenciones de nomenclatura que las dependencias «principales» de Kotlin – y también se incluyen en las mismas versiones – por lo que se puede utilizar la función de ayuda kotlin() dependency-handler en Gradle (por ejemplo, implementation(implementation(kotlin(“scripting-common”))), así como omitir la versión del paquete si se utiliza el plugin Kotlin Gradle.

Paso 2: Configuración de la compilación de scripts

A continuación, es necesario definir un objeto que contenga la configuración de cómo se cargarán los scripts Kotlin:

object HtmlScriptCompilationConfiguration : ScriptCompilationConfiguration(
   {
       jvm {
           jvmTarget("19")
           dependenciesFromCurrentContext("main", "kotlinx-html-jvm-0.8.0")
       }
   }
)

Como se ha visto anteriormente, el objeto debe contener dos declaraciones de configuración:

  1. La versión de la JVM que se utilizará para compilar los scripts Kotlin: debe coincidir con la versión de la JVM que ejecuta el host del script (es decir, la aplicación web Spring Boot).
  2. Cualquier dependencia que deba pasarse al contexto que carga y ejecuta los scripts Kotlin. «main» es obligatorio para importar las librerías del núcleo de Kotlin; «kotlinx-html-jvm-0.8.0» es para el código kotlinx.html que se introdujo en el artículo anterior.

Paso 3: Definición de marcador de posición de script

Con el objeto de configuración de compilación de scripts definido, ahora podemos definir la clase abstracta que servirá como marcador de posición para los scripts que se cargarán y ejecutarán:

@KotlinScript(
   fileExtension = "html.kts",
   compilationConfiguration = HtmlScriptCompilationConfiguration::class
)
abstract class HtmlScript(@Suppress("unused") val model: Map<String, Any?>)

Como demuestra el código, es necesario pasar la extensión de archivo que identificará los archivos de script que utilizarán la configuración de compilación previamente definida. Además, el constructor de la clase abstracta sirve como punto de entrada para cualquier variable que deba pasarse al script durante la ejecución; en este caso, el parámetromodel se ha definido para que sirva de forma similar a como funciona el objeto model de nombre similar para los archivos de plantilla HTML de Thymeleaf.

  ORM: evitar el uso de many-to-many relationships

Paso 4: Ejecutor de script

Después de definir el marcador de posición del script, ahora es posible definir el código que cargará y ejecutará los 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 ?: ""
   }
}

Hay que mencionar un par de cosas:

  • Evaluar un script en Kotlin requiere dos configuraciones: una para la compilación y otra para la evaluación. La primera se definió en un paso anterior, mientras que la segunda debe generarse para cada ejecución de script, ya que es aquí donde se pasan los argumentos al script a través de la llamada a la función constructorArgs()(en nuestro caso, al parámetro model definido en el paso anterior).
  • El host de ejecución de scripts no lanzará ninguna ejecución encontrada al ejecutar un script (por ejemplo, errores de sintaxis). En su lugar, agregará todos los «informes» recopilados durante la evaluación del script y los devolverá en un parámetro denominado como tal en el objeto de response. Por lo tanto, es necesario crear un mecanismo de informes (en este caso, el objeto logger) después del hecho para informar al desarrollador y/o al usuario si se ha producido una excepción.
  • No existe una definición de tipo para lo que debe devolver una ejecución de script «correcta»; por lo tanto, el valor devuelto debe convertirse al tipo apropiado antes de ser devuelto por la función.

Paso 5: Definición del script

Ahora podemos definir el script de renderizado HTML. Siguiendo con los ejemplos del artículo anterior, crearemos el script que renderiza la página «ver todos los autores» del sitio web:

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()
   }
}

Los puntos a tener en cuenta:

  • El soporte de IDE para scripts Kotlin es limitado, y actualmente (por ejemplo con IDEA 2022.3.2) no reconoce los argumentos pasados al script como el objeto model. Como consecuencia, el IDE probablemente marcará el archivo como erróneo, aunque este no sea realmente el caso.
  • Se recomienda simular la estructura de paquetes dentro de la cual deben colocarse los scripts. En este caso, el archivo anterior se encuentra en resources/com/severett/thymeleafcomparison/kotlinscripting/scripting y, por tanto, está marcado como perteneciente al paquete com.severett.thymeleafcomparison.kotlinscripting.scripting. Esto le permite acceder a las funciones del archivo common.kt, como las funciones header() y footer()que generan las secciones de encabezado y pie de página del código HTML, respectivamente.

Paso 6: Web Controller

El paso final es crear el controlador web que dicta qué scripts deben ejecutarse para generar el código HTML para las peticiones web. El resultado final es similar al enfoque para kotlinx.html – es decir, devolver un cuerpo de respuesta de text/html – con la diferencia de qué mecanismo se llama realmente para generar el cuerpo de respuesta, en este caso, el ejecutor de script definido anteriormente:

@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")
   }
}

Ten en cuenta que SCRIPT_LOCATION es «com/severett/thymeleafcomparison/kotlinscripting/scripting» y se utiliza como prefijo común para todas las rutas de script de la aplicación.

  Bunkerización digital en tiempos de pandemia: Conceptos básicos

Análisis

Como resultado, ahora tenemos una aplicación web que combina las características de lenguaje de Kotlin y la capacidad de recarga rápida de código generador de páginas web como está disponible para los motores de plantillas como Thymeleaf. Misión cumplida, ¿verdad? Desgraciadamente, no.

  1. La configuración del host de scripting requiere muchas dependencias, muchas más que las aplicaciones Thymeleaf o kotlinx.html. La ejecución de la tarea Gradle bootJar para la aplicación Kotlin Scripting produce un archivo JAR de 87,24 megabytes de tamaño, bastante más grande que las aplicaciones Thymeleaf (26,94 megabytes) o kotlinx.html (26,26 megabytes).
  2. Además, este enfoque reintroduce uno de los inconvenientes de utilizar un motor de plantillas en comparación con kotlinx.html: el código del sitio web ha vuelto a dividirse en dos ubicaciones diferentes y tener que rastrear entre ambas aumentará la carga cognitiva del desarrollador, especialmente dado el soporte incompleto del que goza Kotlin Scripting en los IDE en comparación con tecnologías más maduras como Thymeleaf.
  3. Por último, el tiempo de ejecución es bastante malo en comparación con los dos enfoques del artículo anterior:
Kotlin Scripting Result

Más de un segundo para cargar una página web relativamente pequeña: ¡hasta 26 veces más lento que las aplicaciones basadas en Thymeleaf o kotlinx.html! Cualquier ganancia potencial de la recarga en caliente va a ser devorada casi inmediatamente por este enfoque.

Conclusión

Al final, se trata de un ejercicio interesante para explorar las capacidades de Kotlin Scripting y cómo podría integrarse en una aplicación web Spring Boot, pero las limitaciones técnicas actuales y la relativa falta de documentación no lo convierten en una opción atractiva para el desarrollo web, al menos en el ecosistema Spring Boot en este momento. Aún así, saber que hay más herramientas disponibles siempre es bueno: aunque este caso de uso finalmente nunca resulte viable para Kotlin Scripting, es posible que uno se encuentre con un escenario diferente en el futuro en el que sí resulte útil. Además, los autores de Kotlin tienen un fuerte incentivo para invertir en mejorar el rendimiento de la librería, ya que cualquier aumento de velocidad en la ejecución del código se traducirá en que el DSL de Kotlin para Gradle también se convertirá en una herramienta más atractiva para los desarrolladores. Como ocurre con la mayoría de las tecnologías (relativamente) nuevas y experimentales, el tiempo dirá si Kotlin Scripting se convierte en una mejor opción en el futuro; por ahora.

Author

  • SevernEverett

    Software engineer with more than 10 years of experience working with different technologies such as Java, Docker, Kubernetes, React, CDK, Kotlin.... With high ability to deal with different environments and technologies.

    Ver todas las entradas

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Suscríbete a nuestro boletín de noticias

Recibe actualizaciones de los últimos descubrimientos tecnológicos

¿Tienes un proyecto desafiante?

Podemos trabajar juntos

apiumhub software development projects barcelona
Secured By miniOrange