CodeGym /Cursos /Spring Core /Resolución de dependencias Spring

Resolución de dependencias Spring

Spring Core
Nivel 7, Lección 3
Disponible

1. Resolución de dependencias e inicio de la aplicación

Mientras escribíamos constructores, todo parecía romántico: «He enumerado honestamente las dependencias, así que todo funcionará». Pero al contenedor le sirve de poco su honestidad, necesita entender concretamente qué colocar en cada parámetro. Si usted no se representa este proceso, cualquier stack trace al inicio se sentirá como “Spring se ha ofendido”. Y no se ha ofendido, simplemente no pudo armar el grafo de objetos.

En este punto es útil introducir tres palabras que aparecerán en los mensajes de error y en sus pensamientos cuando la aplicación no arranque:

  • Punto de inyección (injection point) — el lugar donde se debe insertar la dependencia. En el ámbito del día, suele ser el parámetro del constructor, pero en general puede ser un campo o un método.
  • Tipo de dependencia — la clase o interfaz del parámetro del constructor. Es la clave principal de búsqueda.
  • Candidatos (candidates) — los beans en el contenedor que coinciden por tipo y pueden ser inyectados.

Si mantiene estas tres cosas en mente, la mitad de la “magia” se disipa por sí sola. Spring no elige “según su estado de ánimo” — sigue un algoritmo claro: por tipo, por candidatos, y encuentra exactamente uno, o falla honestamente.

2. Cuándo Spring inyecta dependencias

Un pensamiento muy común del principiante: «Pero si todavía no he llamado nada, ¿por qué la aplicación ya ha fallado?». Porque Spring no espera a que usted empiece a crear pedidos; primero construye su aplicación como un LEGO, y luego le permite jugar. La construcción ocurre durante el arranque del ApplicationContext, cuando el contenedor pasa de «sé qué beans hay» a «estoy creando objetos reales».

Si simplificamos mucho (justo lo suficiente para no convertir la lección en un recorrido por el código fuente de Spring), el panorama es el siguiente:

flowchart TD
    A[Inicio de ApplicationContext] --> B[Registro de BeanDefinitions]
    B --> C[Creación de beans singleton]
    C --> D[Selección del constructor]
    D --> E[Resolución de parámetros por tipo]
    E --> F[Creación del objeto]
    F --> G["Inyección de campos/métodos (si existe)"]
    G --> H[Bean listo para trabajar]

Hoy nos interesa el tramo estrecho, pero el más “doloroso”: D → E. En ese momento Spring mira el constructor e intenta responder a la pregunta: «¿Qué beans concretos debo colocar en los parámetros?».

Y he aquí lo clave: si el bean se crea al iniciar el contexto (normalmente así ocurre para beans singleton en una aplicación de aprendizaje), los errores de resolución los verá al inicio. Esto es el comportamiento fail-fast: mejor fallar de inmediato con un claro “no hay candidato”, que dentro de 20 minutos de ejecución de un escenario chocar de repente con un NullPointerException en medio de una cancelación de pedido.

3. Búsqueda de candidatos por tipo

Cuando una clase tiene un constructor con parámetros, el contenedor interpreta esos parámetros como una lista de requisitos: “necesito un bean de tal tipo”. Y entonces hace algo muy concreto: va a su registro y busca beans que sean compatibles por tipo.

El “camino feliz” básico se ve así: para cada parámetro del constructor se encuentra exactamente un bean adecuado, por lo tanto se puede construir el objeto.

Veamos una parte muy conocida de ContextFlow: el servicio de auditoría depende del puerto AuditWriter.

AuditWriter (puerto):

public interface AuditWriter {
    // Contrato del puerto: los servicios escribirán auditoría a través de este método
    void write(String message);
}

Implementación (infraestructura) que se convierte en bean:

import com.example.contextflow.domain.ports.AuditWriter;
import org.springframework.stereotype.Component;

@Component
public class ConsoleAuditWriter implements AuditWriter {
    public void write(String message) {
        // Aquí la "infraestructura" real: escribimos en stdout como si fuera un log de auditoría
        System.out.println("[AUDIT] " + message); // [AUDIT] creado
    }
}

Y el servicio (capa de aplicación) que necesita AuditWriter:

import com.example.contextflow.domain.ports.AuditWriter;
import org.springframework.stereotype.Service;

@Service
public class AuditService {
    private final AuditWriter auditWriter;

    public AuditService(AuditWriter auditWriter) {
        // Spring debe encontrar exactamente un bean de tipo AuditWriter e inyectarlo aquí
        this.auditWriter = auditWriter;
    }
}

¿Por qué funciona? Porque en el contenedor hay un bean que encaja con el tipo AuditWriter: ConsoleAuditWriter. Spring ve el parámetro del constructor AuditWriter y encuentra un candidato por tipo.

En pseudocódigo (para que el cerebro no se ahogue en términos):

para cada parámetro del constructor:
  T = tipo del parámetro
  candidates = todos los beans que "son T"
  si candidates.size == 1 -> lo tomamos
  si candidates.size == 0 -> error: no hay candidato
  si candidates.size > 1 -> error: hay varios candidatos

Es importante destacar: el mismo mecanismo se aplica no solo para constructores, sino también para parámetros de métodos @Bean. Es decir, Spring “entrega” las dependencias de la misma manera aquí:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ScenarioConfig {
    @Bean
    public ScenarioRunner scenarioRunner(AuditService auditService) {
        // La dependencia AuditService también es resuelta por el contenedor según el tipo del parámetro
        return new ScenarioRunner(auditService);
    }
}

La idea es la misma: tipo del parámetro → se necesita un bean de ese tipo.

4. Error: no hay candidato

Ahora la parte más realista: usted ha escrito el constructor, todo bonito, pero el contenedor dice “no”. Esto ocurre cuando para el tipo deseado no se encuentra ningún candidato. Y entonces Spring no se calla, sino que falla con un conjunto de excepciones que al principio parecen aterradoras, pero luego resultan ser una pista muy útil.

Modelemos una situación típica. Imagine que olvidó hacer que la implementación de AuditWriter sea un bean: quitó @Component o accidentalmente movió la clase a un paquete que no está incluido en @ComponentScan.

Esta es la versión “mala” (se olvidó la anotación):

import com.example.contextflow.domain.ports.AuditWriter;

// @Component  <-- se olvidó, y la clase no se convirtió en bean
public class ConsoleAuditWriter implements AuditWriter {
    public void write(String message) {
        // La clase no está registrada como bean -> para el tipo AuditWriter no habrá candidatos en el contenedor
        System.out.println("[AUDIT] " + message);
    }
}

¿Qué ocurrirá al iniciar el contexto? El contenedor intentará crear AuditService, verá el constructor AuditService(AuditWriter auditWriter) y buscará un bean de tipo AuditWriter. Y no encontrará nada.

Fragmento del mensaje de error (simplificado, pero muy cercano en significado):

UnsatisfiedDependencyException: Error al crear el bean 'auditService'
...
Causado por: NoSuchBeanDefinitionException:
No qualifying bean of type '...AuditWriter' available

¿Cómo leer esto de manera humana? Empiece no por la línea superior, sino por la esencia:

El contenedor intentó crear el bean auditService, pero no pudo satisfacer la dependencia: necesita un AuditWriter, y no existe tal bean.

Para que quede claro, aquí hay una pequeña tabla (en lugar de interminables listas de excepciones):

Lo que hizo en el código Lo que vio Spring Cómo terminó
En el constructor hay un parámetro AuditWriter “Necesito un bean de tipo AuditWriter” Si no hay candidatos en el contenedor — NoSuchBeanDefinitionException
Usted olvidó @Component/@Bean en la implementación “Candidatos: 0” UnsatisfiedDependencyException con causa NoSuchBeanDefinitionException

Es importante entender el momento: el error ocurre no cuando usted llama a los métodos de AuditService, sino cuando Spring intenta crear AuditService como bean. Es decir, es un error a nivel de construcción de la aplicación.

En ContextFlow estos problemas ocurren más a menudo por tres razones “terrenales”: olvidó la anotación en la clase, olvidó @Bean en la configuración, o redujo los límites de @ComponentScan de modo que la clase ya no se escanea. Todo lo demás son casos raros, y por ahora no entramos en ellos.

5. Error: hay demasiados candidatos

El segundo dolor popular es el opuesto: los beans existen, e incluso hay demasiados. Y entonces el principiante suele preguntar: «Pues que Spring elija cualquiera…». Spring responde de una forma muy adulta: “no”. Porque “cualquiera” hoy podría ser SmsNotificationSender, y mañana — ConsoleNotificationSender, y obtendrá un bug que cambia según el orden de carga de las clases. Estos bugs son especialmente aficionados a aparecer los viernes por la noche.

Mostremos la situación con notificaciones. Hay una interfaz:

public interface NotificationSender {
    // Envío de notificación: el canal concreto (consola/SMS/correo) decide la implementación
    void send(String message);
}

Y dos implementaciones, ambas convertidas en beans:

import org.springframework.stereotype.Component;

@Component
public class ConsoleNotificationSender implements NotificationSender {
    public void send(String message) {
        // Implementación canal: simplemente imprimimos en consola
        System.out.println("[CONSOLE] " + message);
    }
}
import org.springframework.stereotype.Component;

@Component
public class SmsNotificationSender implements NotificationSender {
    public void send(String message) {
        // Implementación canal: simulamos envío por SMS
        System.out.println("[SMS] " + message);
    }
}

Y el servicio espera un solo NotificationSender:

import org.springframework.stereotype.Service;

@Service
public class NotificationDispatchService {
    public NotificationDispatchService(NotificationSender sender) {
        // Si hay más de un candidato NotificationSender, el contenedor no podrá elegir automáticamente
    }
}

El contenedor ve el parámetro de tipo NotificationSender, recoge los candidatos y descubre: “hay dos”. El resultado es un error, cuyo significado suele ser:

NoUniqueBeanDefinitionException:
expected single matching bean but found 2

Note que no es un “error de Spring”. Es el contenedor comunicando honestamente: “pediste un único objeto de tipo NotificationSender, pero veo dos, y no tengo derecho a adivinar”.

Y aquí nuevamente es importante “cuándo”: el error surge en el momento de crear NotificationDispatchService, porque es entonces cuando Spring resuelve los parámetros del constructor.

En el marco de la lección de hoy no convertimos esto en “cómo elegir al candidato correcto” (es una disciplina aparte), pero fijamos lo principal: para un punto de inyección se necesita un candidato inequívoco, de lo contrario el contenedor detiene el inicio. Es una reacción protectora, no un capricho.

Y aquí surge la siguiente pregunta natural: ¿cómo vivir si hay varias implementaciones de la interfaz y aún así hay que elegir una? Por ahora nos basta con fijar el modelo básico: el contenedor busca por tipo y exige un único candidato inequívoco. Por lo tanto, las dos fallas iniciales del día se ven así de simples: no hay candidato o hay demasiados candidatos.

6. Detalles importantes de la resolución

En la práctica, casi todos los problemas de resolución se resuelven entendiendo dos cosas: “por qué tipo estoy buscando” y “cuántos candidatos están realmente registrados”. Pero a veces Spring intenta ayudarle con pistas sutiles, y es útil saber dónde están para no sorprenderse con un comportamiento extraño. La más útil de esas pistas son los nombres de los parámetros del constructor, y otra son los tipos genéricos (generics).

Nombres de parámetros y el flag -parameters

En la línea base del curso tenemos activado el flag del compilador -parameters. Hace una cosa simple pero importante: conserva en el bytecode los nombres reales de los parámetros del constructor. Sin él, la reflexión a menudo ve no auditWriter, sino algo como arg0, arg1.

¿Para qué sirve esto al contenedor? A veces, si hay varios candidatos, Spring puede mirar el nombre del parámetro como una pista. Por ejemplo, si tiene un bean llamado smsSender, y el parámetro del constructor también se llama smsSender, el contenedor puede intentar emparejarlos.

Ejemplo (lo muestro como mecánica, no como “estilo ideal para siempre”):

import org.springframework.stereotype.Service;

@Service
public class NotificationDispatchService {
    public NotificationDispatchService(NotificationSender smsSender) {
        // nombre del parámetro: smsSender
    }
}

Si uno de los beans NotificationSender realmente se llama smsSender, esto podría ser un desempate. Pero hay un truco: si renombra el parámetro, rompe el cableado. Por lo tanto, tómeselo como “es útil saber por qué algo de repente funciona/falla”, no como la forma principal de diseñar.

Generics como parte del tipo

Aunque Java borra los tipos genéricos durante la ejecución (type erasure), Spring puede leer los genéricos de los metadatos de las clases y usarlos en la resolución. Esto ocurre con menos frecuencia al inicio del camino, pero a veces salva de la ambigüedad.

Idea a nivel de ejemplo: tiene una interfaz Formatter<T> y dos beans, uno para DailyReport y otro para AuditRecord. Entonces la dependencia Formatter<DailyReport> podría ser inequívoca incluso con dos implementaciones de Formatter.

Esqueleto corto del punto de inyección:

import org.springframework.stereotype.Service;

@Service
public class ReportingService {
    public ReportingService(Formatter<DailyReport> formatter) {
    }
}

El objetivo aquí no es que arrastre urgentemente genéricos a ContextFlow (no es necesario), sino que entienda que para Spring “tipo” a veces es un poco más rico que simplemente Formatter. Esto explica por qué en algunos proyectos la gente vive tranquilamente con varias implementaciones de una misma interfaz genérica y no recibe NoUniqueBeanDefinitionException.

Búsqueda rápida de la causa del error

Y por último, una habilidad que ahorra horas: mirar la causa raíz. En la cadena típica de excepciones, la parte superior dice “BeanCreationException/UnsatisfiedDependencyException”, y lo útil está más abajo:

  • si al final hay NoSuchBeanDefinitionException, significa que hay 0 candidatos;
  • si al final hay NoUniqueBeanDefinitionException, significa que hay más de 1 candidato;
  • si en el texto aparece el tipo concreto del parámetro del constructor, esa es su “clave de búsqueda”.

Muy a menudo basta con un pensamiento: “¿qué tipo exacto está buscando?” — y encuentra al instante el problema en el código o en la configuración.

7. Errores típicos en la resolución

Esta parte no es para asustarle, sino para que se reconozca en un par de situaciones y salga más rápido. Los errores de resolución de dependencias casi siempre se repiten: solo cambian el nombre de la interfaz y los nombres de los servicios. Si aprende a reconocer el patrón “0 candidatos / 2 candidatos”, Spring deja de ser aterrador y se convierte en una herramienta bastante directa.

Error nº 1: “Creé una interfaz y olvidé la implementación como bean”.
A menudo se ve así: en el constructor pide honestamente AuditWriter, pero la implementación ConsoleAuditWriter se olvidó de marcarla con @Component o se olvidó de registrarla mediante @Bean. Como resultado, el contenedor ve un contrato bonito y vacío en el registro. Se cura no con filosofía, sino con una verificación banal: la implementación realmente cae dentro de @ComponentScan o realmente se crea mediante un método @Bean.

Error nº 2: “Reduje @ComponentScan para que esté ‘más limpio’, y sin querer desactivé la mitad de la aplicación”.
Cuando los límites del escaneo se vuelven demasiado estrechos, obtiene el efecto de “todo compila, pero el contexto falla”. Al principiante le parece que se rompió el código de negocio, pero en realidad el contenedor simplemente no registró la clase necesaria como bean. En tal situación, es útil olvidarse del dominio por un minuto y preguntarse: “¿este paquete se escanea siquiera?”.

Error nº 3: “Aparecieron dos implementaciones — y esperaba que Spring eligiera cualquiera”.
Este es un conflicto clásico: usted agregó un segundo NotificationSender, pero el servicio sigue pidiendo un solo NotificationSender. El contenedor se niega a adivinar y falla con NoUniqueBeanDefinitionException. Aquí es importante no enfadarse con Spring, sino entender que le protege de un comportamiento aleatorio. Si la elección debe ser inequívoca, debe ser inequívoca ya a nivel de configuración, no “como salga”.

Error nº 4: “Solo leo la primera línea del stack trace”.
La primera línea casi siempre dice “no puedo crear el bean X”, y rara vez es la causa real. La causa real suele estar al final: o un tipo ausente (NoSuchBeanDefinitionException) o ambigüedad (NoUniqueBeanDefinitionException). Si empieza a leer de abajo hacia arriba, de repente descubre que Spring escribe cosas bastante humanas, solo que muy verbosas.

Error nº 5: “Confío en los nombres de los parámetros como mecanismo principal de selección”.
A veces todo funciona “mágicamente” después de renombrar un parámetro del constructor, y uno quiere seguir en la misma línea. El problema es que esto es frágil: renombró el parámetro al refactorizar, y el cableado cambió o se rompió. Los nombres de los parámetros son útiles como pista y como parte del diagnóstico (especialmente con -parameters activado), pero apoyarse en ellos como fundamento del diseño es arriesgado.

1
Tarea
Spring Core, nivel 7, lección 3
Bloqueada
Error del candidato ausente
Error del candidato ausente
1
Tarea
Spring Core, nivel 7, lección 3
Bloqueada
Error de seleccion ambigua por tipo
Error de seleccion ambigua por tipo
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION