Spring Boot con Java funciona bien. Lleva años funcionando bien. El problema no es que Java sea malo, sino que Kotlin hace lo mismo con bastante menos ruido. Menos líneas, menos posibilidades de meter la pata y el compilador ayudando donde Java solo te avisa en tiempo de ejecución.
El primer beneficio que nota cualquiera que viene de Java es la null safety. Con Kotlin, el tipo del campo determina si puede ser nulo o no. Si un campo es String, no puede ser null. Si puede serlo, tienes que declararlo String? y el compilador te obliga a gestionarlo. Esto elimina por completo los NullPointerExceptions en la deserialización de JSON, que son uno de los fallos más frecuentes en APIs REST mal tipadas.
Luego están los data classes. En Java, un DTO sencillo con tres campos necesita getters, setters, equals(), hashCode() y toString(). Con Lombok se reduce algo, pero sigues dependiendo de una herramienta externa y de anotaciones que en algunos entornos generan fricciones. En Kotlin:
data class UserDto(val id: Long, val name: String, val email: String)
Eso es todo. El compilador genera el resto. Y como DTO o como entidad JPA, funciona igual de bien.
Por último, Kotlin permite escribir extension functions, que el DSL de Spring aprovecha para que los controladores REST y la configuración de seguridad sean mucho más legibles. Menos anotaciones en cadena, menos verbosidad, mismo resultado.
Configurar un proyecto Spring Boot con Kotlin
El punto de partida es start.spring.io. Al crear el proyecto, selecciona Kotlin como lenguaje y Gradle - Kotlin como sistema de build. Puedes usar Maven si prefieres, pero el DSL de Kotlin para Gradle (build.gradle.kts) encaja mejor con el ecosistema y te da autocompletado en el IDE sin trucos.
Las dependencias básicas para una API REST con base de datos son estas:
- Spring Web: para los controladores REST
- Spring Data JPA: para el acceso a base de datos con Hibernate
- Spring Security: si necesitas autenticación (opcional en proyectos internos)
- H2 Database o el driver de tu base de datos
En el build.gradle.kts tienes que añadir dos plugins de Kotlin que son imprescindibles:
plugins {
kotlin("jvm") version "2.0.0"
kotlin("plugin.spring") version "2.0.0"
kotlin("plugin.jpa") version "2.0.0"
id("org.springframework.boot") version "3.3.0"
id("io.spring.dependency-management") version "1.1.5"
}
plugin.spring abre las clases de Kotlin automáticamente. Spring necesita subclasificar tus beans para aplicar proxies AOP, y Kotlin cierra las clases por defecto. Sin este plugin tendrías que poner open en cada clase manualmente. plugin.jpa genera los constructores sin argumentos que Hibernate necesita para instanciar las entidades.
Controladores REST con Kotlin
La diferencia más visible respecto a Java está en la inyección de dependencias. En Kotlin lo idiomatic es inyectar por constructor, sin @Autowired en campos:
@RestController
@RequestMapping("/api/users")
class UserController(private val service: UserService) {
@GetMapping("/{id}")
fun getUser(@PathVariable id: Long): UserDto = service.findById(id)
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun createUser(@RequestBody @Validated body: CreateUserRequest): UserDto =
service.create(body)
}
El body de la petición se valida con @Validated y un data class con las anotaciones de Bean Validation:
data class CreateUserRequest(
val name: String,
@field:Email val email: String,
@field:Size(min = 8) val password: String
)
Ojo con el prefijo @field:. En Kotlin, cuando anotas una propiedad de un data class, la anotación puede aplicarse al constructor, al campo o al getter. Para que Bean Validation funcione necesitas que se aplique al campo, de ahí el @field:.
Data classes como entidades JPA
Usar data classes como entidades JPA es tentador y funciona, con algunos matices que conviene conocer antes de que te den problemas en producción.
@Entity
@Table(name = "users")
data class User(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
var name: String,
var email: String
)
Los campos que JPA puede modificar tienen que ser var, no val. Hibernate necesita poder escribir en ellos al cargar la entidad desde la base de datos. El id puede ser val con valor por defecto 0, porque solo se asigna una vez al persistir.
El constructor sin argumentos que Hibernate necesita lo genera kotlin("plugin.jpa") automáticamente. Sin ese plugin tendrías que añadirlo a mano o la sesión de Hibernate fallaría al intentar instanciar la entidad.
Una advertencia sobre equals() y hashCode() en entidades JPA: los que genera el data class comparan todos los campos, lo que puede dar resultados inesperados con entidades en distintos estados del ciclo de vida de Hibernate. Para entidades con un ID numérico, suele ser mejor sobrescribir esos métodos basándote solo en el ID.
Coroutines con Spring WebFlux
Si el proyecto necesita alta concurrencia, Spring WebFlux es el stack reactivo de Spring. La buena noticia es que Kotlin y Spring lo hacen casi transparente gracias a las coroutines.
@RestController
@RequestMapping("/api/users")
class UserController(private val service: UserService) {
@GetMapping
suspend fun getUsers(): List = service.findAll()
@GetMapping("/{id}")
suspend fun getUser(@PathVariable id: Long): UserDto? = service.findById(id)
}
Con suspend en el método, Spring convierte automáticamente el resultado a un Mono o Flux de Project Reactor. No tienes que aprender la API de Reactor si ya sabes usar coroutines, que es mucho más intuitiva.
Para el acceso a base de datos en el stack reactivo, Spring Data R2DBC es la opción correcta. JPA con Hibernate no es compatible con programación reactiva porque Hibernate bloquea el hilo. Con R2DBC:
interface UserRepository : CoroutineCrudRepository {
suspend fun findByEmail(email: String): User?
}
CoroutineCrudRepository expone métodos suspend directamente, sin necesidad de convertir entre coroutines y Mono/Flux en cada llamada.
Spring Security con Kotlin
La configuración de Spring Security en Java siempre ha sido un poco larga. Con el DSL de Kotlin se acorta bastante y se lee de un tirón:
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize("/api/public/**", permitAll)
authorize(anyRequest, authenticated)
}
httpBasic { }
csrf { disable() }
}
return http.build()
}
}
El DSL de Kotlin para Spring Security está disponible desde Spring Security 5.4. Si por algún motivo no lo puedes usar, la API fluida de Java funciona exactamente igual en Kotlin, es solo algo más verbosa. No hay diferencia funcional.
Para JWT o OAuth2, la configuración es la misma que en Java. Kotlin no cambia nada en ese sentido, solo la forma de escribirlo.
Testing con Spring Boot y Kotlin
El testing es donde más se nota la comodidad de Kotlin. Los tests de integración con MockMvc tienen un DSL propio:
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest(@Autowired val mockMvc: MockMvc) {
@Test
fun `GET api users devuelve lista de usuarios`() {
mockMvc.get("/api/users") {
contentType = MediaType.APPLICATION_JSON
accept = MediaType.APPLICATION_JSON
}.andExpect {
status { isOk() }
content { contentType(MediaType.APPLICATION_JSON) }
jsonPath("$[0].name") { exists() }
}
}
}
Los nombres de los tests entre backticks son una de las cosas que más gustan a quien viene de Java. Puedes poner una frase descriptiva completa en lugar de un nombre de método largo y poco legible.
@SpringBootTest levanta el contexto completo, lo que es más lento. Para tests que solo prueban la capa JPA, @DataJpaTest es mucho más rápido porque solo carga lo necesario para Hibernate.
Si prefieres una alternativa a JUnit 5 con un estilo más idiomático en Kotlin, Kotest merece un vistazo. Permite escribir tests con un DSL tipo BDD muy limpio y tiene integración nativa con Spring Boot.
Despliegue y compilación nativa con Spring Boot 3.x
Spring Boot 3.x trajo mejoras importantes en la compilación AOT (Ahead-of-Time) y el soporte para GraalVM Native Image. El resultado es un binario que arranca en milisegundos en lugar de segundos, sin JVM.
Para compilar a nativo con Gradle:
./gradlew nativeCompile
La compilación tarda bastante más que una compilación normal, pero el binario resultante arranca en menos de 100ms y consume mucho menos memoria. Ideal para contenedores y entornos serverless.
El soporte de Kotlin en GraalVM Native Image mejoró mucho en Spring Boot 3.2. Las versiones anteriores tenían problemas con las coroutines y la reflexión, que ahora están resueltos en su mayor parte.
Para generar una imagen Docker sin escribir un Dockerfile:
./gradlew bootBuildImage
Spring Boot usa Cloud Native Buildpacks por debajo y genera una imagen OCI lista para usar en cualquier registro de contenedores. Sin configuración extra, detecta que el proyecto es Kotlin y ajusta la imagen en consecuencia.
Si quieres profundizar más en las ventajas de Kotlin en el servidor, échale un ojo a Kotlin en el backend: las ventajas sobre Java y a Kotlin más allá de Android: el mundo del servidor.
Imagen: Pexels / Brett Sayles
