← Arquitectura por capas con Spring

Sesión 16 · Semana 8

Inyección de dependencias

Proyecto compartido. En el taller de Intermodular que abre esta semana has trabajado la api en una url. En Servidor continúas la implementación del mismo producto.

Se explica

25 minutos · explicación y demostración

Ya tienes capas, pero las clases todavía pueden construir directamente a sus colaboradoras. La inyección de dependencias consiste en recibir esos objetos; Spring los crea y conecta. Una interfaz permite expresar qué necesita el servicio sin atarlo al almacenamiento en memoria.

El problema de decidir tú quién es tu colaborador

private final TareaRepository repositorio = new TareaRepository();

Esa línea dice tres cosas a la vez, y solo una es asunto del service:

Lo que decide una sola línea
  1. Necesito un repositorio. Esto sí es asunto suyo
  2. Y va a ser exactamente este, nunca otro
  3. Lo construye esta clase, en este punto, sin que nadie pueda intervenir

Las dos últimas son decisiones que el service no debería tomar, con consecuencias concretas: en la UD5 habrá que abrirlo para cambiar el repositorio, y en la sesión 17 no se podrá probar sin arrastrar el almacenamiento de verdad.

Inversión de control

Que no seas tú quien construye y conecta los objetos, sino algo externo. Tú declaras qué necesitas; otro te lo da ya montado.

Es la misma idea del framework de la UD1: no llamas tú, te llaman a ti. Aquí, además, no construyes tú: te construyen.

El contenedor de Spring

Bean

Un objeto que crea y administra Spring, en lugar de crearlo tú con new.

Al arrancar, y gracias al @ComponentScan que ya conoces desde la UD1, Spring recorre tus paquetes buscando clases marcadas, crea una instancia de cada una y las guarda. Después mira qué necesita cada una y se las va pasando.

Qué ocurre al arrancar la aplicación
  1. Recorre los paquetes buscando clases marcadas como componentes
  2. Ve que TareaService necesita un TareaRepository en su constructor
  3. Crea primero el repositorio, porque no necesita nada
  4. Crea el servicio pasándole ese repositorio
  5. Crea el controlador pasándole ese servicio

Ese orden lo calcula solo. Tú nunca escribes «primero esto y luego aquello».

Los estereotipos

Marcar una clase es poner una anotación. Hay varias y todas hacen lo mismo: registrar la clase como componente. Se diferencian en lo que le cuentan a quien lee el código.

Anotación Para Añade además
@Component Cualquier cosa Nada. Es la genérica
@Service Lógica de negocio Nada técnico: comunica la intención
@Repository Acceso a datos Traduce excepciones de persistencia. Importará en la UD5
@RestController Capa web Lo que ya sabes desde la UD1

Usa la que corresponde a la capa. No es decorativo: alguien que abra tu proyecto sabrá qué es cada clase sin leerla.

Por constructor, y no de otra forma

Verás una alternativa muy extendida:

@Autowired
private TareaRepository repositorio;

El resultado funciona, pero es inferior por tres motivos que conviene saber defender:

Por campo

El campo no puede ser final. Las dependencias quedan ocultas en el cuerpo de la clase, y probarla sin Spring exige recurrir a la reflexión.

Por constructor

Los campos son final. El constructor enumera todo lo que la clase necesita, de modo que puede instanciarse manualmente en un test con las dependencias que se decidan.

Hay un cuarto motivo, y es el mejor: un constructor con seis parámetros se ve feo, y eso es información. Está diciendo que la clase depende de demasiadas cosas. Con inyección por campo, esa misma clase tiene seis anotaciones repartidas y nadie lo nota.

Se trabaja

140 minutos · implementación guiada sobre el proyecto propio

Paso 1 · Retomar el proyecto y preparar la comprobación

  1. Abre controller, service y repository de la sesión 15. Localiza los new que crean colaboradores entre capas; no confundas esos objetos con nuevos registros o DTO.
  2. Dibuja quién necesita a quién: el controlador necesita el servicio y el servicio necesita el repositorio. Ese será el orden de los constructores.
  3. Identifica las operaciones del repositorio que pasarán a su interfaz. Conserva su implementación en memoria y la colección del contrato.

Paso 2 · Marca y pide por constructor

Los tres bloques modifican tres archivos existentes: repositorio, servicio y controlador. Añade la anotación de cada clase, sustituye el campo inicializado con new por un campo final sin inicializador y asigna ese campo en el constructor mostrado. Conserva todos los métodos de negocio y HTTP. Con un único constructor, Spring lo utiliza para conectar las clases. Reinicia al completar los tres cambios y comprueba una creación y una consulta.

package com.ejemplo.gestor.repository;

import org.springframework.stereotype.Repository;

@Repository
public class TareaRepository {
    // igual que ayer
}
package com.ejemplo.gestor.service;

import org.springframework.stereotype.Service;

@Service
public class TareaService {

    private final TareaRepository repositorio;

    public TareaService(TareaRepository repositorio) {
        this.repositorio = repositorio;
    }

    // los métodos, sin cambiar
}
@RestController
@RequestMapping("/tareas")
public class TareaController {

    private final TareaService servicio;

    public TareaController(TareaService servicio) {
        this.servicio = servicio;
    }

    // los métodos, sin cambiar
}

Ni un new. Cada clase declara lo que necesita en su constructor y Spring se lo entrega al arrancar.

No hace falta @Autowired

Lo verás en mucho código y en muchos tutoriales. Desde hace años, si una clase tiene un solo constructor, Spring lo usa sin que se lo pidas.

Escribirlo no está mal, pero es ruido. Si lo ves en un ejemplo antiguo, ya sabes qué es.

Añade esto temporalmente al service y arranca:

public TareaService(TareaRepository repositorio) {
    this.repositorio = repositorio;
    System.out.println("Construyendo TareaService con " + repositorio);
}

Verás la línea una sola vez, al arrancar, y no una por petición. Haz cinco peticiones: no aparece más. Los beans son únicos y compartidos por toda la aplicación.

La consecuencia de que sean únicos

Como hay una sola instancia atendiendo a todos, un bean no debe guardar datos de una petición concreta. Si tu service tuviera un campo «la tarea que estoy procesando», dos usuarios simultáneos se pisarían.

El ArrayList del repositorio sí es estado compartido, y es intencionado: es el almacén, y es de todos. En la UD5 ese papel lo hará la base de datos.

Paso 3 · Saca el repositorio a una interfaz

Renombra primero la clase actual a TareaRepositorioEnMemoria con la herramienta de refactorización del IDE; cambia también su archivo y constructor si lo tiene. Después crea una interfaz nueva llamada TareaRepository, declara sus métodos y haz que la clase renombrada la implemente. Corrige el import del servicio para que dependa de la interfaz y deja @Repository solo en la implementación. El ejemplo alternativo de repositorio con registro es una demostración: no lo actives a la vez hasta comprender cómo se elige un candidato.

package com.ejemplo.gestor.repository;

import com.ejemplo.gestor.model.Tarea;

import java.util.List;
import java.util.Optional;

public interface TareaRepository {

    List<Tarea> findAll();

    Optional<Tarea> findById(int id);

    Tarea save(Tarea tarea);

    boolean deleteById(int id);

    boolean existsById(int id);
}

La implementación correspondiente a esta sesión es esta:

package com.ejemplo.gestor.repository;

import org.springframework.stereotype.Repository;

@Repository
public class TareaRepositorioEnMemoria implements TareaRepository {
    // exactamente el código de ayer
}

El service no cambia ni una línea. Sigue pidiendo un TareaRepository en su constructor; lo que ocurre es que ahora eso es un contrato y no una clase concreta, y Spring le pasa la única implementación que encuentra.

Lo que acabas de dejar preparado

En la UD5, TareaRepositorioEnMemoria se borra y en su lugar aparece una interfaz que extiende JpaRepository. Spring la implementa solo.

Y el service, que ya depende de una interfaz con esos mismos nombres de método, no se entera. Cambias dónde se guardan los datos sin abrir la capa que decide las reglas. Eso es exactamente lo que prometían las capas, y hoy es cuando se cobra.

Crea una segunda implementación que registre cada llamada:

@Repository
@Primary
public class TareaRepositorioConRegistro implements TareaRepository {

    private final TareaRepositorioEnMemoria interno =
            new TareaRepositorioEnMemoria();

    @Override
    public Optional<Tarea> findById(int id) {
        System.out.println("Buscando la tarea " + id);
        return interno.findById(id);
    }

    // el resto, delegando igual
}

Arranca y haz una petición: aparece el mensaje. Has cambiado la implementación del almacenamiento sin tocar el service ni el controller. Después bórrala o quítale las anotaciones.

@Primary es lo que resuelve el empate cuando hay dos candidatos. Sin ella, la aplicación no arranca y dice que hay más de un bean del mismo tipo — pruébalo, porque es un error que te vas a encontrar.

Paso 4 · Cuando el contenedor se queja

Lo que ves al arrancar Qué significa Qué haces
NoSuchBeanDefinitionException Pide algo que nadie ha registrado ¿Falta la anotación? ¿La clase está fuera del paquete principal?
NoUniqueBeanDefinitionException Hay dos candidatos y no sabe cuál Marca uno con @Primary, o distingue con @Qualifier
The dependencies of some of the beans form a cycle Dos clases se necesitan mutuamente Rediseña: es un problema tuyo, no de Spring

La primera fila enlaza directamente con la UD1: si tu clase no cuelga del paquete de la clase principal, @ComponentScan no la ve, y aquí el síntoma ya no es un 404 silencioso sino un error de arranque bien explicado. Ha mejorado.

La tercera fila es la respuesta al reto de ayer

Preguntábamos qué peligro aparece si un service llama a otro service. Este: que ProyectoService necesite a TareaService y TareaService necesite a ProyectoService. Spring no puede construir ninguno de los dos, porque cada uno necesita al otro terminado.

Y hace bien en negarse. Una dependencia circular casi siempre significa que las responsabilidades están mal repartidas: hay un tercer concepto que no has nombrado, o una de las dos clases está haciendo algo que no le toca.

Paso 5 · Sustituir la creación manual de colaboradores por inyección

  1. Marca con su estereotipo todas tus clases de service y repository.
  2. Pásalas todas a inyección por constructor.
  3. Saca ProyectoRepository a interfaz con su implementación en memoria.
  4. Comprueba que no queda ningún new de un colaborador en controladores ni servicios. El new Tarea() de un mapper sí puede quedarse: eso no es un colaborador, es un dato.
  5. Ejecuta la colección: verde otra vez, sin tocar nada.

Paso 6 · Comprobar y registrar el resultado del proyecto

  1. Arranca la aplicación y comprueba que Spring puede construir las capas sin dependencias ausentes, ambiguas o circulares.
  2. Ejecuta el CRUD completo y revisa que el servicio depende de la interfaz. Crear objetos de dominio con new sigue siendo correcto; lo que cambia es la construcción de colaboradores gestionados por Spring.

Ampliación si has completado el trabajo

Primero termina y verifica los pasos anteriores. Estos retos profundizan en el mismo contenido; no sustituyen la entrega ni obligan a iniciar otro proyecto.

Reto · Provoca los tres errores

Uno a uno, provócalos, lee el mensaje entero y anótalo con tus palabras. Este reto es de diagnóstico, no de código.

  1. Quita el @Service de TareaService. ¿Qué error sale y en qué momento?
  2. Deja dos implementaciones de TareaRepository sin @Primary. ¿Qué error sale? ¿Nombra las dos candidatas el mensaje?
  3. Haz que TareaService reciba un ProyectoService y que ProyectoService reciba un TareaService. Arranca.

Para cada uno responde: ¿el error aparece al arrancar o al hacer la primera petición? Y después, la pregunta que importa:

¿Por qué es una buena noticia que estos tres fallos se detecten al arrancar y no durante una petición de un usuario?

Deja los tres arreglados antes de terminar.

Objetivo mínimoSin ningún new de colaboradores, todo por constructor, y la colección en verde.
Si lo tienesLos dos repositorios sacados a interfaz, con la implementación en memoria detrás.
RetoLos tres errores del contenedor provocados, leídos y explicados, incluida la dependencia circular.
Ver respuestas

1 · Que necesita un colaborador, cuál exactamente y quién lo construye. Solo la primera le corresponde: las otras dos las decide ahora el contenedor.

2 · Los campos pueden ser final y el constructor enumera todas las dependencias a la vista; además la clase se puede construir a mano en un test. Un constructor con demasiados parámetros advierte, por su parte, de que la clase depende de demasiadas cosas.

3 · Porque hay una sola instancia atendiendo a todas las peticiones, así que dos usuarios simultáneos se pisarían los datos.

4 · Que dos clases se necesitan mutuamente y ninguna se puede construir primero. Casi siempre indica que las responsabilidades están mal repartidas y falta un concepto por nombrar.

Cierre

15 minutos · resultado comprobable y explicación individual

Al terminar la sesión:

La aplicación arranca, la colección pasa y cada dependencia del servicio se ve en su constructor.

Cada integrante explica una decisión del código apoyándose en una de las comprobaciones realizadas.