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.
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.
La forma estándar de inicializar un proyecto Spring Boot es a través de Spring Initializr. Al configurar un nuevo proyecto seleccionamos:
Dependencias principales:
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
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).
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.
El flujo de ejecución de una petición sigue estos pasos:
GET /api/v1/books).DispatcherServlet intercepta la petición entrante.@RestController y qué método están configurados para responder a esa ruta y verbo HTTP.JSON de la petición en un objeto Java (Record / DTO).JSON y lo envía al cliente dentro de la respuesta 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:
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);
}
}
Para exponer recursos como endpoints REST, creamos clases anotadas con @RestController y @RequestMapping.
@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).@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).
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<T>.
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<List<BookResponse>> getAll() {
List<BookResponse> books = bookService.findAll();
return ResponseEntity.ok(books); // Estado 200 OK
}
// GET /api/v1/books/12 -> 200 OK si existe
@GetMapping("/{id}")
public ResponseEntity<BookResponse> 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<BookResponse> 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<Void> delete(@PathVariable Long id) {
bookService.delete(id);
return ResponseEntity.noContent().build(); // Estado 204 No Content
}
}
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.