Spring framework básico

XML puro a Spring Boot: historia y fundamentos


¿Qué se necesita saber ? Java básico (clases, interfaces, herencia).

Java Básico

Java Avanzado


📌 ÍNDICE

  1. ¿Por qué existe Spring?
  2. Historia y Evolución
  3. Los Pilares de Spring: IoC y DI
  4. El Contenedor de Spring
  5. Configuración con XML (la forma original)
  6. Configuración con Anotaciones
  7. Configuración con Java puro (@Configuration)
  8. Anotaciones Esenciales de Spring
  9. Spring MVC — La capa Web

1. ¿Por qué existe Spring?

El problema: Java EE en el año 2002

Para que entiendan Spring, hay que entender el problema que existía antes de él

En los años 2000, si querian construir una aplicación empresarial en Java, se usaba Java EE (Java Enterprise Edition) y su componente estrella: los EJB (Enterprise JavaBeans). (Aún muy usado hoy en día para arquitecturas orientadas en componentes).

El problema es que los EJB eran su complejidad, aunque hoy en día es mucho mas usable.

// Así se veía un componente de negocio con EJB 2.x (año 2002)
// Solo para crear un servicio "CalculadoraBean" necesitaba:
 
// 1. Una interfaz remota
public interface CalculadoraRemote extends javax.ejb.EJBObject {
    int sumar(int a, int b) throws RemoteException;
}
 
// 2. Una interfaz home (para crear instancias)
public interface CalculadoraHome extends javax.ejb.EJBHome {
    CalculadoraRemote create() throws RemoteException, CreateException;
}
 
// 3. La implementación (extiende una clase base de EJB)
public class CalculadoraBean implements javax.ejb.SessionBean {
    private SessionContext context;
 
    public int sumar(int a, int b) {
        return a + b;
    }
 
    // Métodos de ciclo de vida OBLIGATORIOS — aunque no se usen
    public void ejbCreate() throws CreateException {}
    public void ejbRemove() {}
    public void ejbActivate() {}
    public void ejbPassivate() {}
    public void setSessionContext(SessionContext ctx) { this.context = ctx; }
}
 
// 4. Un descriptor XML (ejb-jar.xml) — decenas de líneas de configuración
// 5. Desplegar en un servidor de aplicaciones completo (WebLogic, JBoss, WebSphere)
// 6. Solo para ejecutar: return a + b;

Los problemas de EJB

Desarrollar se complicaba por que:

- Demasiado código boilerplate — interfaces, clases abstractas, métodos vacíos obligatorios
- Acoplado al contenedor — no podías probar tu código sin levantar un servidor
- Lento — tanto en desarrollo como en ejecución
- Complejo — la curva de aprendizaje era enorme
- Difícil de testear — imposible hacer unit tests simples
- Requería servidores caros — no podías ejecutar en Tomcat básico

El resultado: los proyectos tardaban meses en arrancar,
los tests eran casi inexistentes, y los desarrolladores sufrían.

2. Historia y Evolución

2002 — Rod Johnson cambia todo

En 2002, Rod Johnson publicó el libro “Expert One-on-One J2EE Design and Development” y con él incluyó un framework de ~30,000 líneas de código que demostraba que se podía construir aplicaciones empresariales robustas sin EJB.

Su idea central era revolucionaria pero simple:

“El framework debe adaptarse al código del desarrollador, no al revés.”

El código que incluía en ese libro se convirtió en Spring Framework, lanzado oficialmente en 2003.

La línea de tiempo de Spring

2002  Rod Johnson publica su libro con el código seminal de Spring
  │
2003  Spring Framework 1.0 — Lanzamiento oficial
  │   • IoC Container (el corazón de Spring)
  │   • AOP (Programación Orientada a Aspectos)
  │   • Configuración 100% XML -> Hoy en día innecesaria
  │
2006  Spring Framework 2.0
  │   • Mejor soporte XML, namespaces
  │   • Primeras anotaciones (@Transactional)
  │
2007  Spring Framework 2.5
  │   • Anotaciones para componentes (@Component, @Autowired)
  │   • Se puede reducir el XML considerablemente
  │
2009  Spring Framework 3.0
  │   • Configuración Java pura (@Configuration, @Bean)
  │   • Se usa spring sin XML
  │   • Spring MVC mejorado
  │
2013  Spring Framework 4.0
  │   • Soporte Java 8 (lambdas, streams)
  │   • Spring Boot 1.0 (primer lanzamiento)
  │   • WebSocket support
  │
2014  Spring Boot 1.0 — punto de inflexión
  │   • Convención sobre configuración
  │   • Sin XML, sin boilerplate que era lo que solucionaba de los EJB
  │   • Aplicaciones ejecutables (fat jar)
  │
2017  Spring Framework 5.0 + Spring Boot 2.0
  │   • Spring WebFlux (programación reactiva)
  │   • Soporte Java 9+
  │   • Kotlin support
  │
2022  Spring Framework 6.0 + Spring Boot 3.0
  |   • Requiere Java 17+
  |   • Jakarta EE (javax → jakarta)
  |   • Virtual Threads support (Java 21)
  |   • GraalVM Native Images
2026  Spring Framework 7.0 + Spring Boot 4.0.2
  |   • Requierve Java 25+
  |   • Soporte de Jackson 3
  |   • Configuraciones de resiliencia nativas (Sin uso de Resilience4J)
  |   • Orientado a aplicaciones modulares
  |   • Gran soporte a MCP y AI Applications (Spring AI)

¿Qué problema resolvió Spring?

// El mismo "CalculadoraBean" con Spring:
public class CalculadoraService {
    public int sumar(int a, int b) {
        return a + b;
    }
}
 
// Una clase Java normal (POJO — Plain Old Java Object)
// Sin extender clases de framework
// Sin implementar interfaces de infraestructura
// Sin mtodos obligatorios vacíos
// Completamente testeable sin servidor (Unit test)

Spring demostró que la complejidad de EJB era innecesaria. Se podia tener todas las características empresariales (transacciones, seguridad, inyección de dependencias) trabajando con POJOs simples.


3. Los Pilares de Spring: IoC y DI

Inversión de Control (IoC)

IoC es el principio más importante de Spring. Para entenderlo, primero veamos el problema que resuelve.

El problema: acoplamiento fuerte

// Sin IoC — el código crea sus propias dependencias
public class PedidoService {
 
    // PedidoService CREA su propia dependencia
    private EmailService emailService = new EmailService();  // acoplamiento fuerte
    private PedidoRepository repo = new PedidoRepository();
 
    public void crearPedido(Pedido pedido) {
        repo.guardar(pedido);
        emailService.enviar(pedido.getEmail(), "Pedido confirmado");
    }
}
 
// PROBLEMAS:
// 1. Para testear PedidoService, se debe tener EmailService y PedidoRepository
//    funcionando (conectados a BD, servidor de email, etc.)
// 2. No se puede cambiar la implementación sin modificar PedidoService
// 3. Si EmailService falla al crearse, PedidoService ni siquiera arranca
// 4. Difícil de testear en aislamiento (unit tests)

La solución: Inversión de Control

// Con IoC — las dependencias son PROVISTAS desde afuera
public class PedidoService {
 
    // Las dependencias son DECLARADAS, no creadas
    private final EmailService emailService;
    private final PedidoRepository repo;
 
    // Alguien externo (Spring) las proveerá por el constructor
    public PedidoService(EmailService emailService, PedidoRepository repo) {
        this.emailService = emailService;
        this.repo = repo;
    }
 
    public void crearPedido(Pedido pedido) {
        repo.guardar(pedido);
        emailService.enviar(pedido.getEmail(), "Pedido confirmado");
    }
}
 
// VENTAJAS:
// 1. Para testear, paso EmailService y PedidoRepository "falsos" (mocks)
// 2. Puedo cambiar la implementación de EmailService sin tocar PedidoService
// 3. El código es más limpio y fácil de entender
// 4. Unit tests triviales

IoC significa: “No llames a mis dependencias, yo te las daré” — el control de la creación se invierte: ya no lo tiene la clase, lo tiene el framework.

Inyección de Dependencias (DI)

DI es la implementación de IoC. Es el mecanismo concreto por el cual Spring entrega las dependencias.

// Existen 3 formas de inyección:
 
// 1. Inyección por Constructor (recomendada según el estándar) 
public class PedidoService {
    private final EmailService emailService;  // final → inmutable
 
    public PedidoService(EmailService emailService) {
        this.emailService = emailService;
    }
}
// Dependencias obligatorias y visibles
// Objeto siempre en estado válido
// Facilita testing
// Fácil de ver qué necesita la clase
 
//  2. Inyección por Setter 
public class PedidoService {
    private EmailService emailService;
 
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }
}
// Para dependencias opcionales
// El objeto puede quedar en estado inválido si no se inyecta
 
//  3. Inyección por Campo / @Autowired (comun en Spring, pero lo acopla al framework) 
public class PedidoService {
    @Autowired  // Spring inyecta aquí usando reflexión
    private EmailService emailService;
}
// Menos código
// No se puede hacer final (no inmutable)
// Dificulta el testing sin Spring
// Las dependencias son ocultas

La analogía de la fábrica de coches

SIN IoC:
El motor CONSTRUYE su propio carburador:
  Motor {
    Carburador carburador = new Carburador();  // ← el motor decide qué carburador usar
  }

CON IoC:
La FÁBRICA (Spring) ensambla el coche:
  Motor {
    Carburador carburador;  // ← el motor declara que necesita un carburador
    Motor(Carburador c) { this.carburador = c; }
  }
  // Spring crea el Carburador y se lo da al Motor.
  // El Motor ni sabe cómo se creó el Carburador.

4. El Contenedor de Spring

¿Qué es el ApplicationContext?

El contenedor IoC de Spring (llamado ApplicationContext) es el corazón del framework. Es una clase de “fábrica” que:

  1. Lee la configuración (XML, anotaciones o Java)
  2. Crea los objetos que necesita la aplicación (llamados Beans)
  3. Conecta las dependencias entre ellos (inyección)
  4. Gestiona el ciclo de vida de los objetos

image.png

¿Qué es un Bean?

Un Bean es simplemente un objeto gestionado por el contenedor de Spring. Spring:

  • Lo crea
  • Lo configura
  • Lo ensambla (inyecta sus dependencias)
  • Gestiona su ciclo de vida (creación, uso, destrucción)
// Esta clase es solo Java normal...
public class EmailService {
    public void enviar(String destino, String mensaje) {
        System.out.println("Enviando a " + destino + ": " + mensaje);
    }
}
 
// ...pero cuando Spring la gestiona, SE CONVIERTE EN UN BEAN.
// Spring la crea UNA SOLA VEZ (singleton por defecto) y la comparte.

Los tipos de ApplicationContext

// 1. ClassPathXmlApplicationContext — lee XML del classpath
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
 
// 2. FileSystemXmlApplicationContext — lee XML del sistema de archivos
ApplicationContext ctx = new FileSystemXmlApplicationContext("/ruta/applicationContext.xml");
 
// 3. AnnotationConfigApplicationContext — lee clases @Configuration (sin XML)
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
 
// 4. AnnotationConfigWebApplicationContext — para aplicaciones web
// (Spring Boot lo configura automáticamente)
 
// Obtener un Bean del contenedor:
PedidoService servicio = ctx.getBean(PedidoService.class);
// o por nombre:
PedidoService servicio = (PedidoService) ctx.getBean("pedidoService");

El ciclo de vida de un Bean

CREACIÓN:
    1. Spring instancia la clase (llama al constructor)
    2. Inyecta las dependencias (setters, campos, o constructor)
    3. Llama a @PostConstruct (si existe)
    4. El Bean está listo para usarse

USO:
    5. La aplicación usa el Bean normalmente

DESTRUCCIÓN (cuando se cierra el contexto):
    6. Llama a @PreDestroy (si existe)
    7. El objeto es destruido por el GC
// Ejemplo con ciclo de vida
@Component
public class ConexionBD {
 
    private Connection conexion;
 
    @PostConstruct  // se llama DESPUÉS de la inyección de dependencias
    public void inicializar() {
        System.out.println("Conectando a la base de datos...");
        conexion = crearConexion();
    }
 
    public void ejecutarQuery(String sql) {
        // usar la conexion...
    }
 
    @PreDestroy  // se llama ANTES de destruir el bean
    public void cerrar() {
        System.out.println("Cerrando conexión...");
        if (conexion != null) {
            conexion.close();
        }
    }
}

5. Configuración con XML (la forma original)

Esta es la forma en que Spring se configuraba desde 2003. Aunque hoy nadie la usa, es importante entenderla para comprender de dónde vino Spring Boot.

Estructura básica del XML

<!-- applicationContext.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">
 
    <!-- Definir un Bean -->
    <bean id="emailService" class="com.ejemplo.EmailService"/>
 
    <!-- Bean con constructor injection -->
    <bean id="pedidoRepository" class="com.ejemplo.PedidoRepository"/>
 
    <!-- Bean con dependencias -->
    <bean id="pedidoService" class="com.ejemplo.PedidoService">
        <!-- Inyección por constructor -->
        <constructor-arg ref="emailService"/>
        <constructor-arg ref="pedidoRepository"/>
    </bean>
 
</beans>

Inyección de dependencias en XML

<!-- ━━━ Inyección por Constructor ━━━ -->
<bean id="pedidoService" class="com.ejemplo.PedidoService">
    <!-- ref = referencia a otro bean -->
    <constructor-arg ref="emailService"/>
    <constructor-arg ref="pedidoRepository"/>
    <!-- value = valor literal -->
    <constructor-arg value="5"/>
</bean>
 
<!-- ━━━ Inyección por Setter ━━━ -->
<bean id="pedidoService" class="com.ejemplo.PedidoService">
    <!-- Llama a setEmailService(emailService) -->
    <property name="emailService" ref="emailService"/>
    <!-- Llama a setMaxIntentos(3) -->
    <property name="maxIntentos" value="3"/>
    <!-- Llama a setNombre("Servicio Principal") -->
    <property name="nombre" value="Servicio Principal"/>
</bean>
 
<!-- ━━━ Inyección de listas y mapas ━━━ -->
<bean id="notificadorService" class="com.ejemplo.NotificadorService">
    <property name="canales">
        <list>
            <value>email</value>
            <value>sms</value>
            <value>push</value>
        </list>
    </property>
    <property name="configuracion">
        <map>
            <entry key="host" value="smtp.gmail.com"/>
            <entry key="puerto" value="587"/>
        </map>
    </property>
</bean>

Alcance (Scope) de los Beans

<!-- singleton (defecto): una sola instancia compartida -->
<bean id="emailService" class="com.ejemplo.EmailService" scope="singleton"/>
 
<!-- prototype: nueva instancia cada vez que se pide -->
<bean id="carrito" class="com.ejemplo.Carrito" scope="prototype"/>
 
<!-- request: una instancia por petición HTTP (solo en web) -->
<bean id="contextoUsuario" class="com.ejemplo.ContextoUsuario" scope="request"/>
 
<!-- session: una instancia por sesión HTTP (solo en web) -->
<bean id="sesionUsuario" class="com.ejemplo.SesionUsuario" scope="session"/>

Cómo se usaba en el código

// Clase de dominio (sin anotaciones Spring — POJO puro)
public class EmailService {
    private String host;
    private int puerto;
 
    // Setter para inyección XML
    public void setHost(String host) { this.host = host; }
    public void setPuerto(int puerto) { this.puerto = puerto; }
 
    public void enviar(String destino, String mensaje) {
        System.out.println("Enviando desde " + host + ":" + puerto);
    }
}
 
// Cargar el contexto y usar los beans
public class Main {
    public static void main(String[] args) {
 
        // Cargar la configuración XML
        ApplicationContext ctx =
            new ClassPathXmlApplicationContext("applicationContext.xml");
 
        // Obtener el bean y usarlo
        PedidoService servicio = ctx.getBean("pedidoService", PedidoService.class);
        servicio.crearPedido(new Pedido("laptop", 1200.00));
 
        // Cerrar el contexto (dispara @PreDestroy)
        ((ConfigurableApplicationContext) ctx).close();
    }
}

El problema del XML

Con el tiempo, los proyectos grandes tenían archivos XML de miles de líneas. Era difícil de mantener, no había autocompletado, los errores solo aparecían en tiempo de ejecución, y era imposible navegar.

<!-- Un applicationContext.xml real de 2008 podía verse así: -->
<beans>
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mibd"/>
        <property name="username" value="root"/>
        <property name="password" value="secret"/>
    </bean>
    <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
                <prop key="hibernate.show_sql">true</prop>
            </props>
        </property>
    </bean>
    <!-- ... 200 beans más ... -->
</beans>

Esta complejidad llevó a buscar una alternativa → las anotaciones.


6. Configuración con Anotaciones

En Spring 2.5 (2007), llegó el soporte para anotaciones. La idea: en vez de describir los beans en XML, marcar directamente las clases con anotaciones.

Habilitar el escaneo de componentes

Para que Spring encuentre las clases anotadas, se debe indicar dónde buscarlas. En aquella época se hacía con una línea de XML (o más adelante con @ComponentScan):

<!-- applicationContext.xml reducido -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="...">
 
    <!-- Escanear todos los componentes en el paquete "com.ejemplo" y subpaquetes -->
    <context:component-scan base-package="com.ejemplo"/>
 
</beans>

@Component y sus especializaciones

// @Component — marca cualquier clase como Bean gestionado por Spring
// (uso genérico — cuando no encaja en ninguna categoría específica)
@Component
public class Utilidades {
    public String formatearFecha(Date fecha) {
        return new SimpleDateFormat("dd/MM/yyyy").format(fecha);
    }
}
 
// ━━━ Las 3 especializaciones semánticas de @Component ━━━
// Las especializaciones no cambian el comportamiento de los beans, solo indican
// al que lee el codigo el proposito del bean
 
// @Service — lógica de negocio (la capa de servicios)
// Semánticamente indica "esto es un servicio de negocio"
@Service
public class PedidoService {
    public Pedido crearPedido(String producto, double precio) {
        return new Pedido(producto, precio);
    }
}
 
// @Repository — acceso a datos (la capa de persistencia)
// Además traduce excepciones de BD a excepciones de Spring
@Repository
public class PedidoRepository {
    public void guardar(Pedido pedido) {
        // lógica de acceso a BD
    }
    public Optional<Pedido> buscarPorId(Long id) {
        // lógica de búsqueda
        return Optional.empty();
    }
}
 
// @Controller — controlador web (la capa de presentación)
// Maneja peticiones HTTP en Spring MVC
@Controller
public class PedidoController {
    @GetMapping("/pedidos")
    public String listar(Model model) {
        return "pedidos/lista";  // nombre de la vista
    }
}
 
// @RestController — controlador REST (devuelve datos, no vistas)
// = @Controller + @ResponseBody en cada método
@RestController
public class PedidoApiController {
    @GetMapping("/api/pedidos")
    public List<Pedido> listar() {
        return List.of();  // se serializa automáticamente a JSON
    }
}

@Autowired — Inyección automática

@Service
public class PedidoService {
 
    // FORMA 1: Inyección por campo (simple pero tiene desventajas)
    @Autowired
    private PedidoRepository repository;
 
    @Autowired
    private EmailService emailService;
 
    // FORMA 2: Inyección por constructor, mas usada
    // Desde Spring 4.3, si hay un solo constructor, @Autowired es opcional
    private final PedidoRepository repository;
    private final EmailService emailService;
 
    @Autowired  // opcional si hay un solo constructor
    public PedidoService(PedidoRepository repository, EmailService emailService) {
        this.repository = repository;
        this.emailService = emailService;
    }
 
    // FORMA 3: Inyección por setter
    @Autowired
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }
}

@Qualifier — Cuando hay múltiples implementaciones

// Problema: Spring no sabe cuál de las dos inyectar
@Repository
public class MySQLPedidoRepository implements PedidoRepository { /* ... */ }
 
@Repository
public class PostgresPedidoRepository implements PedidoRepository { /* ... */ }
 
// Solución: @Qualifier especifica cuál queremos
@Service
public class PedidoService {
 
    @Autowired
    @Qualifier("mySQLPedidoRepository")  // nombre del bean (primera letra minúscula) -> camelCase
    private PedidoRepository repository;
}
 
// Alternativa: dar un nombre explícito al bean
@Repository("repoProductivo")
public class MySQLPedidoRepository implements PedidoRepository { /* ... */ }
 
@Service
public class PedidoService {
    @Autowired
    @Qualifier("repoProductivo")
    private PedidoRepository repository;
}

@Value — Inyectar valores de configuración

# src/main/resources/application.properties
app.nombre=Mi Tienda Online
app.email.host=smtp.gmail.com
app.email.puerto=587
app.pedido.max-items=50
@Service
public class ConfiguracionService {
 
    @Value("${app.nombre}")
    private String nombreApp;
 
    @Value("${app.email.host}")
    private String emailHost;
 
    @Value("${app.email.puerto}")
    private int emailPuerto;
 
    @Value("${app.pedido.max-items:20}")  // 20 es el valor por defecto
    private int maxItemsPorPedido;
 
    @Value("${variable.inexistente:valorPorDefecto}")
    private String conDefault;
}

@Scope con anotaciones

// Singleton (defecto) — una instancia compartida por toda la aplicación
@Service
@Scope("singleton")  // opcional, es el defecto
public class PedidoService { /* ... */ }
 
// Prototype — nueva instancia cada vez que se inyecta
@Component
@Scope("prototype")
public class Carrito {
    private List<Item> items = new ArrayList<>();
    // cada vez que alguien pide este bean, recibe una instancia nueva
}

7. Configuración con Java puro (@Configuration)

En Spring 3.0 (2009), llegó la configuración 100% Java. Sin XML. Sin anotaciones de escaneo. Solo código Java.

// La clase de configuración 
@Configuration  // ← le dice a Spring "esta clase define beans"
public class AppConfig {
 
    // Cada método @Bean define un Bean
    // El nombre del método ES el nombre del bean por defecto
    @Bean
    public EmailService emailService() {
        return new EmailService("smtp.gmail.com", 587);
    }
 
    @Bean
    public PedidoRepository pedidoRepository() {
        return new PedidoRepository();
    }
 
    // Spring inyecta automáticamente los parámetros del método @Bean
    @Bean
    public PedidoService pedidoService(EmailService emailService,
                                        PedidoRepository repository) {
        return new PedidoService(emailService, repository);
    }
 
    // Equivale exactamente a esto en XML:
    // <bean id="pedidoService" class="com.ejemplo.PedidoService">
    //     <constructor-arg ref="emailService"/>
    //     <constructor-arg ref="pedidoRepository"/>
    // </bean>
}
 
// ━━━ Usar la configuración Java ━━━
public class Main {
    public static void main(String[] args) {
        // En vez de ClassPathXmlApplicationContext, usamos:
        ApplicationContext ctx =
            new AnnotationConfigApplicationContext(AppConfig.class);
 
        PedidoService servicio = ctx.getBean(PedidoService.class);
        servicio.crearPedido("Laptop", 1200.00);
    }
}

Combinar @Configuration con @ComponentScan

// Se pueden combinar: @Configuration para beans de infraestructura (Aquellos que son de librerias externas)
// + @ComponentScan para escanear automáticamente nuestros propios componentes
@Configuration
@ComponentScan(basePackages = "com.ejemplo")  // escanea com.ejemplo y subpaquetes
@PropertySource("classpath:application.properties")  // carga un .properties
public class AppConfig {
 
    // Solo definir beans que NO tienen @Component/@Service/@Repository
    // (generalmente beans de librerías de terceros o de infraestructura)
 
    @Bean
    public DataSource dataSource() {
        // Configurar conexión a la base de datos
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/mibd");
        ds.setUsername("root");
        ds.setPassword("secret");
        return ds;
    }
 
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

Comparativa: XML vs Anotaciones vs Java

XMLAnotaciones@Configuration
AutocompletadoNOSINO
Type SafetyNOSINO
Error enEjecuciónCompilacionCompilacion
Configuracion librerias de tercerosSINONO
VerbosidadAltaBajaMed
La práctica estándar hoy:
• @Component/@Service/@Repository/@Controller para nuestras clases
• @Configuration/@Bean para infraestructura y librerías externas
• Spring Boot elimina casi todo el boilerplate restante

8. Anotaciones Esenciales de Spring

Anotaciones de Estereotipo (Componentes)

// @Component 
// Propósito: marcar una clase genérica como Bean de Spring
// Cuándo usar: cuando no encaja en Service, Repository ni Controller
@Component
public class ConvertidorMoneda {
    public double convertir(double valor, String monedaOrigen, String monedaDestino) {
        // lógica de conversión
        return valor * obtenerTipoCambio(monedaOrigen, monedaDestino);
    }
}
 
// @Service
// Propósito: capa de lógica de negocio
// Cuándo usar: para servicios que orquestan operaciones de negocio
@Service
public class FacturacionService {
    public Factura generarFactura(Pedido pedido) {
        // lógica de negocio: calcular impuestos, descuentos, etc.
        return new Factura(pedido, calcularTotal(pedido));
    }
}
 
// @Repository
// Propósito: acceso a datos (DAO pattern)
// Beneficio extra: Spring convierte excepciones de BD (SQLException, etc.)
//   en su propia jerarquía de excepciones (DataAccessException)
@Repository
public class ProductoRepository {
    @Autowired
    private JdbcTemplate jdbc;
 
    public List<Producto> findAll() {
        return jdbc.query("SELECT * FROM productos",
            (rs, row) -> new Producto(rs.getLong("id"), rs.getString("nombre")));
    }
}
 
// @Controller
// Propósito: manejar peticiones HTTP, retornar vistas (HTML)
@Controller
@RequestMapping("/productos")
public class ProductoController {
    @GetMapping("/lista")
    public String lista(Model model) {
        model.addAttribute("productos", service.listar());
        return "productos/lista";  // nombre del template Thymeleaf/JSP
    }
}
 
// @RestController
// Propósito: manejar peticiones HTTP, retornar datos (JSON/XML)
// = @Controller + @ResponseBody automático en cada método
@RestController
@RequestMapping("/api/productos")
public class ProductoRestController {
    @GetMapping
    public List<ProductoDTO> listar() {
        return service.listar();  // serializado automáticamente a JSON
    }
}

Anotaciones de Inyección

// @Autowired
// Propósito: inyectar un Bean automáticamente por tipo
@Service
public class PedidoService {
    @Autowired  // Spring busca un bean del tipo EmailService
    private EmailService emailService;
}
 
// @Qualifier 
// Propósito: elegir entre múltiples beans del mismo tipo
@Service
public class NotificacionService {
    @Autowired
    @Qualifier("emailServiceSMTP")  // cuál de los EmailService quiero
    private EmailService emailService;
}
 
// @Value 
// Propósito: inyectar valores de properties o expresiones SpEL
@Component
public class Config {
    @Value("${app.max-reintentos:3}")
    private int maxReintentos;
 
    @Value("#{sistemProperties['user.name']}")  // SpEL — Spring Expression Language
    private String nombreUsuario;
}
 
// @Primary
// Propósito: marcar el bean preferido cuando hay múltiples del mismo tipo
// Spring lo elige cuando no hay @Qualifier
@Service
@Primary  // este es el EmailService por defecto
public class EmailServiceSMTP implements EmailService { /* ... */ }
 
@Service
public class EmailServiceMock implements EmailService { /* ... */ }

Anotaciones de Ciclo de Vida

@Service
public class ServicioCache {
    private Map<String, Object> cache;
 
    // ━━━ @PostConstruct ━━━
    // Se ejecuta UNA VEZ después de que Spring inyecta todas las dependencias
    // Ideal para: inicialización, cargar configuraciones, precargar datos
    @PostConstruct
    public void inicializar() {
        System.out.println("Inicializando cache...");
        this.cache = new ConcurrentHashMap<>();
        // cargar datos iniciales, conectar a servicios externos, etc.
    }
 
    // ━━━ @PreDestroy ━━━
    // Se ejecuta UNA VEZ antes de que Spring destruya el bean
    // Ideal para: cerrar conexiones, liberar recursos, guardar estado
    @PreDestroy
    public void limpiar() {
        System.out.println("Liberando recursos de la cache...");
        this.cache.clear();
        // cerrar conexiones, flush de datos, etc.
    }
}

Anotaciones de Transacciones

// @Transactional
// Propósito: envolver un método en una transacción de base de datos
// Spring ABRE la transacción antes del método y la CIERRA (commit o rollback) al salir
 
@Service
@Transactional  // aplica a todos los métodos de la clase
public class BancoService {
 
    // Transferencia — si falla en cualquier punto, todo se revierte
    @Transactional  // también se puede por método
    public void transferir(Long cuentaOrigenId, Long cuentaDestinoId, double monto) {
        Cuenta origen = cuentaRepo.findById(cuentaOrigenId).orElseThrow();
        Cuenta destino = cuentaRepo.findById(cuentaDestinoId).orElseThrow();
 
        origen.debitar(monto);   // si esto falla →
        destino.acreditar(monto); // esto también se revierte
 
        cuentaRepo.save(origen);
        cuentaRepo.save(destino);
        // Si llegamos aquí → COMMIT
        // Si lanzó excepción → ROLLBACK automático
    }
 
    // Lectura optimizada — no abre transacción de escritura
    @Transactional(readOnly = true)
    public List<Cuenta> listarCuentas() {
        return cuentaRepo.findAll();
    }
}

Anotaciones de Configuración

// @Configuration 
// Propósito: indica que la clase define beans para el contexto de Spring
@Configuration
public class AppConfig {
    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper();
    }
}
 
// @ComponentScan 
// Propósito: indicar dónde buscar @Component, @Service, etc.
@Configuration
@ComponentScan(
    basePackages = "com.ejemplo",
    excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,
                                            classes = Controller.class)
)
public class AppConfig { /* ... */ }
 
// @PropertySource
// Propósito: cargar un archivo .properties en el Environment de Spring
@Configuration
@PropertySource("classpath:database.properties")
@PropertySource("classpath:email.properties")
public class AppConfig { /* ... */ }
 
// @Bean 
// Propósito: definir un bean explícitamente dentro de @Configuration
@Configuration
public class InfraConfig {
 
    @Bean(name = "emailPrincipal")  // nombre personalizado
    @Scope("prototype")              // scope
    public EmailService emailService() {
        return new EmailServiceSMTP("smtp.gmail.com", 587);
    }
 
    @Bean
    @ConditionalOnProperty(name = "feature.cache", havingValue = "true")
    // solo si app.feature.cache=true en properties
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("productos", "usuarios");
    }
}

Anotaciones de Spring MVC / REST

// ━━━ Mapeo de rutas ━━━
@RestController
@RequestMapping("/api/v1/productos")  // ruta base
public class ProductoController {
 
    // GET /api/v1/productos
    @GetMapping
    public List<ProductoDTO> listar() { /* ... */ }
 
    // GET /api/v1/productos/42
    @GetMapping("/{id}")
    public ProductoDTO obtener(@PathVariable Long id) { /* ... */ }
 
    // POST /api/v1/productos
    @PostMapping
    public ResponseEntity<ProductoDTO> crear(@RequestBody CrearProductoRequest req) { /* ... */ }
 
    // PUT /api/v1/productos/42
    @PutMapping("/{id}")
    public ProductoDTO actualizar(@PathVariable Long id, @RequestBody ActualizarRequest req) { /* ... */ }
 
    // DELETE /api/v1/productos/42
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> eliminar(@PathVariable Long id) { /* ... */ }
 
    // PATCH /api/v1/productos/42
    @PatchMapping("/{id}/activar")
    public ProductoDTO activar(@PathVariable Long id) { /* ... */ }
}
 
// ━━━ Parámetros de entrada ━━━
@GetMapping("/buscar")
public List<ProductoDTO> buscar(
    @RequestParam String nombre,                        // ?nombre=laptop
    @RequestParam(required = false) String categoria,   // opcional
    @RequestParam(defaultValue = "0") int pagina,       // con valor por defecto
    @PathVariable Long id,                              // parte de la URL
    @RequestBody CrearProductoRequest body,             // cuerpo JSON
    @RequestHeader("Authorization") String token,       // header HTTP
    @CookieValue("sessionId") String sessionId          // cookie
) { /* ... */ }

9. Spring MVC — La capa Web

¿Qué es el patrón MVC?

image.png

DispatcherServlet — El corazón de Spring MVC

El DispatcherServlet es el “Front Controller” de Spring MVC. Todas las peticiones HTTP pasan por él, y él decide a qué controlador enviarlas.

<!-- Configuración en web.xml (antes de Spring Boot) -->
<web-app>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring/mvc-config.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
 
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>  <!-- todas las peticiones van al dispatcher -->
    </servlet-mapping>
</web-app>
// Con Spring Boot, esto se configura automáticamente. NO se necesita web.xml.
// Spring Boot registra el DispatcherServlet automáticamente al detectar
// que se tiene spring-boot-starter-web en el classpath.

Model, View y Template

// En un controlador MVC (no REST), el método agrega datos al Model
// y retorna el nombre de la vista a renderizar
 
@Controller
@RequestMapping("/productos")
@RequiredArgsConstructor
public class ProductoWebController {
 
    private final ProductoService service;
 
    // Model — contenedor de datos que se pasan a la vista
    // Se comporta como un mapa, es de tipo clave-valor al agregar datos
    @GetMapping("/lista")
    public String lista(Model model) {
        List<Producto> productos = service.listarActivos();
        model.addAttribute("productos", productos);           // datos para la vista
        model.addAttribute("titulo", "Catálogo de Productos");
        return "productos/lista";  // nombre de la vista → templates/productos/lista.html
    }
 
    @GetMapping("/{id}")
    public String detalle(@PathVariable Long id, Model model) {
        Producto producto = service.buscar(id)
            .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
        model.addAttribute("producto", producto);
        return "productos/detalle";  // templates/productos/detalle.html
    }
 
    // Mostrar formulario de creación
    @GetMapping("/nuevo")
    public String mostrarFormulario(Model model) {
        model.addAttribute("producto", new ProductoForm());
        return "productos/formulario";
    }
 
    // Procesar el formulario
    @PostMapping("/nuevo")
    public String procesarFormulario(
        @ModelAttribute @Valid ProductoForm form,
        BindingResult errores,
        RedirectAttributes flash  // para mensajes después de redirect
    ) {
        if (errores.hasErrors()) {
            return "productos/formulario";  // mostrar errores
        }
 
        service.crear(form);
        flash.addFlashAttribute("mensaje", "Producto creado exitosamente");
        return "redirect:/productos/lista";  // redirect → evita doble envío del form
    }
}

Thymeleaf — El motor de plantillas clásico de Spring

<!-- templates/productos/lista.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"> <--//Schema link -->
<head>
    <title th:text="${titulo}">Título</title>
</head>
<body>
    <h1 th:text="${titulo}">Catálogo</h1>
 
    <!-- Iterar sobre la lista -->
    <table>
		    <!-- Se parece a un for each -->
        <tr th:each="producto : ${productos}">
            <td th:text="${producto.nombre}">Nombre</td>
            <td th:text="${#numbers.formatDecimal(producto.precio, 1, 2)}">0.00</td>
            <!-- Enlace dinámico -->
            <td>
                <a th:href="@{/productos/{id}(id=${producto.id})}">Ver detalle</a>
            </td>
        </tr>
    </table>
 
    <!-- Condicional -->
    <p th:if="${#lists.isEmpty(productos)}">No hay productos disponibles.</p>
 
    <!-- Mensaje flash -->
    <div th:if="${mensaje}" class="alerta" th:text="${mensaje}"></div>
</body>
</html>
 
<!-- templates/productos/formulario.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
    <!-- Formulario vinculado al objeto 'producto' del Model -->
    <form th:action="@{/productos/nuevo}" th:object="${producto}" method="post">
 
        <label>Nombre:</label>
        <input type="text" th:field="*{nombre}"/>
        <!-- Mostrar errores de validación -->
        <span th:errors="*{nombre}" class="error"></span>
 
        <label>Precio:</label>
        <input type="number" th:field="*{precio}" step="0.01"/>
        <span th:errors="*{precio}" class="error"></span>
 
        <button type="submit">Crear Producto</button>
    </form>
</body>
</html>

Conceptos clave para recordar

- IoC = Inversión de Control
   → Spring crea y gestiona los objetos, no tu código

- DI = Inyección de Dependencias
   → Spring "entrega" las dependencias a los objetos que las necesitan
   → Por constructor (recomendado), setter o campo

- Bean = objeto gestionado por Spring
   → Creado, configurado y destruido por el contenedor

- ApplicationContext = el contenedor IoC
   → Lee la configuración, crea beans, los conecta

- @SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan
   → El punto de entrada de toda aplicación Spring Boot

- Auto-configuración de Boot
   → Spring Boot "adivina" la configuración según tus dependencias

- @RestController vs @Controller
   → @RestController: retorna datos JSON (API REST)
   → @Controller: retorna nombre de vista HTML (aplicación web)

- ResponseEntity
   → Permite controlar el código HTTP de respuesta (200, 201, 404, etc.)

🔗 Recursos oficiales: