Inyección de Dependencias en Spring Boot 4
Cómo VehicleValuationService obtiene su repository y sus clientes de proveedores sin construirlos jamás — y por qué Boot 4 los encuentra más rápido que nunca.
En la lección anterior, VehicleValuationService aparecía ya con un VehicleRepository y tres implementaciones de ValuationProviderClient, y ninguna de ellas se creó con un new dentro de la clase. Eso es inyección de dependencias (DI): en lugar de que una clase busque y construya las cosas de las que depende, esas cosas se le entregan desde afuera — se 'inyectan' — por un contenedor que se encarga de ensamblar todo el grafo de la aplicación. El contenedor de Spring, el ApplicationContext, es ese ensamblador. Construye cada bean que tu app declara, descubre qué beans dependen de cuáles otros, y los construye en el orden correcto.
Spring Boot admite tres estilos de inyección — por constructor, por campo y por setter — pero la inyección por constructor es la única que deberías usar por defecto, y Boot 4 no solo la recomienda, se apoya en ella. La inyección por campo (@Autowired lateinit var repository: VehicleRepository) parece conveniente, pero significa que la clase puede instanciarse en un estado a medio construir con dependencias nulas, hace imposible construir la clase manualmente en una prueba unitaria sin trucos de reflexión o un contexto completo de Spring, y esconde las dependencias obligatorias de quien lea la superficie pública de la clase. La inyección por constructor convierte cada dependencia en un val del constructor primario: el objeto literalmente no puede existir sin ellas, el compilador impone la inmutabilidad, y una prueba unitaria simple puede escribir VehicleValuationService(fakeRepo, fakeBlackBook, fakeCarfax, fakeVis) sin involucrar a Spring en absoluto.
La inyección por constructor también te da algo que la inyección por campo no puede: detección temprana de dependencias circulares en el arranque. Si VehicleValuationService dependiera de algún bean ProviderHealthMonitor, y ese monitor dependiera de vuelta de VehicleValuationService, un ciclo basado en constructores es estructuralmente imposible de satisfacer — Spring lanza una BeanCurrentlyInCreationException clara en el instante en que arrancas la app, diciéndote exactamente qué beans están involucrados. Con inyección por campo o por setter, Spring a veces puede disimular tal ciclo inyectando un proxy parcialmente inicializado, lo que cambia un fallo ruidoso en el arranque por un bug sutil en tiempo de ejecución que solo aparece cuando alguien realmente llama al bean a medio construir.
Spring encuentra la mayoría de tus beans mediante anotaciones de estereotipo, que en realidad son solo alias semánticos de @Component: @RestController marca a VehicleController como un bean de la capa web cuyos métodos se mapean a rutas HTTP, @Service marca a VehicleValuationService como un bean de lógica de negocio, y @Repository marca los beans de acceso a datos (aunque con Spring Data JPA, la propia interfaz ya es suficiente — Spring genera el bean que la implementa por ti). Estas anotaciones no cambian cómo funciona la DI mecánicamente; documentan la intención y, en el caso de @Repository, habilitan la traducción automática de excepciones específicas de la base de datos a la jerarquía consistente de DataAccessException de Spring.
Los estereotipos solo funcionan para clases que posees y puedes anotar. BlackBookClient, CarfaxClient y VisClient podrían envolver un SDK HTTP de terceros, tu cliente de Redis, o un ObjectMapper configurado con ajustes específicos — tipos que viven en una librería y no pueden llevar @Service encima. Para esos casos escribes una clase @Configuration con métodos de fábrica anotados @Bean: un método llamado blackBookClient() que construye y devuelve un BlackBookClient configurado, que Spring luego trata exactamente como cualquier otro bean y puede inyectar donde se necesite. Esta es la vía de escape que mantiene la DI funcionando de manera uniforme tanto en tu código como en todo lo que no escribiste tú.
Spring Boot 4 cambia cuánto de este cableado ocurre mediante escaneo de classpath con mucha reflexión frente al registro explícito. El Spring Boot clásico escanea el classpath en el arranque, encuentra cada anotación de la familia @Component, y construye reflexivamente una definición de bean para cada una — flexible, pero cuesta tiempo de arranque y memoria real, especialmente a medida que la app crece. Boot 4 apuesta fuerte por el registro funcional de beans (registrar beans mediante código explícito, como llamadas a registerBean, en lugar de escaneo por anotaciones) combinado con procesamiento Ahead-of-Time (AOT), donde Spring analiza el grafo de beans en tiempo de compilación y genera la mayor parte del cableado por adelantado en lugar de descubrirlo escaneando clases en cada arranque. Las anotaciones de estereotipo que escribes siguen funcionando igual desde tu perspectiva — sigues escribiendo @Service y parámetros de constructor — pero por debajo, Boot 4 puede resolver gran parte de ese grafo antes de que la JVM siquiera arranque la app, que es lo que hace que los builds procesados con AOT y las imágenes nativas arranquen dramáticamente más rápido.
Nada de esto cambia el modelo mental que necesitas día a día: depende de abstracciones a través de tu constructor, deja que Spring provea las instancias concretas, y nunca instancies un colaborador tú mismo dentro de un bean gestionado por Spring. Ya sea que el contenedor ensamble VehicleValuationService vía reflexión clásica o vía código generado por AOT, la clase misma se ve idéntica — un constructor primario que lista todo lo que necesita, sin nada oculto, sin nada opcional que no debería serlo.
@Serviceclass VehicleValuationService(private val repository: VehicleRepository,private val blackBook: BlackBookClient,private val carfax: CarfaxClient,private val vis: VisClient,) {// No @Autowired needed on the constructor: Spring picks the single// constructor automatically when there is only one.}// Contrast: field injection, which this codebase avoids.// class VehicleValuationServiceBad {// @Autowired// lateinit var repository: VehicleRepository // nullable window before injection runs// }
Constructor injection: every collaborator is a val in the primary constructor, so the class cannot exist without them. Requires a real Spring context to actually be instantiated by the container, so it does not run in-browser.
@Configurationclass ProviderClientConfig {@Beanfun blackBookClient(@Value("\${providers.black-book.base-url}") baseUrl: String,httpClient: OkHttpClient,): BlackBookClient = BlackBookClient(baseUrl, httpClient)@Beanfun httpClient(): OkHttpClient = OkHttpClient.Builder().callTimeout(Duration.ofSeconds(5)).build()}
A @Bean method for a third-party type you don't own and cannot annotate with a stereotype. Requires a Spring ApplicationContext to process the @Configuration class, so it does not run in-browser.
class VehicleValuationServiceTest {private val repository = mockk<VehicleRepository>()private val blackBook = mockk<BlackBookClient>()private val carfax = mockk<CarfaxClient>()private val vis = mockk<VisClient>()private val service = VehicleValuationService(repository, blackBook, carfax, vis)@Testfun `valuate returns a decision built from all three providers`() = runTest {coEvery { repository.findActiveRulesFor(any()) } returns emptyList()coEvery { blackBook.quote(any()) } returns Quote(1200_00)coEvery { carfax.quote(any()) } returns Quote(1150_00)coEvery { vis.quote(any()) } returns Quote(1180_00)val result = service.valuate("1HGCM82633A004352")assertEquals("APPROVED", result.decision)}}
Because dependencies arrive via the constructor, a unit test can build the service with fakes and zero Spring involvement. Uses MockK, which requires the project's test classpath, so it does not run in-browser.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. A teammate refactors VehicleValuationService to use field injection with @Autowired lateinit var properties instead of constructor injection, arguing it reduces boilerplate in the class header. What is the strongest technical objection to this change?