===== 10. Frameworks Web: Spring Boot y APIs REST ===== Hasta ahora hemos construido la arquitectura por capas (''Controller'', ''Service'', ''DAO/Repository'') instanciando y conectando las dependencias de forma manual. En este tema utilizaremos **Spring Boot**, el framework estándar de la industria Java, para delegar esta tarea a su contenedor de ''IoC'' y exponer nuestra aplicación como una API REST profesional. ==== 1. Introducción a Spring Boot ==== Un framework proporciona la infraestructura necesaria (enrutamiento HTTP, servidor web embebido, gestión de beans, filtros) para que el desarrollador se concentre únicamente en implementar la lógica de negocio. Crear una aplicación web tradicional en Java requería configurar archivos de despliegue complejos, compilar un archivo ''.war'' y desplegarlo manualmente en un servidor externo (como **Apache Tomcat**). Spring Boot simplifica este proceso integrando un servidor //Tomcat// embebido dentro del propio ejecutable ''.jar'' de la aplicación. === Creación del proyecto === La forma estándar de inicializar un proyecto Spring Boot es a través de [[https://start.spring.io/|Spring Initializr]]. Al configurar un nuevo proyecto seleccionamos: * **Gestor de dependencias**: Maven.\\ \\ * **Lenguaje**: Java.\\ \\ * **Versión de Spring Boot**: 4.x.\\ \\ * **Metadatos del proyecto**: Group ID y Artifact ID.\\ \\ * **Empaquetado**: Jar.\\ \\ * **Configuration**: Properties\\ \\ * **Versión de Java**: 25.\\ \\ Dependencias principales: * **Spring Web**: Incluye el servidor Tomcat embebido y todo el soporte para APIs REST.\\ \\ === Punto de entrada y configuración === La clase principal anotada con ''@SpringBootApplication'' es la encargada de arrancar el servidor embebido y la aplicación mediante el método ''main'': import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class BookstoreApiApplication { public static void main(String[] args) { SpringApplication.run(BookstoreApiApplication.class, args); } } Por defecto, el servidor web escucha en el puerto ''8080''. Para cambiar este comportamiento, basta con incluir la siguiente propiedad en el archivo ''src/main/resources/application.properties'': server.port=8081 ==== 2. Arquitectura Interna: Servlets y el patrón Front Controller ==== Para comprender cómo procesa //Spring Boot// las peticiones HTTP, es necesario entender la especificación base del desarrollo web en Java: los ''Servlets''. Un ''Servlet'' (definido en la especificación ''jakarta.servlet'') es una clase Java encargada de atender peticiones HTTP (''HttpServletRequest'') y generar respuestas HTTP (''HttpServletResponse''). === El patrón Front Controller (DispatcherServlet) === En lugar de requerir la creación de un ''Servlet'' independiente para cada ruta del sistema, //Spring Boot// implementa el patrón de diseño **Front Controller** a través de una clase central llamada ''DispatcherServlet''. @startuml top to bottom direction skinparam componentStyle rectangle package "Cliente" { [Navegador / Cliente HTTP] as Client } package "Servidor Web (Tomcat)" { [Cadena de Filtros (Filter Chain)] as Filters package "Spring Context (IoC Container)" { [DispatcherServlet] as DS [Handler Mapping] as HM [Http Message Converter] as HMC [BookController] as Ctrl [BookService] as Service } } ' Flujo principal descendente Client --> Filters : 1. Petición HTTP Filters --> DS : 2. Intercepta petición ' Consultas internas del DispatcherServlet DS <--> HM : 3. Asocia URI con Método DS <--> HMC : 4. Transforma JSON <-> Java DTO ' Continuación del flujo descendente DS --> Ctrl : 5. Ejecuta controlador Ctrl --> Service : 6. Invoca capa de negocio @enduml El flujo de ejecución de una petición sigue estos pasos: - **Petición del cliente**: El cliente envía una petición HTTP (por ejemplo, ''GET /api/v1/books'').\\ \\ - **Recepción en el Front Controller**: El ''DispatcherServlet'' intercepta la petición entrante.\\ \\ - **Enrutamiento (Handler Mapping)**: Consulta sus tablas internas para localizar qué ''@RestController'' y qué método están configurados para responder a esa ruta y verbo HTTP.\\ \\ - **Deserialización**: Transforma automáticamente el cuerpo ''JSON'' de la petición en un objeto Java (''Record'' / ''DTO'').\\ \\ - **Invocación**: Ejecuta el método del controlador pasándole los argumentos procesados.\\ \\ - **Serialización y Respuesta**: Toma el resultado devuelto por el controlador (un DTO o colección de DTOs), lo convierte al formato ''JSON'' y lo envía al cliente dentro de la respuesta HTTP.\\ \\ ==== 3. Interceptación de peticiones: Filtros HTTP ==== Antes de que una petición alcance el ''DispatcherServlet'' (y, por extensión, nuestros controladores), es posible interceptarla mediante **Filtros HTTP**. Los filtros forman una cadena de responsabilidad (**Filter Chain**) y se utilizan para procesar aspectos transversales a la aplicación, tales como: * Registro de logs de auditoría (tiempo de respuesta, dirección IP de origen).\\ \\ * Verificación de cabeceras de autenticación o seguridad.\\ \\ * Gestión de cabeceras CORS (Cross-Origin Resource Sharing).\\ \\ === Ejemplo: Creación de un filtro personalizado === Extendiendo de la clase ''OncePerRequestFilter'', garantizamos que el filtro se ejecute exactamente una vez por cada petición HTTP: import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @Component public class AuditLogFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { long startTime = System.currentTimeMillis(); // Permite que la petición continúe hacia el DispatcherServlet / Controller filterChain.doFilter(request, response); long duration = System.currentTimeMillis() - startTime; IO.printf("[%s] %s -> Estado: %d (%d ms)%n", request.getMethod(), request.getRequestURI(), response.getStatus(), duration); } } ==== 4. Construcción de APIs REST con Spring Boot ==== Para exponer recursos como endpoints REST, creamos clases anotadas con ''@RestController'' y ''@RequestMapping''. === Anotaciones de mapeo HTTP === * ''@RestController'': Define la clase como un controlador web REST e indica a Spring que el valor devuelto por sus métodos debe serializarse directamente en el cuerpo de la respuesta HTTP (habitualmente en formato ''JSON'').\\ \\ * ''@RequestMapping("/api/v1/books")'': Define la ruta base para todos los endpoints contenidos en esa clase.\\ \\ * ''@GetMapping'': Asocia el método a peticiones ''GET'' (lectura).\\ \\ * ''@PostMapping'': Asocia el método a peticiones ''POST'' (creación).\\ \\ * ''@PutMapping'': Asocia el método a peticiones ''PUT'' (actualización completa).\\ \\ * ''@DeleteMapping'': Asocia el método a peticiones ''DELETE'' (eliminación).\\ \\ === Extraer datos de la Petición === * ''@PathVariable'': Extrae valores incrustados en la propia URL (ejemplo: ''/api/v1/books/{id}'').\\ \\ * ''@RequestParam'': Extrae parámetros de consulta pasados tras el símbolo ? (ejemplo: ''/api/v1/books?page=2'').\\ \\ * ''@RequestBody'': Deserializa automáticamente el cuerpo JSON de la petición entrante en un objeto Java (''Record'' / ''DTO'').\\ \\ ==== 5. Gestión de Respuestas HTTP con ResponseEntity ==== En el diseño de APIs REST, el controlador debe devolver tanto los datos formateados (DTOs) como el código de estado HTTP correspondiente. Para lograr un control explícito sobre la respuesta se utiliza la clase ''ResponseEntity''. === Ejemplo práctico: BookController === import com.fpmislata.bookstoreapi.controller.dto.BookCreateRequest; import com.fpmislata.bookstoreapi.controller.dto.BookResponse; import com.fpmislata.bookstoreapi.service.BookService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/v1/books") public class BookController { private final BookService bookService; // Inyección de dependencias por constructor realizada automáticamente por Spring public BookController(BookService bookService) { this.bookService = bookService; } // GET /api/v1/books -> 200 OK con el listado @GetMapping public ResponseEntity> getAll() { List books = bookService.findAll(); return ResponseEntity.ok(books); // Estado 200 OK } // GET /api/v1/books/12 -> 200 OK si existe @GetMapping("/{id}") public ResponseEntity getById(@PathVariable Long id) { BookResponse book = bookService.findById(id); return ResponseEntity.ok(book); // Estado 200 OK } // POST /api/v1/books -> 201 Created con el objeto creado @PostMapping public ResponseEntity create(@RequestBody BookCreateRequest request) { BookResponse createdBook = bookService.create(request); return ResponseEntity.status(HttpStatus.CREATED).body(createdBook); // Estado 201 Created } // DELETE /api/v1/books/12 -> 204 No Content @DeleteMapping("/{id}") public ResponseEntity delete(@PathVariable Long id) { bookService.delete(id); return ResponseEntity.noContent().build(); // Estado 204 No Content } } ==== 6. Integración con el Contenedor IoC de Spring ==== Spring detecta y gestiona de forma automática el ciclo de vida de nuestras clases mediante el escaneo de anotaciones: ^ Anotación ^ Capa de la Arquitectura ^ Descripción ^ | ''@RestController ''| Presentación | Expone endpoints REST y serializa las respuestas en JSON. | | ''@Service'' | Negocio | Contiene la lógica de negocio del sistema. | | ''@Repository'' | Persistencia | Implementa el acceso a datos (DAOs / Repositorios). | | ''@Component'' | Genérica | Para componentes o utilidades transversales (Filtros, Mapeadores). | Al aplicar ''@Service'' en la capa de negocio y ''@Repository'' en la capa de persistencia, el contenedor de //IoC// de Spring resuelve e inyecta las dependencias automáticamente a través del constructor del ''@RestController'', eliminando por completo la necesidad de instanciarlas manualmente.