¿Qué es Spring Boot?
Spring Boot es una capa sobre el Spring Framework que elimina la configuración que Spring requería anteriormente. En lugar de archivos XML, configuración de servlets y un contenedor donde desplegar, escribes una clase con un método main, añades una dependencia starter y obtienes una aplicación en funcionamiento con un servidor embebido.
Tres conceptos hacen el trabajo pesado. Los Starters agrupan las dependencias de una funcionalidad en una sola línea. La Auto-configuration inspecciona el classpath y configura beans basándose en valores predeterminados coherentes. El servidor embebido significa que la aplicación es un proceso de Java normal; no hay nada que instalar ni donde desplegar.
El resultado es que Spring Boot se ha convertido en la forma predeterminada de construir servicios en la JVM, desde pequeñas API internas hasta grandes flotas de microservicios, manteniendo todo el poder del ecosistema de Spring en su base.
El ecosistema de Spring en un minuto
Spring no es una sola librería, sino una familia que comparte un modelo de programación:
- Spring Framework — el contenedor núcleo, la inyección de dependencias y la capa web.
- Spring Data — repositorios sobre almacenes SQL y NoSQL.
- Spring Security — autenticación y autorización.
- Spring Batch / Integration — procesamiento por lotes y mensajería.
- Spring Cloud — configuración, descubrimiento y resiliencia para sistemas distribuidos.
Debido a que están diseñados en conjunto, un proyecto puede adoptarlos uno a uno sin cambiar la forma en que está escrito. Esa consistencia es la verdadera razón por la cual los equipos estandarizan su desarrollo en Spring.
Starters y gestión de dependencias
Un starter es un conjunto seleccionado de dependencias que funcionan entre sí. Declaras la funcionalidad, no los jars individuales.
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
El equivalente en Gradle es igual de corto:
dependencies {
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "org.springframework.boot:spring-boot-starter-validation"
runtimeOnly "org.postgresql:postgresql"
}
El POM padre o el plugin de dependency-management fija las versiones compatibles por ti. Sobrescribe una versión solo cuando tengas un motivo justificado y deja que la plataforma gestione el resto.
La clase de aplicación y el servidor embebido
Toda aplicación de Spring Boot comienza aquí. Esta única clase es suficiente para lanzar un servidor web.
package com.example.blog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BlogApplication {
public static void main(String[] args) {
SpringApplication.run(BlogApplication.class, args);
}
}
@SpringBootApplication combina tres anotaciones: @Configuration, @EnableAutoConfiguration y @ComponentScan. El escaneo de componentes comienza en el paquete de esta clase y busca hacia abajo, razón por la cual la clase principal debe estar en la raíz de la estructura de tus paquetes.
Ejecutar ./mvnw spring-boot:run inicia un Tomcat embebido en el puerto 8080. Puedes cambiarlo por Jetty o Netty modificando una dependencia, y el código de la aplicación no se ve afectado.
La capa web anotada
Spring MVC mapea las solicitudes HTTP a métodos mediante anotaciones. @RestController combina @Controller y @ResponseBody, de modo que los objetos devueltos se serializan a JSON mediante Jackson.
@RestController
@RequestMapping("/api/posts")
public class PostController {
private final PostService service;
public PostController(PostService service) {
this.service = service;
}
@GetMapping
public List<PostResponse> list(
@RequestParam(defaultValue = "0") int page) {
return service.findPage(page);
}
@GetMapping("/{id}")
public PostResponse get(@PathVariable Long id) {
return service.findById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public PostResponse create(@Valid @RequestBody CreatePostRequest request) {
return service.create(request);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
service.delete(id);
}
}
@PathVariable lee desde la URL, @RequestParam desde la query string y @RequestBody deserializa el payload. Mantén los controladores ligeros: su función es traducir HTTP en una llamada a un servicio y traducir el resultado de vuelta. La lógica de dominio pertenece a la capa de servicio.
Inyección de dependencias y el contexto de la aplicación
El contexto de la aplicación es el contenedor que crea los objetos, llamados beans, y suministra sus dependencias. Tú declaras lo que una clase necesita y Spring decide cómo construirlo.
@Service
public class PostService {
private final PostRepository repository;
private final Clock clock;
public PostService(PostRepository repository, Clock clock) {
this.repository = repository;
this.clock = clock;
}
@Transactional
public PostResponse create(CreatePostRequest request) {
var post = new Post(request.title(), request.body(), clock.instant());
return PostResponse.from(repository.save(post));
}
}
Las anotaciones de estereotipo marcan las clases que deben gestionarse: @Component para beans genéricos, @Service para servicios de dominio, @Repository para acceso a datos y @RestController para endpoints web. Para cualquier cosa que no puedas anotar —como una clase de terceros o un cliente configurado— decláralo en una clase @Configuration.
@Configuration
class ClockConfig {
@Bean
Clock clock() {
return Clock.systemUTC();
}
}
Prioriza la inyección por constructor en todo momento. Esto hace que las dependencias sean explícitas, permite usar campos final y te permite instanciar la clase en una prueba unitaria sencilla sin necesidad de iniciar Spring.
Spring Data JPA y la base de datos
Las entidades mapean objetos Java a tablas, y los repositorios te brindan acceso a los datos sin necesidad de una implementación.
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 120)
private String title;
@Column(nullable = false)
private String body;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
private Author author;
protected Post() {
}
public Post(String title, String body, Author author) {
this.title = title;
this.body = body;
this.author = author;
}
// getters and domain methods
}
public interface PostRepository extends JpaRepository<Post, Long> {
List<Post> findByPublishedTrueOrderByCreatedAtDesc();
@Query("select p from Post p join fetch p.author where p.id = :id")
Optional<Post> findWithAuthor(Long id);
}
Spring Data deriva una consulta a partir del nombre del método, por lo que findByPublishedTrueOrderByCreatedAtDesc no necesita cuerpo. Usa @Query cuando el nombre derivado sea ilegible, y join fetch para evitar el problema de N+1 del lazy-loading. Mantén ddl-auto en validate en producción y gestiona el esquema con Flyway o Liquibase.
Configuración con application.yml y perfiles
La configuración reside en src/main/resources y puede ser sobrescrita mediante variables de entorno, lo que permite que el mismo jar se ejecute en cualquier lugar.
# src/main/resources/application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/blog
username: blog
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate
open-in-view: false
server:
port: 8080
Los perfiles permiten que una misma compilación se comporte de manera diferente según el entorno.
# src/main/resources/application-prod.yml
spring:
datasource:
url: ${DATABASE_URL}
logging:
level:
root: warn
Activa un perfil con --spring.profiles.active=prod o mediante la variable de entorno SPRING_PROFILES_ACTIVE. @ConfigurationProperties vincula un grupo de ajustes a una clase tipada, lo cual es más seguro que dispersar anotaciones @Value por todo el código base.
Validación
Las restricciones de Jakarta Bean Validation en un objeto de solicitud se aplican cuando el parámetro del controlador está anotado con @Valid.
public record CreatePostRequest(
@NotBlank @Size(max = 120) String title,
@NotBlank String body
) {
}
Un @RestControllerAdvice centraliza la respuesta de error para que cada endpoint devuelva la misma estructura.
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> onValidation(MethodArgumentNotValidException ex) {
return ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage,
(first, second) -> first));
}
}
Valida en el límite y permite que el dominio confíe en sus entradas. Combinado con un manejador de excepciones consistente, esto elimina las comprobaciones repetitivas de if (input == null) en cada servicio.
Spring Security en breve
Spring Security es una cadena de filtros situada delante de tu aplicación. En las versiones modernas de Spring Boot, se configura mediante un bean en lugar de usar XML.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));
return http.build();
}
}
Ese único bean requiere un JWT válido en cada endpoint, excepto en el de health check. Las reglas a nivel de método con @PreAuthorize añaden una autorización detallada donde sea necesario. La seguridad es un tema complejo: planea leer la documentación detenidamente antes de exponer cualquier cosa a internet.
Actuator: salud y métricas
Añadir el starter de Actuator expone endpoints operativos de forma gratuita.
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
curl localhost:8080/actuator/health
/actuator/health se integra con las sondas de liveness y readiness, y /actuator/metrics reporta estadísticas de la JVM, HTTP y del datasource. Expón solo lo que necesites y protege el resto; estos endpoints revelan mucha información sobre un sistema en ejecución.
Pruebas
@SpringBootTest inicia el contexto de la aplicación y sirve como base para las pruebas de integración. MockMvc pone a prueba la capa web sin abrir un puerto real.
@SpringBootTest
@AutoConfigureMockMvc
class PostControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void listsPosts() throws Exception {
mockMvc.perform(get("/api/posts"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title").value("Hello"));
}
}
Para obtener una respuesta más rápida, reduce el contexto únicamente a la capa que estás probando y utiliza mocks para los colaboradores.
@WebMvcTest(PostController.class)
class PostControllerSliceTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean
private PostService service;
@Test
void returnsNotFound() throws Exception {
given(service.findById(1L)).willThrow(new PostNotFoundException(1L));
mockMvc.perform(get("/api/posts/1"))
.andExpect(status().isNotFound());
}
}
Utiliza slices para los controladores y repositorios, y reserva el contexto completo para unas pocas pruebas end-to-end. Testcontainers te permite tener una base de datos real en las pruebas sin necesidad de hacer mocking de la persistencia.
Construcción y despliegue
El proceso de build genera un executable fat jar que contiene tus clases, dependencias y el servidor embebido.
./mvnw clean package
java -jar target/blog-0.0.1-SNAPSHOT.jar
Para contenedores, Buildpacks crean una imagen optimizada sin necesidad de un Dockerfile.
./mvnw spring-boot:build-image
La configuración se gestiona a través del entorno, por lo que el mismo artefacto pasa de staging a producción sin cambios. Añade un Dockerfile solo cuando necesites un control que Buildpacks no ofrezca, y prefiere un multi-stage build para mantener la imagen ligera.
Mejores prácticas
- Organiza los paquetes por funcionalidad (package by feature) para que cada dominio sea dueño de su propio controller, service y repository.
- Utiliza inyección por constructor y mantén los campos
final. - Devuelve DTOs desde los controllers en lugar de entidades JPA.
- Valida los objetos de petición con
@Validy gestiona los errores en un único@RestControllerAdvice. - Mantén
ddl-autoenvalidatey gestiona los cambios de esquema con Flyway o Liquibase. - Externaliza la configuración y utiliza perfiles en lugar de volver a compilar para cada entorno.
- Añade Actuator, luego expón y asegura únicamente los endpoints que realmente necesites.
Errores comunes
- Inyección de campos con
@Autowired, lo que oculta las dependencias y complica las pruebas. - Serializar entidades directamente, provocando la filtración de columnas internas o de lazy-loading.
- Permitir que los controladores acumulen lógica de negocio en lugar de delegarla a los servicios.
- Usar
ddl-auto: updateen producción, lo que provoca una divergencia respecto al esquema real. - Iniciar todo el contexto en cada prueba unitaria, ralentizando la suite de pruebas.
- Exponer todos los endpoints de Actuator a la internet pública.
- Añadir starters que no necesitas y pagar el coste de rendimiento al iniciar la aplicación.
Próximos pasos
Spring Boot enseña inyección de dependencias, autoconfiguración y diseño por capas; conceptos que se aplican mucho más allá de Java. Si te gusta el modelo de módulos e inyección, NestJS lo implementa en TypeScript, mientras que Express muestra la alternativa minimalista. La capa web anotada es, en última instancia, una REST API, y la mayoría de las aplicaciones persisten los datos en un almacenamiento relacional, por lo que PostgreSQL es el siguiente paso natural.