Pular para o conteúdo

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

Tamanho da fonte:
Largura da coluna:
Tom:
Espaço entre linhas:
Fonte:

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 .

Sem injeção, PedidoService cria o EmailNotificador com new; com injeção, o ApplicationContext cria o bean e o entrega ao serviço
figura 1 · Quem cria as dependências: a própria classe ou o contêiner do Spring

À 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.

Vários beans do mesmo tipo: @Primary vence, @Qualifier ou o nome do parâmetro escolhe, e sem nenhum deles a aplicação falha ao subir
figura 2 · Como o Spring escolhe um bean quando há mais de um candidato

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.

Contêiner tenta criar Estoque, que pede Pedidos, que pede Estoque de volta; o ciclo fecha sem bean pronto e a aplicação falha ao subir
figura 3 · Criação de Estoque e Pedidos quando cada um depende do outro

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

  1. Guiado. Crie um Contador que implementa Notificador, 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
  1. Renomeie para canal o parâmetro de PedidoService da seção 5, sem @Primary e sem @Qualifier. Antes de rodar, diga o que acontece. A resposta é a falha abaixo, porque canal nã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.
  • @Configuration e @Bean: classe que declara beans, e método que devolve um.
  • @Primary e @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.

Relacionados

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?