# Día 12: El Contenedor de Beans de Spring Boot 3 e Inyección Idiomática

> ApplicationContext, Component Scanning, Requisitos de Java 17 y Compatibilidad con Java 21

- Plan: [JavaMastery](https://aprende-java.webs-economicas.es/), día 12 de 30
- Fase 3: El Salto a Spring Boot 3 y APIs REST
- Dedicación: 3.5h
- Profundidad: para dominar
- Revisado: 2026-10-29
- Versión interactiva: https://aprende-java.webs-economicas.es/dias/spring-boot-beans-inyeccion/

## Al terminar este día sabrás

- Explicar por qué Spring Boot 3 exige Java 17 y qué cambió con Jakarta EE.
- Describir el ciclo de vida de un bean, desde el escaneo hasta su destrucción.
- Declarar beans propios y de terceros con inyección por constructor idiomática.
- Leer configuración tipada e inmutable con `@ConfigurationProperties` y records.


## Conceptos

### 1. Spring Boot 3 Exige Java 17 LTS como Base Mínima

Spring Boot 3 abandonó Java 8 y 11. El estándar empresarial mínimo de Spring hoy es Java 17, permitiendo usar records y características modernas en todo el framework.

**Cómo funciona por dentro.**

Spring Boot 3 está construido sobre Spring Framework 6, que fijó Java 17 como versión mínima. No es un capricho: el código del framework usa records, sealed, text blocks y otras APIs del JDK 17, y se compila a bytecode de versión 61. Una JVM 11 no puede ni cargar esas clases.

Esa base permitió otros cambios de fondo: el salto a Jakarta EE 9+ (paquetes `jakarta.*`), la compilación anticipada (AOT) para generar imágenes nativas con GraalVM, la observabilidad con Micrometer integrada y, a partir de Spring Boot 3.2 con Java 21, el soporte de Virtual Threads.

En la práctica, en un proyecto nuevo se elige Java 21 (la LTS más reciente con soporte amplio) y Spring Boot 3.x. Java 17 sigue siendo el mínimo y es lo habitual en empresas que aún no han migrado.

*Comprobar versiones en arranque*

```java
@SpringBootApplication
public class TaskFlowApplication {

    public static void main(String[] args) {
        SpringApplication.run(TaskFlowApplication.class, args);
    }

    @Bean
    ApplicationRunner informarVersiones(Environment env) {
        return args -> {
            System.out.println("Java " + Runtime.version());
            System.out.println("Spring Boot " + SpringBootVersion.getVersion());
            System.out.println("Hilos virtuales: " + env.getProperty("spring.threads.virtual.enabled", "false"));
        };
    }
}
```

- **En una línea:** En `pom.xml`: `<java.version>17</java.version>` o `<java.version>21</java.version>`.
- **Por qué importa:** Si aplicas a un puesto de Spring Boot 3, debes dominar como mínimo Java 17.
- **Error típico:** Intentar compilar Spring Boot 3 con Java 11 o paquetes antiguos `javax.*` en lugar de `jakarta.*`.
- **Java 17 frente a 21:** Java 17 es la base mínima; Java 21 es la versión recomendada para nuevas implementaciones.
- **Se conecta con:** Jakarta EE, Spring Framework 6, Migración

### 2. El ApplicationContext y el Ciclo de Vida de los Beans

El contenedor de Spring gestiona la creación, configuración y destrucción de tus objetos (Beans). Por defecto crea una única instancia por bean (Singleton Scope).

**Cómo funciona por dentro.**

Al arrancar, Spring Boot crea un `ApplicationContext`. Primero registra definiciones de beans: las clases anotadas que encuentra al escanear el paquete de tu clase principal (`@Component`, `@Service`, `@Repository`, `@Controller`, `@Configuration`), los métodos `@Bean` y las autoconfiguraciones que se activan según lo que haya en el classpath.

Después, para cada bean singleton: lo instancia (llamando al constructor con sus dependencias ya resueltas), aplica los `BeanPostProcessor` (aquí es donde Spring envuelve tu objeto en un proxy si tiene `@Transactional`, `@Async` o `@Cacheable`), ejecuta los métodos `@PostConstruct` y lo deja listo. Al cerrar el contexto, ejecuta los `@PreDestroy` en orden inverso.

El ámbito por defecto es singleton: una sola instancia por contexto, compartida por todos. Por eso un bean no debe guardar estado de una petición concreta en sus campos: varias peticiones lo usan a la vez desde hilos distintos.

*Ganchos del ciclo de vida*

```java
@Component
public class CatalogoEtiquetas {
    private final EtiquetaRepository repo;
    private volatile Set<String> etiquetas = Set.of();   // estado global, no de petición

    CatalogoEtiquetas(EtiquetaRepository repo) {          // 1. instanciación con dependencias
        this.repo = repo;
        System.out.println("constructor");
    }

    @PostConstruct                                        // 2. dependencias ya inyectadas
    void cargar() {
        etiquetas = Set.copyOf(repo.todas());
        System.out.println("cargadas " + etiquetas.size() + " etiquetas");
    }

    @EventListener(ApplicationReadyEvent.class)           // 3. la aplicación ya acepta tráfico
    void listo() {
        System.out.println("aplicación lista");
    }

    @PreDestroy                                           // 4. al cerrar el contexto
    void liberar() {
        System.out.println("liberando recursos");
    }
}
```

- **En una línea:** `@Service public class TareaServicio { ... }` administrado automáticamente.
- **Por qué importa:** Automatiza todo el cableado manual que hicimos en el Día 6 en el método main().
- **Error típico:** Crear beans con estado mutable compartido entre peticiones sin sincronización multihilo.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** IoC, Inyección por Constructor, Spring Core

### 3. Inyección por Constructor sin Anotaciones Innecesarias

En Spring moderno, si una clase tiene un solo constructor, Spring inyecta las dependencias automáticamente sin necesidad de `@Autowired`.

**Cómo funciona por dentro.**

Desde Spring 4.3, si una clase tiene un único constructor, Spring lo usa para inyectar sin necesidad de `@Autowired`. Combinado con campos `final`, el resultado es una clase que no depende de Spring en absoluto salvo por la anotación de estereotipo, y que puedes crear con `new` en un test.

Cuando la clase que quieres convertir en bean no es tuya (un cliente HTTP, un `Clock`, un `ObjectMapper` personalizado), no puedes anotarla. Para eso existen los métodos `@Bean` dentro de una clase `@Configuration`: el método construye el objeto y Spring lo registra. Los parámetros del método también se inyectan.

Si hay varias implementaciones de la misma interfaz, Spring no sabe cuál elegir y falla al arrancar. Se resuelve con `@Primary` en la implementación por defecto, con `@Qualifier` en el punto de inyección, o inyectando `List<Interfaz>` para recibirlas todas.

*Beans propios y de terceros*

```java
@Service
public class TareaService {                       // sin @Autowired: un único constructor
    private final TareaRepository repo;
    private final Notificador notificador;
    private final Clock reloj;

    TareaService(TareaRepository repo, Notificador notificador, Clock reloj) {
        this.repo = repo;
        this.notificador = notificador;
        this.reloj = reloj;
    }
}

@Configuration
class InfraestructuraConfig {

    @Bean
    Clock reloj() {                               // clase del JDK: no se puede anotar
        return Clock.systemUTC();
    }

    @Bean
    RestClient githubClient(RestClient.Builder builder) {   // el builder lo autoconfigura Boot
        return builder.baseUrl("https://api.github.com").build();
    }

    @Bean
    @Primary
    Notificador notificadorEmail(JavaMailSender mail) { return new NotificadorEmail(mail); }

    @Bean
    Notificador notificadorSlack(RestClient.Builder b) { return new NotificadorSlack(b); }
}
```

- **En una línea:** `public MiClase(Dependencia dep) { this.dep = dep; }`
- **Por qué importa:** Mantiene el código desacoplado de anotaciones de Spring en la firma de la clase.
- **Error típico:** Llenar los atributos con `@Autowired private Servicio s;` por inercia.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** Inmutabilidad, Clean Architecture, Testing

### 4. Configuración Tipada con @ConfigurationProperties y Records

Spring Boot 3 permite vincular propiedades de `application.yml` directamente a un `record` Java inmutable.

**Cómo funciona por dentro.**

`@Value("${...}")` lee propiedades sueltas como cadenas y las convierte, pero dispersa la configuración por el código, no se valida y no tiene autocompletado. `@ConfigurationProperties` agrupa un prefijo de configuración en un tipo Java. Con un `record`, el binding se hace por constructor y el objeto resultante es inmutable.

Spring Boot aplica binding relajado: `taskflow.max-tareas-por-proyecto`, `TASKFLOW_MAXTAREASPORPROYECTO` como variable de entorno o `taskflow.maxTareasPorProyecto` acaban en el mismo componente. Convierte tipos útiles como `Duration` (`30s`, `5m`), `DataSize` (`10MB`), listas y mapas.

Con `@Validated` y anotaciones de Bean Validation en los componentes, la aplicación no arranca si la configuración es inválida: es mucho mejor fallar al desplegar que a las tres de la mañana cuando se usa el valor por primera vez.

*Configuración tipada, inmutable y validada*

```java
@ConfigurationProperties(prefix = "taskflow")
@Validated
public record TaskFlowProperties(
        @Min(1) @Max(500) int maxTareasPorProyecto,
        @NotNull Duration expiracionToken,
        @NotEmpty List<String> dominiosPermitidos,
        Notificaciones notificaciones) {

    public record Notificaciones(boolean email, @Email String remitente) {}
}

@SpringBootApplication
@ConfigurationPropertiesScan          // registra los records anotados
public class TaskFlowApplication { }

// application.yml
// taskflow:
//   max-tareas-por-proyecto: 200
//   expiracion-token: 30m
//   dominios-permitidos: [acme.com, acme.es]
//   notificaciones:
//     email: true
//     remitente: no-reply@acme.com
```

- **En una línea:** `@ConfigurationProperties(prefix = "app.mail") public record MailConfig(String host, int port) {}`
- **Por qué importa:** Elimina anotaciones sueltas `@Value("${prop}")` y valida la configuración al arrancar la app.
- **Error típico:** Usar `@Value` con nombres de propiedades con erratas que fallan silenciosamente en producción.
- **Java 17 frente a 21:** Soporte completo de Records en 17 y 21.
- **Se conecta con:** application.yml, Records, Validación

### 5. Paquetes `jakarta.*` frente al antiguo `javax.*`

En Spring Boot 3 y Java 17+, todos los paquetes de Java EE pasaron a llamarse Jakarta (`jakarta.persistence.*`, `jakarta.validation.*`).

**Cómo funciona por dentro.**

Java EE (Servlet, JPA, Bean Validation, JAX-RS...) pasó de Oracle a la Fundación Eclipse con el nombre Jakarta EE. Por motivos de marca registrada, Oracle no permitió seguir evolucionando el espacio de nombres `javax`, así que Jakarta EE 9 renombró todos los paquetes de `javax.*` a `jakarta.*`. Funcionalmente las APIs eran las mismas; solo cambió el nombre.

Spring Boot 3 usa Jakarta EE 10. Eso implica que cualquier `import javax.persistence.Entity` o `javax.validation.constraints.NotNull` debe pasar a `jakarta.*`, y que las librerías de terceros tienen que tener una versión compatible con Jakarta. Ojo: no todo `javax` cambió. Lo que forma parte del JDK (`javax.sql.DataSource`, `javax.crypto`, `javax.net.ssl`) sigue igual.

Para migrar proyectos grandes, OpenRewrite tiene recetas que hacen el cambio de imports, dependencias y propiedades automáticamente.

*Qué cambia y qué no*

```java
// Spring Boot 2.x (Java EE 8)          // Spring Boot 3.x (Jakarta EE 10)
// import javax.persistence.Entity;     import jakarta.persistence.Entity;
// import javax.validation.Valid;       import jakarta.validation.Valid;
// import javax.servlet.Filter;         import jakarta.servlet.Filter;

// Siguen en javax porque pertenecen al JDK, no a Java EE:
import javax.sql.DataSource;
import javax.crypto.Cipher;
import javax.net.ssl.SSLContext;

/*
  Migración automática con OpenRewrite:
  ./mvnw -U org.openrewrite.maven:rewrite-maven-plugin:run \
    -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:RELEASE \
    -Drewrite.activeRecipes=org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_3
*/
```

- **En una línea:** `import jakarta.annotation.PostConstruct;`
- **Por qué importa:** Cualquier código o tutorial antiguo que use `javax.persistence` fallará al compilar en Spring Boot 3.
- **Error típico:** Importar `javax.validation.constraints.NotNull` en vez de `jakarta.validation.constraints.NotNull`.
- **Java 17 frente a 21:** Estándar en 17 y 21.
- **Se conecta con:** Jakarta EE 10, Spring Boot 3, Hibernate 6


## Sesión de 90 minutos

### Bloque 1: Arquitectura Spring Boot 3 (0 - 25 min)

- **Qué estudiar:** ApplicationContext, Bean lifecycle, y el requisito de Java 17/21.
- **Pregunta a responder:** ¿Por qué `@SpringBootApplication` agrupa 3 anotaciones clave?
- **Entregable:** Proyecto en start.spring.io con Spring Boot 3.2+ y Java 17 o 21 verificado.

### Bloque 2: Inyección por Constructor Idiomática (25 - 50 min)

- **Qué estudiar:** Creación de capas Controller -> Service -> Repository sin un solo `@Autowired`.
- **Pregunta a responder:** ¿Cómo resuelve Spring las dependencias cuando hay una única interfaz y dos implementaciones (`@Primary`, `@Qualifier`)?
- **Entregable:** Servicio inyectado limpiamente con variables `final`.

### Bloque 3: Configuración con Records (50 - 80 min)

- **Qué estudiar:** Uso de `@ConfigurationProperties` vinculado a un `record`.
- **Pregunta a responder:** ¿Cómo leer propiedades de `application.yml` de forma inmutable y fuertemente tipada?
- **Entregable:** Record de configuración tipado y validado en el arranque.

### Cierre y Consolidación (80 - 90 min)

- **Qué estudiar:** Repaso activo del ciclo de vida de los beans.
- **Pregunta a responder:** Explicar qué hace `@PostConstruct` y `@PreDestroy`.
- **Entregable:** Notas de estudio registradas en la app local.


## Explícalo desde cero

### Cómo encuentra Spring tus Beans (@ComponentScan)

- **Qué entender:** `@SpringBootApplication` contiene `@ComponentScan`. Al arrancar, escanea el paquete actual hacia abajo buscando `@Component`, `@Service`, `@Repository` y `@RestController` para registrarlos en el grafo de dependencias.
- **Qué suele explicarse mal:** Pensar que Spring "adivina" o busca en todo el disco duro. Si creas una clase en un paquete superior fuera de la jerarquía de la clase Application, Spring no la verá.
- **Ejemplo sencillo:** Un radar que solo barre el piso en el que está colocado y los pisos inferiores del edificio; si pones un objeto en la terraza superior, no lo detecta.
- **Señal de alerta:** Si no sabes por qué Spring lanza `NoSuchBeanDefinitionException` al mover una clase de paquete.


## Preguntas

### Nivel 1 (Básico): ¿Por qué Spring Boot 3 no permite arrancar aplicaciones con Java 8 o Java 11?

Porque Spring Framework 6 fue compilado con target de Java 17 y migró la especificación corporativa a Jakarta EE 10, requiriendo un JDK 17 o superior como baseline.

**Respuesta razonada:** Porque las clases de Spring Framework 6 y Spring Boot 3 están compiladas para Java 17 (bytecode versión 61). Una JVM 8 (versión 52) o 11 (versión 55) se niega a cargarlas y la aplicación falla nada más arrancar con `UnsupportedClassVersionError`. No es una comprobación artificial: el framework usa internamente APIs y características que no existen en esas versiones.

Subir la versión mínima permitió además simplificar el código del framework, adoptar Jakarta EE 9+ y habilitar AOT y GraalVM. Si un sistema está atado a Java 11, debe quedarse en Spring Boot 2.7, que ya no tiene soporte gratuito.

### Nivel 2 (Intermedio): ¿Qué diferencia práctica hay entre `@Component`, `@Service` y `@Repository` en Spring?

En tiempo de ejecución son prácticamente iguales (todas son especializaciones de `@Component`), pero `@Repository` añade traducción automática de excepciones de base de datos a `DataAccessException`, y `@Service` comunica semántica de capa de negocio.

**Respuesta razonada:** Las cuatro son estereotipos: anotaciones meta-anotadas con `@Component`, así que el escaneo las detecta igual y todas crean un bean singleton. La diferencia está en la intención y en algún comportamiento extra.

`@Service` es puramente semántica: marca lógica de negocio y no añade comportamiento. `@Repository` sí añade algo: el `PersistenceExceptionTranslationPostProcessor` envuelve el bean para traducir excepciones nativas de JPA, Hibernate o JDBC a la jerarquía `DataAccessException` de Spring. `@Controller` y `@RestController` hacen que Spring MVC busque en ellos métodos `@GetMapping`, `@PostMapping`, etc. Usar el estereotipo correcto documenta la arquitectura y permite aplicar aspectos por capa.

### Nivel 3 (Entrevista Senior): ¿Cómo activas Virtual Threads de Java 21 en una aplicación Spring Boot 3.2+ y qué cambia internamente en Tomcat?

Con `spring.threads.virtual.enabled=true`. Internamente, el `TomcatProtocolHandler` sustituye su pool de hilos de plataforma por un `Executors.newVirtualThreadPerTaskExecutor()`, atendiendo cada petición HTTP en un Virtual Thread efímero.

**Respuesta razonada:** Con Java 21 y Spring Boot 3.2 o superior basta con `spring.threads.virtual.enabled=true` en la configuración. Internamente, Tomcat deja de usar su pool de hilos de plataforma (200 por defecto) y pasa a usar un ejecutor que crea un hilo virtual por petición. También se configuran con hilos virtuales el `TaskExecutor` de `@Async`, el planificador de `@Scheduled` y los listeners de Kafka y RabbitMQ.

El efecto es que las peticiones que esperan a E/S (base de datos, llamadas HTTP) ya no bloquean un hilo real, así que la aplicación aguanta muchas más peticiones concurrentes sin aumentar memoria. Hay que vigilar tres cosas: el pinning por bloques `synchronized` que hacen E/S (mejorado en Java 24), el abuso de `ThreadLocal` (cada hilo virtual tiene el suyo) y que el pool de conexiones a la base de datos pasa a ser el verdadero límite.


## Reto del tutor

**Reto:** Un desarrollador júnior dice: "Tengo un bean con `@Scope("prototype")` y otro con `@Scope("singleton")`. Si inyecto el Prototype dentro del Singleton, cada petición web obtendrá una instancia nueva del Prototype". ¿Es correcto?

- **Lo que está bien:** Reconoce que Prototype genera instancias nuevas al solicitarse al contenedor.
- **Respuesta superficial:** Decir que "sí, porque es prototype".
- **Lo que falta de fondo:** No entender que el Singleton solo se instancia UNA vez al arrancar la app; en ese momento se le inyecta el Prototype y ese mismo Prototype quedará fijado para siempre dentro del Singleton.
- **Respuesta senior:** Es completamente FALSO (el clásico "problema del bean scoping"). El Singleton se inicializa una sola vez; por tanto, la inyección del Prototype ocurre una sola vez. Cada petición web usará la misma instancia inyectada al inicio. Para resolverlo se debe usar un `ObjectProvider<MiPrototype>`, `@Lookup` o rediseñar la arquitectura evitando beans con estado mutable.


## Katas

### Kata 12.1: Inyección limpia en Spring Boot 3

Crea una aplicación Spring Boot 3 con un `@RestController` y un `@Service`. Inyecta el servicio en el controlador sin usar `@Autowired`, usando únicamente constructor y variables `final`.


## Reglas de oro

- Usa siempre inyección por constructor sobre campos `private final`; evita `@Autowired` en atributos.
- Importa siempre `jakarta.*`, jamás el obsoleto `javax.*`.
- En Java 21 activa Virtual Threads para maximizar el throughput HTTP de Tomcat.


## Java 17 frente a Java 21

| Característica | Java 17 | Java 21 |
| --- | --- | --- |
| Versión mínima requerida | Obligatoria en Spring Boot 3 | Completamente soportada |
| Virtual Threads integrados | No disponible | Disponible con `spring.threads.virtual.enabled=true` |


## Proyecto guiado «Pedidos»: Pasar a Spring Boot 3

Dejar que Spring cree y conecte los componentes que hasta ahora ensamblabas a mano.

**Tareas:**

- Convierte el proyecto en una aplicación Spring Boot 3 y elimina el ensamblado manual del `main`.
- Mueve las comisiones de cada medio de pago a `application.yml` y léelas con `@ConfigurationProperties` sobre un record.
- Crea los perfiles `dev` (H2) y `prod` (PostgreSQL).

**Está listo cuando:**

- La aplicación arranca con `./mvnw spring-boot:run` y el perfil `dev` sin configuración extra.
- Ningún componente usa `@Autowired` sobre campos.


## Fuentes oficiales

- [Spring Framework: el contenedor IoC y los beans](https://docs.spring.io/spring-framework/reference/core/beans.html)
- [Spring Boot: beans e inyección de dependencias](https://docs.spring.io/spring-boot/reference/using/spring-beans-and-dependency-injection.html)
- [Spring Boot: configuración externa](https://docs.spring.io/spring-boot/reference/features/external-config.html)

