Trilha de Kotlin · parte 37 de 59
Injeção de dependência e IoC no Spring
Como o Spring monta os objetos de um serviço por injeção no construtor, com beans, @Primary, @Qualifier e dependência circular, em saídas reais.
intermediárioPublicada em · 11 min de leitura
Pense numa cozinha de restaurante. O cozinheiro não vai ao mercado a cada prato, porque os ingredientes chegam na bancada. Uma classe que cria sozinha as próprias dependências faz o papel do cozinheiro que sai para comprar. A é a bancada: a classe declara o que precisa, e alguém de fora entrega. A analogia tem limite: a bancada não escolhe o cardápio, e o Spring não escreve a regra de negócio. Ele só cria os objetos e os entrega a quem os pede.
o mapa da aula
A aula vai da classe que cria a própria dependência até o do Spring, passando por beans, candidatos, e o plugin kotlin-spring. Fecham o teste, os erros e os exercícios. O domínio é um só: ao finalizar um pedido, o cliente recebe uma confirmação por um notificador.
Versões: Spring Boot 4.1.2, Kotlin 2.4.21 e Gradle 9.7.0. A divergência com as outras aulas de Spring está no fim.
1. o problema: a classe que cria o que usa
É o ponto de partida mais comum sem injeção.
class EmailNotificador {
fun enviar(mensagem: String) {
println("e-mail enviado: $mensagem")
}
}
class PedidoService {
private val notificador = EmailNotificador()
fun finalizar(id: Int) {
println("pedido $id finalizado")
notificador.enviar("pedido $id confirmado")
}
}
fun main() {
PedidoService().finalizar(7)
}
pedido 7 finalizado
e-mail enviado: pedido 7 confirmado
Cada finalizar imprime uma linha simulando o envio, e trocar o canal exige editar a classe. A dependência fica escondida na linha private val notificador = EmailNotificador(). Injetar é receber a dependência de fora, e a criação sai para o main:
interface Notificador {
fun enviar(mensagem: String)
}
class EmailNotificador : Notificador {
override fun enviar(mensagem: String) {
println("e-mail enviado: $mensagem")
}
}
class PedidoService(private val notificador: Notificador) {
fun finalizar(id: Int) {
println("pedido $id finalizado")
notificador.enviar("pedido $id confirmado")
}
}
fun main() {
val servico = PedidoService(EmailNotificador())
servico.finalizar(7)
}
pedido 7 finalizado
e-mail enviado: pedido 7 confirmado
A saída não muda. Muda quem escolhe a implementação. O main virou a raiz de composição, nome que a aula sobre Arquitetura hexagonal com ArchUnit também usa.
2. e o contêiner
Na inversão de controle, o framework cria os objetos e chama o seu código. No Spring, esse papel é do ApplicationContext, e cada objeto que ele gerencia é um .
À esquerda, o new sai da própria classe. À direita, o contêiner cria e entrega.
3. declarar beans com @Component, @Service e @Repository
@Component marca qualquer classe como bean. @Service, @Repository e @Controller são especializações dela para as camadas de serviço, persistência e apresentação. Para objetos que você não pode anotar, veja a seção 5.
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.stereotype.Component
import org.springframework.stereotype.Repository
import org.springframework.stereotype.Service
interface Notificador {
fun enviar(mensagem: String)
}
@Component
class EmailNotificador : Notificador {
override fun enviar(mensagem: String) {
println("e-mail enviado: $mensagem")
}
}
@Repository
class PedidoRepositorio {
fun salvar(id: Int) {
println("pedido $id salvo")
}
}
@Service
class PedidoService(
private val repositorio: PedidoRepositorio,
private val notificador: Notificador,
) {
fun finalizar(id: Int) {
println("pedido $id finalizado")
repositorio.salvar(id)
notificador.enviar("pedido $id confirmado")
}
}
@SpringBootApplication
class App
fun main(args: Array<String>) {
val contexto = runApplication<App>(*args)
contexto.getBean(PedidoService::class.java).finalizar(7)
contexto.close()
}
pedido 7 finalizado
pedido 7 salvo
e-mail enviado: pedido 7 confirmado
Cada private val do vira propriedade e recebe o argumento. Não há @Autowired, porque a documentação diz que a anotação não é necessária quando há um só construtor. getBean pede um bean pelo tipo.
4. injeção por construtor
A documentação do Spring recomenda o construtor para dependências obrigatórias. Ele permite objetos imutáveis e garante que a dependência não é nula. Injetar por campo com lateinit adia o erro para o uso, como mostra a aula sobre Null safety e tipos nuláveis.
5. @Configuration e @Bean para o que você não controla
Se o cliente de SMS vem de uma biblioteca, não dá para anotá-lo. Um método @Bean numa classe @Configuration declara o bean. O bloco acrescenta o canal SMS:
class ClienteSms(val remetente: String) {
fun enviar(texto: String) {
println("sms de $remetente: $texto")
}
}
@Configuration
class ConfiguracaoSms {
@Bean
fun clienteSms(): ClienteSms = ClienteSms("LOJA")
}
@Component
class SmsNotificador(private val cliente: ClienteSms) : Notificador {
override fun enviar(mensagem: String) {
cliente.enviar(mensagem)
}
}
Agora há dois Notificador, e o contêiner não escolhe. O Spring não sobe:
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 1 of constructor in aula.s2.PedidoService required a single bean, but 2 were found:
- emailNotificador: defined in URL [jar:file:[...]!/aula/s2/EmailNotificador.class]
- smsNotificador: defined in URL [jar:file:[...]!/aula/s2/SmsNotificador.class]
This may be due to missing parameter name information
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
Ensure that your compiler is configured to use the '-parameters' flag.
You may need to update both your build tool settings as well as your IDE.
(See https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-6.1-Release-Notes#parameter-name-retention)
Os nomes completos trazem o pacote aula.s2, omitido nos trechos. Caminhos e prefixo de log foram encurtados.
Pergunta de entrevistaPor que injeção pelo construtor é preferível a @Autowired no campo?
A injeção pelo construtor torna a dependência explícita e obrigatória: não dá para criar o objeto sem ela. Isso facilita testes, porque posso passar um dublê sem subir o Spring. Com @Autowired no campo, a classe depende do contêiner para existir, e o campo pode ficar nulo fora dele. Em Kotlin, o construtor primário com val resolve isso numa linha, e com um único construtor o Spring nem exige a anotação.
6. escolhendo entre candidatos
Imagine uma escala de plantão com dois funcionários no mesmo turno. @Primary é o titular, e @Qualifier é o nome escrito na escala. A analogia tem limite: a anotação só escolhe o bean. Segundo a documentação, havendo exatamente um primário, ele é injetado:
@Primary
@Component
class EmailNotificador : Notificador {
override fun enviar(mensagem: String) {
println("e-mail enviado: $mensagem")
}
}
pedido 7 finalizado
pedido 7 salvo
e-mail enviado: pedido 7 confirmado
Para o serviço pedir o SMS, @Qualifier vai no parâmetro:
@Service
class PedidoService(
private val repositorio: PedidoRepositorio,
@Qualifier("smsNotificador") private val notificador: Notificador,
) {
fun finalizar(id: Int) {
println("pedido $id finalizado")
repositorio.salvar(id)
notificador.enviar("pedido $id confirmado")
}
}
pedido 7 finalizado
pedido 7 salvo
sms de LOJA: pedido 7 confirmado
Quando nada desempata, o Spring compara o nome do parâmetro com o nome dos beans. Um parâmetro chamado smsNotificador, sem anotação, recebeu o bean SMS. Renomear o parâmetro muda o resultado sem erro de compilação, e o exercício 2 mostra isso.
A figura resume a ordem: primeiro o primário, depois o qualificador, e a falha se nada decidir.
7. escopos e ciclo de vida
O padrão é singleton, com uma instância por contêiner. Prototype cria uma nova a cada pedido. Testei os dois com dois getBean seguidos, em JDK 21 e 25. Escopos web ficam fora desta aula. Entre os callbacks de ciclo de vida, a documentação do Spring cita o @PostConstruct como boa prática; aqui ele só é citado.
8. dependência circular
Estoque precisa de Pedidos, e Pedidos precisa de Estoque. É como dois funcionários que só começam o turno depois do outro: nenhum começa. A analogia falha porque o contêiner detecta o impasse e para.
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.stereotype.Service
@Service
class Estoque(private val pedidos: Pedidos) {
fun reservar(id: Int) {
println("reservando pedido $id")
}
}
@Service
class Pedidos(private val estoque: Estoque) {
fun abrir(id: Int) {
estoque.reservar(id)
}
}
@SpringBootApplication
class App
fun main(args: Array<String>) {
val contexto = runApplication<App>(*args)
contexto.getBean(Pedidos::class.java).abrir(7)
contexto.close()
}
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| estoque defined in URL [jar:file:[...]!/aula/ciclo/Estoque.class]
↑ ↓
| pedidos defined in URL [jar:file:[...]!/aula/ciclo/Pedidos.class]
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
Com injeção por construtor, a documentação diz que esse ciclo não tem solução, e que o contêiner lança BeanCurrentlyInCreationException. Desde o Spring Boot 2.6, ciclos são proibidos por padrão. A saída cita spring.main.allow-circular-references, que não testei. Desfaça o ciclo, extraindo a parte comum para outra classe.
A seta de Pedidos é laranja, e as setas rosa mostram a dependência que volta e a falha.
9. classes abertas e o
Kotlin torna as classes final, e o proxy do Spring precisa de classe aberta. A aula sobre Interfaces, delegação e classes open explica o mecanismo. No jar do plugin 2.4.21, o preset spring lista @Component, @Async, @Transactional, @Cacheable e @SpringBootTest, e as meta-anotações de @Component, como @Service, também entram. O projeto usa o starter mínimo, e o BOM foi importado com platform, porque o plugin do Spring Boot não aplicou o gerenciamento de versões:
plugins {
kotlin("jvm") version "2.4.21"
kotlin("plugin.spring") version "2.4.21"
id("org.springframework.boot") version "4.1.2"
application
}
repositories {
mavenCentral()
}
kotlin {
jvmToolchain(21)
}
dependencies {
implementation(platform("org.springframework.boot:spring-boot-dependencies:4.1.2"))
testImplementation(platform("org.springframework.boot:spring-boot-dependencies:4.1.2"))
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.jetbrains.kotlin:kotlin-reflect")
testImplementation(kotlin("test"))
}
tasks.test {
useJUnitPlatform()
}
application {
mainClass.set("aula.s1.MainKt")
}
Sem o plugin, a classe ConfiguracaoSms, que é final, não sobe:
Configuration problem: @Configuration class 'ConfiguracaoSms' may not be final. Remove the final modifier to continue.
Offending resource: class path resource [aula/s3/ConfiguracaoSms.class]
[pilha de chamadas omitida]
Um @Service final subiu sem o plugin, num teste à parte. O erro aparece quando a anotação exige proxy.
10. testar com um dublê feito à mão
Sem Spring, o teste passa um dublê que guarda o que recebe. Não usamos MockK, que a aula de Testes com Kotest, MockK e Testcontainers apresenta.
import kotlin.test.Test
import kotlin.test.assertEquals
class NotificadorFalso : Notificador {
val mensagens = mutableListOf<String>()
override fun enviar(mensagem: String) {
mensagens.add(mensagem)
}
}
class PedidoServiceTest {
@Test
fun finalizarEnviaAConfirmacaoPeloNotificadorInjetado() {
val falso = NotificadorFalso()
val servico = PedidoService(PedidoRepositorio(), falso)
servico.finalizar(7)
assertEquals(listOf("pedido 7 confirmado"), falso.mensagens)
}
}
./gradlew test rodou um teste, sem falhas:
<testsuite name="aula.s3.PedidoServiceTest" tests="1" skipped="0" failures="0" errors="0">
[atributos time e timestamp omitidos]
11. erros comuns e boas práticas
Criar a dependência dentro da classe por comodidade impede o teste de trocá-la. Declare tudo no construtor.
Contar com o nome do parâmetro muda o bean escolhido sem erro de compilação. Use @Primary e @Qualifier.
Esquecer o plugin kotlin-spring só aparece ao subir a aplicação, e não no build.
Liberar o ciclo com allow-circular-references esconde um desenho em que duas classes misturam responsabilidades.
Injetar com lateinit deixa a dependência faltar até o uso.
12. exercícios com respostas
- Guiado. Crie um
Contadorque implementaNotificador, finalize dois pedidos e imprima o total. Antes de rodar, diga quantos e-mails saem. A resposta é nenhum:
interface Notificador {
fun enviar(mensagem: String)
}
class Contador : Notificador {
var total = 0
override fun enviar(mensagem: String) {
total++
}
}
class PedidoService(private val notificador: Notificador) {
fun finalizar(id: Int) {
println("pedido $id finalizado")
notificador.enviar("pedido $id confirmado")
}
}
fun main() {
val contador = Contador()
val servico = PedidoService(contador)
servico.finalizar(1)
servico.finalizar(2)
println("enviadas: ${contador.total}")
}
pedido 1 finalizado
pedido 2 finalizado
enviadas: 2
- Renomeie para
canalo parâmetro dePedidoServiceda seção 5, sem@Primarye sem@Qualifier. Antes de rodar, diga o que acontece. A resposta é a falha abaixo, porquecanalnão coincide com nenhum bean. Só a classe muda:
@Service
class PedidoService(
private val repositorio: PedidoRepositorio,
private val canal: Notificador,
) {
fun finalizar(id: Int) {
println("pedido $id finalizado")
repositorio.salvar(id)
canal.enviar("pedido $id confirmado")
}
}
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 1 of constructor in aula.ex2.PedidoService required a single bean, but 2 were found:
- emailNotificador: defined in URL [jar:file:[...]!/aula/ex2/EmailNotificador.class]
- smsNotificador: defined in URL [jar:file:[...]!/aula/ex2/SmsNotificador.class]
This may be due to missing parameter name information
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
Ensure that your compiler is configured to use the '-parameters' flag.
You may need to update both your build tool settings as well as your IDE.
(See https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-6.1-Release-Notes#parameter-name-retention)
13. glossário
- Contêiner: objeto que cria, guarda e entrega os beans (
ApplicationContext). - Bean: objeto gerenciado pelo contêiner.
- Injeção de dependência: receber de fora os objetos de que a classe precisa.
- Inversão de controle: o framework cria os objetos e chama o código da aplicação.
- Construtor primário: construtor no cabeçalho da classe.
@Component,@Service,@Repository: anotações que marcam beans.@Configuratione@Bean: classe que declara beans, e método que devolve um.@Primarye@Qualifier: preferência e escolha por nome entre candidatos.- Escopo: quantas instâncias um bean tem.
- Dependência circular: dois beans que precisam um do outro para existir.
- Dublê: objeto de teste que substitui uma dependência real.
- Plugin kotlin-spring: plugin que abre as classes anotadas com anotações do Spring.
o que não foi confirmado ou medido
A versão 4.1.2 é a estável na documentação de 04/11/2026. As aulas de Spring desta trilha citam 4.1.1 e Spring Framework 7.0.9, e aqui o Spring Framework resolvido foi 7.0.10. Rodei em macOS, com JDK 21 e JDK 25 via java -cp. O ./gradlew test rodou só na JVM 21, que é o toolchain do projeto. Windows e Linux não foram testados. A página do plugin all-open não lista o preset spring, e a lista veio do jar. A documentação não cobre o caso sem primário nem qualificador; a falha citada foi observada. A regra do nome do parâmetro foi observada só em JDK 21, sem a flag -parameters. Não testei allow-circular-references, escopos web nem spring-boot-starter-test. Caminhos e prefixos de log foram encurtados.
Fontes
Consultadas em 04/11/2026.
- Spring Boot, requisitos (4.1.2 como estável, Spring Framework 7.0.10): https://docs.spring.io/spring-boot/system-requirements.html
- Spring Boot, Kotlin (classes
final, plugin kotlin-spring): https://docs.spring.io/spring-boot/reference/features/kotlin.html - Spring Framework,
@Autowiredcom construtor único: https://docs.spring.io/spring-framework/reference/core/beans/annotation-config/autowired.html - Spring Framework,
@Primary: https://docs.spring.io/spring-framework/reference/core/beans/annotation-config/autowired-primary.html - Spring Framework,
@Qualifier: https://docs.spring.io/spring-framework/reference/core/beans/annotation-config/autowired-qualifiers.html - Spring Framework, escopos: https://docs.spring.io/spring-framework/reference/core/beans/factory-scopes.html
- Spring Framework, injeção por construtor e dependências circulares: https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html
- Spring Framework, estereótipos: https://docs.spring.io/spring-framework/reference/core/beans/classpath-scanning.html
- Spring Framework, callbacks de ciclo de vida: https://docs.spring.io/spring-framework/reference/core/beans/factory-nature.html
- Spring Boot 2.6, notas de versão, consultadas por resultado de busca: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.6-Release-Notes
- Kotlin, all-open: https://kotlinlang.org/docs/all-open-plugin.html
- Jar local
kotlin-allopen-compiler-plugin-embeddable-2.4.21.jar, do cache do Gradle.
Relacionados
- Trilha de Kotlin
- Curriculo de um Desenvolvedor Kotlin
- HTTP e REST
- Classes, objetos e propriedades
- Interfaces, delegação e classes open
- Null safety e tipos nuláveis
- Gradle Kotlin DSL e multi-módulo
- Spring Boot com controllers suspend
- Arquitetura hexagonal com ArchUnit
- Testes com Kotest, MockK e Testcontainers