Neste tutorial completo, você vai aprender a criar uma aplicação Spring Boot com Kotlin do zero. Vamos cobrir a configuração do projeto, REST controllers, services, repositórios com Spring Data JPA, funcionalidades específicas do Kotlin no Spring, integração com Coroutines e configuração avançada. Kotlin é uma linguagem oficialmente suportada pelo Spring e oferece uma experiência de desenvolvimento mais concisa e segura que o Java.
Por que usar Kotlin com Spring Boot?
O Spring Framework oferece suporte oficial ao Kotlin desde a versão 5.0, e o Spring Boot desde a versão 2.0. As vantagens incluem:
- Null safety: o sistema de tipos do Kotlin com nullable elimina NullPointerExceptions em tempo de compilação
- Data classes: perfeitas para DTOs e entidades, eliminando getters, setters, equals, hashCode e toString
- Extension functions: permitem estender classes do Spring de forma elegante
- Coroutines: suporte nativo para programação assíncrona não-bloqueante
- Código conciso: menos boilerplate que Java, mantendo a mesma funcionalidade
Passo 1: Criando o Projeto
A maneira mais fácil de criar um projeto Spring Boot com Kotlin é usando o Spring Initializr. Selecione:
- Language: Kotlin
- Build: Gradle - Kotlin
- Dependencies: Spring Web, Spring Data JPA, H2 Database (ou PostgreSQL), Spring Validation
O build.gradle.kts deve incluir os plugins e dependências essenciais:
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "3.2.3"
id("io.spring.dependency-management") version "1.1.4"
kotlin("jvm") version "1.9.22"
kotlin("plugin.spring") version "1.9.22" // abre classes para proxying
kotlin("plugin.jpa") version "1.9.22" // gera construtor sem args para JPA
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
// Coroutines (opcional)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
runtimeOnly("com.h2database:h2") // banco em memória para desenvolvimento
// runtimeOnly("org.postgresql:postgresql") // para produção
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict" // null safety para anotações Java
jvmTarget = "17"
}
}
Os plugins plugin.spring e plugin.jpa são essenciais: o primeiro adiciona o modificador open automaticamente às classes com anotações Spring (já que o Kotlin cria classes final por padrão), e o segundo gera construtores sem argumentos necessários pelo JPA.
Passo 2: Configurando a Aplicação
O ponto de entrada da aplicação:
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class Application
fun main(args: Array<String>) {
runApplication<Application>(*args)
}
Note que usamos runApplication — uma extension function do Spring para Kotlin que simplifica a inicialização. O application.yml (ou application.properties) configura o ambiente:
// application.yml
// spring:
// datasource:
// url: jdbc:h2:mem:testdb
// driver-class-name: org.h2.Driver
// jpa:
// hibernate:
// ddl-auto: update
// show-sql: true
// h2:
// console:
// enabled: true
// server:
// port: 8080
Passo 3: Criando a Entity com JPA
Com Kotlin, usamos data classes para entidades, mas com algumas considerações especiais para o JPA:
import jakarta.persistence.*
import java.time.LocalDateTime
@Entity
@Table(name = "produtos")
data class Produto(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
@Column(nullable = false, length = 100)
val nome: String,
@Column(length = 500)
val descricao: String = "",
@Column(nullable = false)
val preco: Double,
@Column(name = "em_estoque")
val emEstoque: Boolean = true,
@Column(name = "criado_em")
val criadoEm: LocalDateTime = LocalDateTime.now()
)
O plugin kotlin-jpa gera o construtor sem argumentos que o Hibernate precisa. Os valores padrão nos parâmetros permitem criar instâncias parciais sem precisar de builders.
Para DTOs de entrada e saída, usamos data classes simples com validação:
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Positive
import jakarta.validation.constraints.Size
data class ProdutoRequest(
@field:NotBlank(message = "Nome é obrigatório")
@field:Size(min = 2, max = 100, message = "Nome deve ter entre 2 e 100 caracteres")
val nome: String,
val descricao: String = "",
@field:Positive(message = "Preço deve ser positivo")
val preco: Double
)
data class ProdutoResponse(
val id: Long,
val nome: String,
val descricao: String,
val preco: Double,
val emEstoque: Boolean,
val criadoEm: LocalDateTime
) {
companion object {
fun fromEntity(produto: Produto) = ProdutoResponse(
id = produto.id,
nome = produto.nome,
descricao = produto.descricao,
preco = produto.preco,
emEstoque = produto.emEstoque,
criadoEm = produto.criadoEm
)
}
}
Note o uso de @field: antes das anotações de validação — isso é necessário em Kotlin para que as anotações sejam aplicadas ao campo Java subjacente e não ao parâmetro do construtor.
Passo 4: Criando o Repository com Spring Data JPA
O Spring Data JPA gera a implementação do repositório automaticamente a partir da interface:
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository
@Repository
interface ProdutoRepository : JpaRepository<Produto, Long> {
// Query derivada do nome do método
fun findByNomeContainingIgnoreCase(nome: String): List<Produto>
fun findByEmEstoque(emEstoque: Boolean): List<Produto>
fun findByPrecoLessThanEqual(precoMaximo: Double): List<Produto>
// Query JPQL personalizada
@Query("SELECT p FROM Produto p WHERE p.preco BETWEEN :min AND :max ORDER BY p.preco")
fun findByFaixaDePreco(min: Double, max: Double): List<Produto>
// Query nativa
@Query(
value = "SELECT * FROM produtos WHERE em_estoque = true ORDER BY criado_em DESC LIMIT :limite",
nativeQuery = true
)
fun findRecentes(limite: Int): List<Produto>
fun countByEmEstoque(emEstoque: Boolean): Long
}
O Spring Data interpreta o nome do método e gera a query SQL automaticamente. Para consultas mais complexas, use @Query com JPQL ou SQL nativo.
Passo 5: Criando o Service
A camada de serviço contém a lógica de negócios:
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class ProdutoService(
private val repository: ProdutoRepository
) {
fun listarTodos(): List<ProdutoResponse> {
return repository.findAll().map { ProdutoResponse.fromEntity(it) }
}
fun buscarPorId(id: Long): ProdutoResponse {
val produto = repository.findById(id)
.orElseThrow { ProdutoNaoEncontradoException(id) }
return ProdutoResponse.fromEntity(produto)
}
fun buscarPorNome(nome: String): List<ProdutoResponse> {
return repository.findByNomeContainingIgnoreCase(nome)
.map { ProdutoResponse.fromEntity(it) }
}
@Transactional
fun criar(request: ProdutoRequest): ProdutoResponse {
val produto = Produto(
nome = request.nome,
descricao = request.descricao,
preco = request.preco
)
val salvo = repository.save(produto)
return ProdutoResponse.fromEntity(salvo)
}
@Transactional
fun atualizar(id: Long, request: ProdutoRequest): ProdutoResponse {
val existente = repository.findById(id)
.orElseThrow { ProdutoNaoEncontradoException(id) }
val atualizado = existente.copy(
nome = request.nome,
descricao = request.descricao,
preco = request.preco
)
val salvo = repository.save(atualizado)
return ProdutoResponse.fromEntity(salvo)
}
@Transactional
fun deletar(id: Long) {
if (!repository.existsById(id)) {
throw ProdutoNaoEncontradoException(id)
}
repository.deleteById(id)
}
}
class ProdutoNaoEncontradoException(id: Long) :
RuntimeException("Produto com ID $id não encontrado")
Note como o Kotlin torna o código mais limpo: injeção de dependência pelo construtor, copy() para atualização parcial, e string templates para mensagens de erro.
Passo 6: Criando o REST Controller
O controller expõe os endpoints da API:
import jakarta.validation.Valid
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/produtos")
class ProdutoController(
private val service: ProdutoService
) {
@GetMapping
fun listarTodos(): ResponseEntity<List<ProdutoResponse>> {
return ResponseEntity.ok(service.listarTodos())
}
@GetMapping("/{id}")
fun buscarPorId(@PathVariable id: Long): ResponseEntity<ProdutoResponse> {
return ResponseEntity.ok(service.buscarPorId(id))
}
@GetMapping("/buscar")
fun buscarPorNome(@RequestParam nome: String): ResponseEntity<List<ProdutoResponse>> {
return ResponseEntity.ok(service.buscarPorNome(nome))
}
@PostMapping
fun criar(@Valid @RequestBody request: ProdutoRequest): ResponseEntity<ProdutoResponse> {
val produto = service.criar(request)
return ResponseEntity.status(HttpStatus.CREATED).body(produto)
}
@PutMapping("/{id}")
fun atualizar(
@PathVariable id: Long,
@Valid @RequestBody request: ProdutoRequest
): ResponseEntity<ProdutoResponse> {
return ResponseEntity.ok(service.atualizar(id, request))
}
@DeleteMapping("/{id}")
fun deletar(@PathVariable id: Long): ResponseEntity<Unit> {
service.deletar(id)
return ResponseEntity.noContent().build()
}
}
Passo 7: Tratamento Global de Erros
Use @ControllerAdvice para tratar exceções de forma centralizada:
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
data class ErroResponse(
val status: Int,
val mensagem: String,
val erros: List<String> = emptyList()
)
@ControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(ProdutoNaoEncontradoException::class)
fun handleNaoEncontrado(ex: ProdutoNaoEncontradoException): ResponseEntity<ErroResponse> {
val erro = ErroResponse(
status = HttpStatus.NOT_FOUND.value(),
mensagem = ex.message ?: "Recurso não encontrado"
)
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(erro)
}
@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleValidacao(ex: MethodArgumentNotValidException): ResponseEntity<ErroResponse> {
val erros = ex.bindingResult.fieldErrors.map { "${it.field}: ${it.defaultMessage}" }
val erro = ErroResponse(
status = HttpStatus.BAD_REQUEST.value(),
mensagem = "Erro de validação",
erros = erros
)
return ResponseEntity.badRequest().body(erro)
}
@ExceptionHandler(Exception::class)
fun handleGenerico(ex: Exception): ResponseEntity<ErroResponse> {
val erro = ErroResponse(
status = HttpStatus.INTERNAL_SERVER_ERROR.value(),
mensagem = "Erro interno do servidor"
)
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(erro)
}
}
Passo 8: Coroutines no Spring
O Spring Boot suporta Coroutines nativamente com o módulo WebFlux. Você pode usar funções suspend e Flow diretamente nos controllers:
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/async")
class AsyncController(
private val service: AsyncService
) {
// Endpoint com suspend function
@GetMapping("/produto/{id}")
suspend fun buscarProduto(@PathVariable id: Long): ProdutoResponse {
return service.buscarProdutoAsync(id)
}
// Endpoint que retorna Flow (streaming)
@GetMapping("/stream")
fun streamProdutos(): Flow<ProdutoResponse> = flow {
val produtos = service.listarTodosAsync()
produtos.forEach { produto ->
delay(100) // simula processamento
emit(produto)
}
}
}
@Service
class AsyncService(
private val repository: ProdutoRepository
) {
suspend fun buscarProdutoAsync(id: Long): ProdutoResponse {
// Em aplicações reais, use R2DBC para acesso não-bloqueante ao banco
val produto = repository.findById(id)
.orElseThrow { ProdutoNaoEncontradoException(id) }
return ProdutoResponse.fromEntity(produto)
}
suspend fun listarTodosAsync(): List<ProdutoResponse> {
return repository.findAll().map { ProdutoResponse.fromEntity(it) }
}
}
Para aproveitar ao máximo as coroutines no Spring, considere usar Spring WebFlux com R2DBC em vez do Spring MVC com JPA, pois o JPA tradicional é bloqueante por natureza.
Passo 9: Funcionalidades Kotlin-Specific no Spring
O Spring oferece várias extensões para Kotlin que tornam o código mais idiomático:
import org.springframework.beans.factory.getBean
import org.springframework.context.support.beans
// Bean definition DSL — alternativa a @Configuration
val beans = beans {
bean<ProdutoService>()
bean {
ProdutoController(ref())
}
}
// Router DSL — alternativa funcional aos controllers
import org.springframework.web.servlet.function.router
fun produtoRoutes(service: ProdutoService) = router {
"/api/produtos".nest {
GET("") { _ ->
ok().body(service.listarTodos())
}
GET("/{id}") { request ->
val id = request.pathVariable("id").toLong()
ok().body(service.buscarPorId(id))
}
POST("") { request ->
val body = request.body(ProdutoRequest::class.java)
status(HttpStatus.CREATED).body(service.criar(body))
}
}
}
Essas DSLs são opcionais e oferecem uma abordagem funcional como alternativa às anotações tradicionais.
Erros Comuns
Esquecer o plugin
kotlin-spring: Sem esse plugin, as classes Kotlin sãofinalpor padrão, e o Spring não consegue criar proxies para injeção de dependência e transações. O plugin adicionaopenautomaticamente.Não usar
@field:nas anotações de validação: Em Kotlin, anotações em propriedades do construtor primário precisam do target@field:para que as validações do Bean Validation funcionem corretamente.Usar
varnas Entities sem necessidade: Prefira val e usecopy()para criar versões modificadas. Entidades mutáveis são fonte de bugs difíceis de rastrear.Ignorar o
-Xjsr305=strict: Essa flag do compilador faz com que as anotações de nulidade do Java (como@Nullablee@NonNulldo Spring) sejam respeitadas pelo tipo do Kotlin, melhorando a segurança de tipos.Misturar JPA bloqueante com coroutines sem cuidado: Usar
suspendno controller com JPA tradicional não torna o acesso ao banco não-bloqueante. Para I/O verdadeiramente assíncrono, use R2DBC.
Conclusão e Próximos Passos
Neste tutorial, você aprendeu a criar uma aplicação Spring Boot completa com Kotlin: configuração do projeto com plugins essenciais, entidades JPA com data classes, repositórios com Spring Data JPA, services com lógica de negócios, REST controllers com validação, tratamento global de erros, coroutines no Spring, e funcionalidades específicas do Kotlin como Router DSL e Bean DSL.
Como próximos passos, recomendamos:
- Explorar Spring Security com Kotlin para autenticação e autorização
- Estudar R2DBC para acesso não-bloqueante ao banco de dados com coroutines
- Aprender sobre testes com MockK (alternativa Kotlin-friendly ao Mockito)
- Implementar documentação da API com SpringDoc/Swagger
- Consultar o glossário de extension function e interface para reforçar conceitos
- Explorar o Kotlin Flow para streaming de dados no backend
Spring Boot com Kotlin é uma combinação poderosa para desenvolvimento backend, oferecendo produtividade, segurança de tipos e todo o ecossistema maduro do Spring Framework.