Módulo 18: Spring Boot
Objetivos del módulo
- Entender qué problema resuelve Spring Boot y qué añade sobre el Spring Framework que ya conoces
- Generar un proyecto con Spring Initializr desde la web, desde IntelliJ IDEA y desde la línea de órdenes
- Conocer la estructura de un proyecto Spring Boot y el papel de cada fichero
- Saber qué combina la anotación
@SpringBootApplicationy cómo arranca una aplicación - Elegir los starters adecuados y comprender cómo funciona la autoconfiguración y sus condiciones
@ConditionalOn... - Configurar la aplicación con
application.propertiesoapplication.yml: puerto, base de datos MySQL, JPA y logging - Crear tus propias propiedades tipadas con
@ConfigurationPropertiesy records - Separar la configuración por entornos con perfiles (
dev,prod…) - Usar SLF4J/Logback, DevTools y Actuator (
health,info,metrics,env) - Ejecutar la aplicación con
mvnw spring-boot:runy como JAR ejecutable, e interpretar el log de arranque
1. ¿Qué es Spring Boot?
En el módulo anterior montaste a mano un contenedor de Spring: declaraste beans con @Configuration y @Bean, configuraste un DataSource, un JdbcTemplate, un gestor de transacciones… Funcionaba, pero para una aplicación real tendrías que añadir además Hibernate, Spring MVC, un servidor web, Jackson para JSON, un motor de plantillas, logging, métricas… y elegir una versión de cada librería que sea compatible con las demás.
📘 Concepto: Spring Boot es un proyecto del ecosistema Spring que permite crear aplicaciones Spring listas para producción con la mínima configuración. No sustituye a Spring Framework: lo usa por debajo y lo configura por ti siguiendo unas opiniones razonables (opinionated defaults). Si una opción por defecto no te convence, la cambias con una propiedad o declarando tu propio bean.
Una analogía: Spring Framework es una cocina profesional con todos los utensilios; Spring Boot es esa misma cocina con el menú del día ya preparado. Puedes cocinar otra cosa, pero si quieres el menú estándar, solo tienes que sentarte.
Figura 1. Lo que haces tú con Spring Framework y lo que Spring Boot hace por ti.
Las piezas clave de Spring Boot, que estudiarás en este módulo, son:
| Pieza | Qué hace | Sección |
|---|---|---|
| Starters | Dependencias «paquete» con versiones probadas entre sí | 6 |
| Autoconfiguración | Crea los beans de infraestructura si detecta las librerías en el classpath | 7 |
| Servidor embebido | Tomcat dentro del JAR: java -jar y listo | 8 |
| Configuración externa | application.properties/YAML, variables de entorno, perfiles | 9-11 |
| Actuator | Endpoints de salud, métricas e información para operaciones | 14 |
| Plugin de Maven | Empaqueta un JAR ejecutable con todas las dependencias | 15 |
Versiones que usa el curso
Spring Boot publica una versión menor cada seis meses (mayo y noviembre). En octubre de 2026 la versión estable es Spring Boot 4.1 (en concreto, 4.1.1), que trae estas versiones de sus dependencias principales (las gestiona Boot, no tienes que escribirlas):
| Componente | Versión en Boot 4.1.1 |
|---|---|
| Java mínimo / del curso | 17 / 25 LTS |
| Spring Framework | 7.0.x |
| Jakarta EE | 11 (Servlet 6.1, Persistence 3.2, Validation 3.1) |
| Tomcat embebido | 11.0 |
| Hibernate ORM | 7.4 |
| Jackson | 3.1 (groupId tools.jackson.core, paquetes tools.jackson.databind…) |
| HikariCP | 7.0 |
| Thymeleaf | 3.1 |
| JUnit | 6 (Jupiter) |
Conector MySQL (com.mysql:mysql-connector-j) | 9.7 |
⚠️ Importante: Spring Boot 4 (noviembre de 2025) fue una versión mayor con cambios importantes respecto a Boot 3: se dividió en módulos más pequeños (
spring-boot-jdbc,spring-boot-webmvc,spring-boot-data-jpa…), algunos starters cambiaron de nombre (spring-boot-starter-webpasa a llamarsespring-boot-starter-webmvc), cada starter tiene su starter de pruebas (spring-boot-starter-webmvc-test…) y se adoptó Jackson 3. Si buscas ayuda en Internet, comprueba que el ejemplo sea de Boot 4: mucho material publicado es de Boot 2 o 3.
2. Preparar el entorno
Necesitas el JDK 25, un IDE (el curso usa IntelliJ IDEA) y Docker para la base de datos MySQL con el esquema EMPRESA que ya conoces de los módulos de JDBC e Hibernate.
Arrancar MySQL con Docker
Si no tienes ya el contenedor de los módulos anteriores, créalo así (usuario curso, contraseña curso, base de datos empresa):
docker run -d --name mysql-empresa -p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=root \
-e MYSQL_DATABASE=empresa \
-e MYSQL_USER=curso -e MYSQL_PASSWORD=curso \
mysql:8.4
Y carga los scripts de la carpeta recursos del curso (empresa_mysql_crear.sql y empresa_mysql_datos.sql):
docker exec -i mysql-empresa mysql -ucurso -pcurso empresa < empresa_mysql_crear.sql
docker exec -i mysql-empresa mysql -ucurso -pcurso empresa < empresa_mysql_datos.sql
docker exec mysql-empresa mysql -ucurso -pcurso empresa -e "select count(*) from empleados"
count(*)
24
💡 Consejo: Si quieres volver a dejar los datos como al principio después de hacer pruebas, ejecuta
empresa_mysql_borrar.sqly luego los otros dos scripts.
El proyecto del bloque: empresa-app
Durante los módulos 18 a 24 vas a construir una única aplicación, empresa-app, que crecerá módulo a módulo:
| Módulo | Qué añades a empresa-app |
|---|---|
| 18 | Proyecto base, configuración, perfiles, Actuator |
| 19 | Entidades JPA de EMPRESA y repositorios de Spring Data |
| 20 | API REST de productos, categorías, pedidos y empleados |
| 21 | Aplicación web con Thymeleaf: CRUD de empleados y departamentos |
| 22-24 | Seguridad, pruebas, despliegue y proyecto final |
Su organización por capas será esta (paquete base es.curso.empresa):
es.curso.empresa
├── config configuración (@Configuration, @ConfigurationProperties)
├── modelo entidades JPA: Departamento, Empleado, Categoria, Producto, Cliente, Pedido, LineaPedido
├── repositorio interfaces de Spring Data JPA
├── servicio lógica de negocio y transacciones
├── dto objetos de transferencia (records) y formularios
└── web
├── api controladores REST (@RestController)
└── mvc controladores web (@Controller + Thymeleaf)
3. Crear el proyecto con Spring Initializr
Spring Initializr es un servicio web (https://start.spring.io) que genera el esqueleto de un proyecto Spring Boot: el pom.xml con las dependencias elegidas, la clase principal, la configuración vacía y el wrapper de Maven. Lo usan por debajo la web, IntelliJ IDEA, VS Code y la CLI de Spring Boot: el resultado es siempre el mismo.
3.1. Desde la web
Abre https://start.spring.io y rellena el formulario con los datos de empresa-app:
Figura 2. Spring Initializr (start.spring.io) con los campos de empresa-app rellenos. Captura real.
| Campo | Valor | Explicación |
|---|---|---|
| Project | Maven | Herramienta de construcción (Gradle se estudió en el módulo 01) |
| Language | Java | También admite Kotlin y Groovy |
| Spring Boot | 4.1.1 | Elige siempre la última versión estable, nunca una SNAPSHOT ni una M (milestone) |
| Group | es.curso | Identificador de la organización (dominio al revés) |
| Artifact | empresa-app | Nombre del artefacto: será el nombre del JAR y de la carpeta |
| Package name | es.curso.empresa | Paquete raíz; Initializr propone es.curso.empresa-app y lo corregimos porque - no es válido en un paquete |
| Packaging | Jar | JAR ejecutable con servidor embebido (War solo si vas a desplegar en un Tomcat externo) |
| Configuration | Properties | Formato del fichero de configuración: .properties o YAML |
| Java | 25 | Versión de Java del proyecto (<java.version> del pom.xml) |
💡 Consejo: La web actual de Initializr ya no muestra los campos Name y Description; si los necesitas, puedes pasarlos en la URL o editarlos después en el
pom.xml. IntelliJ y la API sí los admiten.
Pulsa ADD DEPENDENCIES… (o Ctrl + B) y busca cada dependencia por su nombre:
Figura 3. Búsqueda de dependencias en Spring Initializr. Captura real.
Para empresa-app elegimos estas ocho (las usaremos a lo largo del bloque):
| Dependencia (nombre en Initializr) | Para qué |
|---|---|
| Spring Web | Spring MVC + Tomcat embebido: API REST y web |
| Spring Data JPA | Repositorios + Hibernate (módulo 19) |
| MySQL Driver | Conector JDBC de MySQL |
| H2 Database | Base de datos en memoria para desarrollo y pruebas |
| Validation | Bean Validation (Hibernate Validator) |
| Thymeleaf | Plantillas HTML (módulo 21) |
| Spring Boot Actuator | Salud, métricas, información |
| Spring Boot DevTools | Reinicio automático en desarrollo |
Antes de descargar, el botón EXPLORE (Ctrl + Espacio) te enseña el proyecto que se va a generar. Es muy útil para revisar el pom.xml:
Figura 4. Vista previa del proyecto (EXPLORE). Captura real.
Finalmente, GENERATE (Ctrl + Intro) descarga empresa-app.zip. Descomprímelo y ábrelo con tu IDE (en IntelliJ: File → Open y elige la carpeta).
3.2. Desde IntelliJ IDEA
IntelliJ IDEA incluye el asistente Spring Boot, que llama a start.spring.io por ti y abre directamente el proyecto. File → New → Project… y, en el panel izquierdo, el generador Spring Boot:
Figura 5. Esquema del asistente New Project de IntelliJ IDEA (generador Spring Boot), primer paso.
- Elige el generador Spring Boot (el campo Server URL indica que usará start.spring.io).
- Rellena Group, Artifact y Package name como en la web; Name y Location deciden el nombre y la carpeta del proyecto en IntelliJ.
- En JDK elige un JDK 25 instalado (o descárgalo desde el propio desplegable) y en Java la versión 25.
- Pulsa Next.
En el segundo paso eliges la versión de Spring Boot y las dependencias:
Figura 6. Esquema del asistente de IntelliJ IDEA, segundo paso: versión y dependencias.
- Versión de Spring Boot: 4.1.1.
- Marca las dependencias navegando por las categorías o con el buscador.
- Comprueba la lista Added dependencies.
- Create: IntelliJ genera el proyecto, lo abre e importa el
pom.xml.
3.3. Desde la línea de órdenes
Como Initializr es una API HTTP, puedes generar el proyecto con curl. Es la forma más rápida y reproducible (ideal para scripts o para clase):
curl https://start.spring.io/starter.zip \
-d type=maven-project -d language=java -d bootVersion=4.1.1 \
-d groupId=es.curso -d artifactId=empresa-app -d name=empresa-app \
-d packageName=es.curso.empresa -d packaging=jar -d javaVersion=25 \
-d dependencies=web,data-jpa,mysql,h2,validation,thymeleaf,actuator,devtools \
-o empresa-app.zip
unzip empresa-app.zip -d empresa-app
empresa-app/.gitattributes
empresa-app/.gitignore
empresa-app/.mvn/wrapper/maven-wrapper.properties
empresa-app/HELP.md
empresa-app/mvnw
empresa-app/mvnw.cmd
empresa-app/pom.xml
empresa-app/src/main/java/es/curso/empresa/EmpresaAppApplication.java
empresa-app/src/main/resources/application.properties
empresa-app/src/test/java/es/curso/empresa/EmpresaAppApplicationTests.java
Los identificadores de las dependencias (web, data-jpa, mysql…) se consultan con curl https://start.spring.io (devuelve una ayuda en texto con todas las opciones). Si instalas la CLI de Spring Boot (por ejemplo con SDKMAN: sdk install springboot), tienes el atajo spring init:
spring init --build=maven --java-version=25 --group-id=es.curso --artifact-id=empresa-app \
--package-name=es.curso.empresa \
--dependencies=web,data-jpa,mysql,h2,validation,thymeleaf,actuator,devtools empresa-app
✏️ Ejercicio: Genera con
curlun proyectohola-bootcon la única dependenciaweby compara supom.xmlcon el deempresa-app. ¿Qué starters de pruebas aparecen en cada uno?
4. Estructura del proyecto generado
Figura 7. Estructura del proyecto generado por Spring Initializr.
Es la estructura estándar de Maven que ya conoces (módulo 01) con tres añadidos: el Maven Wrapper, la clase principal y las carpetas static/ y templates/ de recursos web (Initializr las crea vacías si eliges Spring Web o Thymeleaf).
El pom.xml
Este es el pom.xml que genera Initializr, resumido (se han quitado los bloques vacíos <licenses>, <developers> y <scm>):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
<modelVersion>4.0.0</modelVersion>
<!-- ① El padre: hereda versiones de cientos de librerías y la configuración de plugins -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>es.curso</groupId>
<artifactId>empresa-app</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>empresa-app</name>
<description>Aplicación de gestión de EMPRESA</description>
<properties>
<java.version>25</java.version> <!-- ② versión de Java -->
</properties>
<dependencies>
<!-- ③ Starters: fíjate en que NO llevan <version> -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-h2console</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</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-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<!-- ④ Solo en desarrollo: optional + runtime, no se empaqueta en producción -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- ⑤ Drivers JDBC: solo hacen falta al ejecutar -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- ⑥ Un starter de pruebas por cada starter (novedad de Boot 4) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa-test</artifactId>
<scope>test</scope>
</dependency>
<!-- ... thymeleaf-test, validation-test y webmvc-test ... -->
</dependencies>
<build>
<plugins>
<!-- ⑦ Empaqueta el JAR ejecutable y añade el objetivo spring-boot:run -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
📘 Concepto:
spring-boot-starter-parenthereda despring-boot-dependencies, un BOM (Bill of Materials) que fija la versión de unas 200 librerías probadas juntas (Hibernate, Jackson, Tomcat, el conector de MySQL…). Por eso tus dependencias no llevan<version>: si mañana actualizas Boot de 4.1.1 a 4.1.2, todas se actualizan de forma coherente cambiando una sola línea.
Si necesitas otra versión de una librería gestionada, sobrescribe su propiedad (los nombres están en la documentación de Boot), por ejemplo:
<properties>
<java.version>25</java.version>
<mysql.version>9.7.0</mysql.version>
</properties>
El Maven Wrapper
mvnw (Linux/macOS) y mvnw.cmd (Windows) descargan la versión exacta de Maven indicada en .mvn/wrapper/maven-wrapper.properties (Maven 3.9.16 en el proyecto generado). Así cualquier compañero, o el servidor de integración continua, compila con la misma versión sin instalar nada:
./mvnw -v # Linux/macOS
mvnw.cmd -v # Windows (o .\mvnw.cmd en PowerShell)
⚠️ Importante: Sube
mvnw,mvnw.cmdy la carpeta.mvnal repositorio Git. El.gitignoregenerado ya excluyetarget/y los ficheros del IDE.
5. La clase principal y @SpringBootApplication
package es.curso.empresa;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class EmpresaAppApplication {
public static void main(String[] args) {
SpringApplication.run(EmpresaAppApplication.class, args);
}
}
Parece poca cosa, pero @SpringBootApplication es una anotación compuesta que equivale a tres:
| Anotación | Qué hace | Lo que hacías en el módulo 17 |
|---|---|---|
@SpringBootConfiguration | Marca la clase como @Configuration (puede declarar @Bean) | @Configuration |
@EnableAutoConfiguration | Activa la autoconfiguración (sección 7) | Declarar a mano DataSource, TransactionManager… |
@ComponentScan | Busca @Component, @Service, @Repository, @Controller… en este paquete y sus subpaquetes | @ComponentScan("es.curso...") |
Y SpringApplication.run(...):
- Prepara el
Environmentleyendo propiedades, variables de entorno, argumentos y perfiles. - Muestra el banner.
- Crea el
ApplicationContextadecuado (uno web, porque hay Spring MVC en el classpath). - Registra tus beans (escaneo) y los autoconfigurados, y los crea.
- Arranca el servidor embebido (Tomcat) y ejecuta los
CommandLineRunner/ApplicationRunnerque hayas declarado.
⚠️ Importante: Deja la clase principal en el paquete raíz (
es.curso.empresa) y todo lo demás en subpaquetes (es.curso.empresa.servicio,...web.api…). Si pones un controlador enes.curso.otro, el escaneo no lo verá y obtendrás un 404 sin ningún error en el log. Es un fallo muy típico.
Puedes personalizar el arranque creando el objeto SpringApplication tú mismo:
public static void main(String[] args) {
SpringApplication app = new SpringApplication(EmpresaAppApplication.class);
app.setBannerMode(Banner.Mode.OFF); // sin banner
app.setAdditionalProfiles("dev"); // perfil siempre activo
app.run(args);
}
En empresa-app añadimos a la clase principal @ConfigurationPropertiesScan, que verás en la sección 10.
6. Starters
📘 Concepto: Un starter es una dependencia que no contiene código propio: solo agrupa las librerías necesarias para una funcionalidad, con versiones compatibles, y el módulo de autoconfiguración correspondiente. Añadir
spring-boot-starter-data-jpatrae Spring Data JPA, Hibernate, HikariCP, Spring JDBC, la API de Jakarta Persistence y sus autoconfiguraciones.
Puedes ver lo que trae cada starter con el árbol de dependencias de Maven:
./mvnw dependency:tree -Dincludes=org.hibernate.orm,com.zaxxer
Los starters oficiales siguen el patrón spring-boot-starter-* (los de terceros, *-spring-boot-starter, por ejemplo springdoc-openapi-starter-webmvc-ui). Los principales en Boot 4:
| Starter | Qué incluye | Módulo |
|---|---|---|
spring-boot-starter-webmvc | Spring MVC, Tomcat embebido, Jackson 3 | 20, 21 |
spring-boot-starter-webflux | Programación web reactiva con Netty | — |
spring-boot-starter-data-jpa | Spring Data JPA, Hibernate 7, HikariCP | 19 |
spring-boot-starter-jdbc | JdbcTemplate, JdbcClient, HikariCP (sin JPA) | 17 |
spring-boot-starter-validation | Jakarta Bean Validation con Hibernate Validator | 20, 21 |
spring-boot-starter-thymeleaf | Motor de plantillas Thymeleaf | 21 |
spring-boot-starter-security | Spring Security | 22 |
spring-boot-starter-actuator | Endpoints de monitorización y Micrometer | 18 |
spring-boot-starter-restclient | RestClient y RestTemplate autoconfigurados | 20 |
spring-boot-starter-mail | Envío de correo | — |
spring-boot-starter-cache | Abstracción de caché (@Cacheable) | — |
spring-boot-starter-flyway | Migraciones de base de datos con Flyway | — |
spring-boot-starter-test | JUnit 6, AssertJ, Mockito, Spring Test | 23 |
spring-boot-starter-tomcat / -jetty | Servidor embebido (Tomcat por defecto) | 18 |
spring-boot-starter-logging | SLF4J + Logback (lo traen todos los starters) | 18 |
💡 Consejo:
spring-boot-starter-websigue existiendo en Boot 4, pero su descripción en Maven Central dice «deprecated in favor of spring-boot-starter-webmvc». En proyectos nuevos usa siemprespring-boot-starter-webmvc, que es lo que genera Initializr.
7. Autoconfiguración
Es la «magia» de Spring Boot, y conviene entenderla para que deje de ser magia.
📘 Concepto: La autoconfiguración es un conjunto de clases
@AutoConfiguration(unas@Configurationespeciales) que Spring Boot incluye en sus módulos. Cada una declara los beans de una tecnología (unDataSource, unEntityManagerFactory, unObjectMapper…) pero solo si se cumplen unas condiciones: que cierta clase esté en el classpath, que no hayas definido tú ese bean, que una propiedad tenga cierto valor…
Figura 8. Cómo decide Spring Boot qué beans crear.
Cómo funciona
@EnableAutoConfigurationbusca en cada JAR del classpath el ficheroMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, que lista sus clases de autoconfiguración. Por ejemplo, el del módulospring-boot-jdbc-4.1.1.jarcontiene:
org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration
org.springframework.boot.jdbc.autoconfigure.DataSourceInitializationAutoConfiguration
org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration
org.springframework.boot.jdbc.autoconfigure.JdbcClientAutoConfiguration
org.springframework.boot.jdbc.autoconfigure.JdbcTemplateAutoConfiguration
org.springframework.boot.jdbc.autoconfigure.JndiDataSourceAutoConfiguration
org.springframework.boot.jdbc.autoconfigure.XADataSourceAutoConfiguration
org.springframework.boot.jdbc.autoconfigure.health.DataSourceHealthContributorAutoConfiguration
org.springframework.boot.jdbc.autoconfigure.metrics.DataSourcePoolMetricsAutoConfiguration
- Para cada candidata evalúa sus condiciones. Si todas se cumplen, procesa la clase como una
@Configurationmás; si no, la descarta sin crear nada. - Los beans creados leen su configuración de propiedades (
spring.datasource.*,spring.jpa.*…), que tú controlas desdeapplication.properties.
💡 Consejo: En Boot 4 cada tecnología tiene su propio módulo con su propio fichero
.imports(antes casi todo estaba en un únicospring-boot-autoconfigure.jar). La consecuencia práctica es buena: si no añades el starter, ni siquiera se evalúan sus autoconfiguraciones, y el arranque es más rápido.
Las condiciones @ConditionalOn…
| Anotación | La configuración se aplica si… | Ejemplo típico |
|---|---|---|
@ConditionalOnClass | una clase está en el classpath | DataSource, EntityManager |
@ConditionalOnMissingClass | una clase no está | Elegir entre Tomcat y Jetty |
@ConditionalOnBean | existe ya un bean de un tipo | JPA necesita un DataSource |
@ConditionalOnMissingBean | no existe un bean de ese tipo | El tuyo tiene prioridad |
@ConditionalOnSingleCandidate | hay exactamente un bean candidato | Un solo DataSource |
@ConditionalOnProperty | una propiedad tiene un valor | spring.h2.console.enabled=true |
@ConditionalOnWebApplication | la aplicación es web (servlet o reactiva) | Spring MVC, Tomcat |
@ConditionalOnResource | existe un recurso | logback-spring.xml |
@ConditionalOnExpression | una expresión SpEL es verdadera | Casos avanzados |
La que más te afecta es @ConditionalOnMissingBean: casi todos los beans autoconfigurados la llevan, de modo que si declaras tú un bean del mismo tipo, Boot se retira. Por ejemplo, Boot crea un LocaleResolver por defecto (basado en la cabecera Accept-Language), pero en el módulo 21 declararemos el nuestro, con cookie, y Boot dejará de crear el suyo:
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver resolver = new CookieLocaleResolver("idioma");
resolver.setDefaultLocale(Locale.of("es", "ES"));
return resolver;
}
Tus propias clases también pueden usar estas condiciones. Por ejemplo, un bean que solo existe si activas una propiedad:
@Configuration
public class AvisosConfig {
@Bean
@ConditionalOnProperty(name = "empresa.avisos.activos", havingValue = "true")
public ServicioAvisos servicioAvisos() {
return new ServicioAvisos();
}
}
Excluir una autoconfiguración
Si una autoconfiguración te estorba (por ejemplo, tienes el driver en el classpath pero aún no quieres base de datos), exclúyela:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
o por propiedad, sin tocar el código:
spring.autoconfigure.exclude=org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration
El informe de condiciones (–debug)
Para saber qué autoconfiguraciones se han aplicado y por qué, arranca la aplicación con --debug (o pon debug=true en application.properties). Al final del arranque aparece el CONDITIONS EVALUATION REPORT, con las coincidencias positivas (Positive matches), las negativas (Negative matches) y las exclusiones. Este es un extracto real de empresa-app arrancada con el perfil h2:
java -jar target/empresa-app-0.0.1-SNAPSHOT.jar --spring.profiles.active=h2 --debug
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches:
-----------------
DataSourceAutoConfiguration matched:
- @ConditionalOnClass found required classes 'javax.sql.DataSource', 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType' (OnClassCondition)
- @ConditionalOnMissingBean (types: io.r2dbc.spi.ConnectionFactory; SearchStrategy: all) did not find any beans (OnBeanCondition)
DataSourceConfiguration.Hikari matched:
- @ConditionalOnClass found required class 'com.zaxxer.hikari.HikariDataSource' (OnClassCondition)
...
H2ConsoleAutoConfiguration matched:
- @ConditionalOnClass found required class 'org.h2.server.web.JakartaWebServlet' (OnClassCondition)
- found 'session' scope (OnWebApplicationCondition)
- @ConditionalOnBooleanProperty (spring.h2.console.enabled=true) matched (OnPropertyCondition)
HibernateJpaAutoConfiguration matched:
- @ConditionalOnClass found required classes 'org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean', 'jakarta.persistence.EntityManager', 'org.hibernate.engine.spi.SessionImplementor' (OnClassCondition)
HibernateJpaConfiguration matched:
- @ConditionalOnSingleCandidate (types: javax.sql.DataSource; SearchStrategy: all) found a single bean 'dataSource' (OnBeanCondition)
JacksonAutoConfiguration matched:
- @ConditionalOnClass found required class 'tools.jackson.databind.json.JsonMapper' (OnClassCondition)
...
TomcatServletWebServerAutoConfiguration matched:
- @ConditionalOnClass found required classes 'jakarta.servlet.ServletRequest', 'org.apache.catalina.startup.Tomcat', 'org.apache.coyote.UpgradeProtocol', 'org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory' (OnClassCondition)
- found 'session' scope (OnWebApplicationCondition)
...
Negative matches:
-----------------
...
DataSourceAutoConfiguration.EmbeddedDatabaseConfiguration:
Did not match:
- EmbeddedDataSource spring.datasource.url is set (DataSourceAutoConfiguration.EmbeddedDatabaseCondition)
...
WebMvcAutoConfiguration.EnableWebMvcConfiguration#localeResolver:
Did not match:
- @ConditionalOnMissingBean (names: localeResolver; SearchStrategy: all) found beans named localeResolver (OnBeanCondition)
WebMvcAutoConfiguration.ProblemDetailsErrorHandlingConfiguration:
Did not match:
- @ConditionalOnBooleanProperty (spring.mvc.problemdetails.enabled=true) did not find property 'spring.mvc.problemdetails.enabled' (OnPropertyCondition)
...
Exclusions:
-----------
None
El informe completo tiene más de 1.500 líneas (262 configuraciones aplicadas y 136 descartadas en empresa-app). Fíjate en tres casos:
HibernateJpaConfigurationse aplica porque hay un únicoDataSource(@ConditionalOnSingleCandidate).EmbeddedDatabaseConfigurationno se aplica porque hemos escritospring.datasource.url: si no la escribiéramos, Boot crearía una base de datos H2 en memoria por su cuenta.- El
localeResolverde Boot no se crea porque ya existe el nuestro (@ConditionalOnMissingBean), justo lo que explicábamos arriba.
💡 Consejo: El mismo informe está disponible en JSON en el endpoint de Actuator
/actuator/conditions(lo hemos expuesto enempresa-app), muy cómodo para buscar conCtrl + Fen el navegador.
8. El servidor embebido
Con spring-boot-starter-webmvc la aplicación lleva Tomcat 11 dentro del JAR. No hay que instalar ningún servidor ni desplegar un WAR: la aplicación es el servidor. Las opciones más habituales se configuran con propiedades server.*:
server.port=8080 # puerto (0 = uno libre al azar, útil en pruebas)
server.servlet.context-path=/empresa # prefijo de todas las URL (por defecto, ninguno)
server.shutdown=graceful # termina las peticiones en curso antes de parar (por defecto en Boot 4)
server.tomcat.threads.max=200 # hilos máximos de Tomcat
server.error.include-message=never # no mostrar mensajes de excepción en los errores
Cambiar de Tomcat a Jetty es solo cuestión de dependencias, gracias a @ConditionalOnClass:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
💡 Consejo: Si al arrancar ves
Web server failed to start. Port 8080 was already in use, otro programa ocupa el puerto (a menudo, una ejecución anterior de tu propia aplicación que no paraste). Para la otra instancia o arranca con--server.port=8081.
9. Configuración: application.properties y application.yml
Toda la configuración de Spring Boot se hace con propiedades clave-valor. Hay más de mil documentadas (busca «Common Application Properties» en la documentación de Boot), y el IDE las autocompleta. Pueden escribirse en dos formatos equivalentes.
El mismo ejemplo en los dos formatos
src/main/resources/application.properties:
spring.application.name=empresa-app
server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/empresa
spring.datasource.username=curso
spring.datasource.password=curso
spring.jpa.open-in-view=false
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.SQL=debug
logging.level.es.curso.empresa=debug
src/main/resources/application.yml:
spring:
application:
name: empresa-app
datasource:
url: jdbc:mysql://localhost:3306/empresa
username: curso
password: curso
jpa:
open-in-view: false
hibernate:
ddl-auto: validate
properties:
hibernate:
format_sql: true
server:
port: 8080
logging:
level:
org.hibernate.SQL: debug
es.curso.empresa: debug
.properties | .yml | |
|---|---|---|
| Sintaxis | clave.completa=valor | Jerarquía por sangría (espacios, nunca tabuladores) |
| Repetición de prefijos | Sí | No |
| Listas | lista[0]=a, lista[1]=b o lista=a,b | - a / - b |
| Varios documentos en un fichero | #--- | --- |
| Codificación | ISO-8859-1 (tildes como \u00f3) | UTF-8 |
| Riesgo típico | Ninguno especial | Un espacio mal puesto cambia el significado |
⚠️ Importante: Usa un solo formato por proyecto. Si existen los dos ficheros, Boot lee ambos y, ante una misma clave, gana el
.properties: es una fuente de confusión segura. En el curso usaremos.properties, que es lo que genera Initializr.
⚠️ Importante: Spring Boot lee los ficheros
.propertiesen ISO-8859-1. Si escribesinfo.app.descripcion=Gestión de la base de datos EMPRESAen UTF-8, el endpoint/actuator/infodevuelve literalmente"Gestión de la base de datos EMPRESA"(lo hemos comprobado). Escribe los caracteres no ASCII comoGesti\u00f3no usa YAML. Los comentarios con tilde no dan problemas.
Las propiedades que más vas a usar
| Propiedad | Para qué | Valor en empresa-app |
|---|---|---|
spring.application.name | Nombre de la aplicación (sale en el log) | empresa-app |
server.port | Puerto HTTP | 8080 |
spring.datasource.url / username / password | Conexión JDBC (Hikari) | MySQL empresa |
spring.datasource.hikari.maximum-pool-size | Tamaño del pool (módulo 13) | 10 (defecto) / 20 en prod |
spring.jpa.hibernate.ddl-auto | Qué hace Hibernate con el esquema: none, validate, update, create, create-drop | validate |
spring.jpa.open-in-view | Mantener la sesión JPA abierta durante la vista | false |
spring.jpa.show-sql | Mostrar el SQL por consola (sin formato) | perfil h2 |
logging.level.<paquete> | Nivel de log de un paquete | ver sección 12 |
spring.profiles.active / default | Perfiles activos / por defecto | dev por defecto |
📘 Concepto:
spring.jpa.hibernate.ddl-auto=validatehace que Hibernate compruebe al arrancar que las entidades coinciden con las tablas de EMPRESA y falle si falta una columna o un tipo no encaja. Como el esquema lo crean nuestros scripts SQL, nunca dejamos que Hibernate lo modifique (updateocreateestán bien para prototipos, jamás contra una base de datos con datos reales).
Variables y valores por defecto
Un valor puede referenciar otra propiedad o una variable de entorno con ${...}, y llevar un valor por defecto tras los dos puntos. Así queda el perfil dev de empresa-app:
spring.datasource.url=${DB_URL:jdbc:mysql://localhost:3306/empresa}
spring.datasource.username=${DB_USER:curso}
spring.datasource.password=${DB_PASSWORD:curso}
Si defines la variable de entorno DB_URL, se usa; si no, se usa jdbc:mysql://localhost:3306/empresa. Así el mismo código funciona en tu portátil, en el de tu compañero y en un servidor.
¿Qué valor gana?
Una misma propiedad puede venir de muchos sitios. Spring Boot los ordena por prioridad:
Figura 9. Orden de prioridad de las fuentes de configuración (simplificado).
Gracias al enlace relajado (relaxed binding), la propiedad spring.datasource.url se puede escribir como variable de entorno SPRING_DATASOURCE_URL (mayúsculas, puntos por guiones bajos, sin guiones). Ejemplos:
java -jar target/empresa-app-0.0.1-SNAPSHOT.jar --server.port=9090 # argumento: gana a todo
SERVER_PORT=9090 java -jar target/empresa-app-0.0.1-SNAPSHOT.jar # variable de entorno (Linux/macOS)
$env:SERVER_PORT = "9090"; java -jar target\empresa-app-0.0.1-SNAPSHOT.jar # variable de entorno (PowerShell)
💡 Consejo: Un fichero
application.propertiescolocado fuera del JAR, en la carpeta desde la que lo ejecutas (o en./config/), tiene prioridad sobre el empaquetado. Es la forma clásica de cambiar la configuración en un servidor sin recompilar.
10. Propiedades propias con @ConfigurationProperties
Para leer una propiedad suelta puedes usar @Value("${empresa.nombre}"), como en el módulo 17. Pero cuando tienes varias relacionadas, lo profesional es agruparlas en una clase tipada y validada. Desde Spring Boot 3 basta con declararla como record (ya no hace falta @ConstructorBinding), y los records encajan perfectamente porque la configuración no debería cambiar en tiempo de ejecución.
En application.properties de empresa-app:
empresa.nombre=EMPRESA S.A.
empresa.email-contacto=info@empresa.es
empresa.paginacion.tamano-por-defecto=10
empresa.paginacion.tamano-maximo=50
Y el record que las recibe (es.curso.empresa.config.EmpresaProperties):
package es.curso.empresa.config;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.validation.annotation.Validated;
/**
* Propiedades propias de la aplicación (prefijo "empresa" en application.properties).
* Al ser un record, Spring las enlaza por constructor y son inmutables.
*/
@Validated
@ConfigurationProperties(prefix = "empresa")
public record EmpresaProperties(
@NotBlank String nombre,
@Email String emailContacto,
@DefaultValue Paginacion paginacion) {
/** Grupo anidado: empresa.paginacion.* */
public record Paginacion(
@DefaultValue("10") @Min(1) int tamanoPorDefecto,
@DefaultValue("50") @Max(200) int tamanoMaximo) { }
}
Fíjate en varios detalles:
email-contacto(kebab-case, la forma recomendada en ficheros) se enlaza con el componenteemailContacto. También valdríanemailContactooEMPRESA_EMAILCONTACTOcomo variable de entorno.@DefaultValueda valores por defecto; enPaginacionsin valor (@DefaultValuevacío) hace que el grupo se cree aunque no haya ninguna propiedadempresa.paginacion.*.@Validatedaplica Bean Validation al arrancar: si la configuración es incorrecta, la aplicación no arranca (mucho mejor que fallar a las tres de la madrugada).
Para registrarla, en la clase principal:
@SpringBootApplication
@ConfigurationPropertiesScan // registra los @ConfigurationProperties del proyecto
public class EmpresaAppApplication { ... }
Y se inyecta como cualquier bean, por constructor. Por ejemplo, la configuración de OpenAPI del módulo 20 la usa para el título de la documentación:
@Bean
public OpenAPI empresaOpenApi(EmpresaProperties props) {
return new OpenAPI().info(new Info()
.title("API de " + props.nombre())
.contact(new Contact().email(props.emailContacto())));
}
¿Qué ocurre si alguien pone un email incorrecto? Lo hemos probado arrancando con --empresa.email-contacto=correo-sin-arroba:
***************************
APPLICATION FAILED TO START
***************************
Description:
Binding to target es.curso.empresa.config.EmpresaProperties failed:
Property: empresa.emailContacto
Value: "correo-sin-arroba"
Origin: "empresa.email-contacto" from property source "commandLineArgs"
Reason: must be a well-formed email address
Action:
Update your application's configuration
El informe dice qué propiedad falla, qué valor tiene, de dónde viene (aquí, de los argumentos de la línea de órdenes, commandLineArgs) y por qué. El mensaje de la razón sale en inglés porque es el mensaje por defecto de @Email en Hibernate Validator y el contenedor no tiene configurado el idioma español; podrías poner tu propio texto con @Email(message = "...").
💡 Consejo: Añade la dependencia
spring-boot-configuration-processor(en Initializr: Configuration Processor) y el IDE autocompletará y documentará también tus propiedadesempresa.*, igual que las de Spring.
@Value | @ConfigurationProperties | |
|---|---|---|
| Uso | Una propiedad suelta | Un grupo de propiedades relacionadas |
| Tipado | Conversión básica | Completo: listas, mapas, Duration, DataSize, objetos anidados |
| Validación | No | Sí, con @Validated |
| Enlace relajado | Limitado | Completo |
| Metadatos para el IDE | No | Sí (con el configuration processor) |
11. Perfiles
Tu portátil, el servidor de pruebas y producción necesitan configuraciones distintas: otra base de datos, otros niveles de log, otras credenciales. Los perfiles permiten tener ficheros específicos que se suman al común:
src/main/resources/
├── application.properties común a todos los entornos
├── application-dev.properties perfil dev: MySQL local y trazas SQL
├── application-prod.properties perfil prod: todo por variables de entorno
└── application-h2.properties perfil h2: base de datos en memoria (módulo 19)
Cuando un perfil está activo, Spring carga application.properties y después application-<perfil>.properties, cuyos valores ganan en caso de conflicto.
application-dev.properties de empresa-app:
# ==========================================================
# Perfil dev: MySQL local (Docker) y trazas detalladas
# ==========================================================
spring.datasource.url=${DB_URL:jdbc:mysql://localhost:3306/empresa}
spring.datasource.username=${DB_USER:curso}
spring.datasource.password=${DB_PASSWORD:curso}
# Ver el SQL que genera Hibernate (formateado) y los parámetros
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.SQL=debug
logging.level.org.hibernate.orm.jdbc.bind=trace
logging.level.es.curso.empresa=debug
application-prod.properties:
# ==========================================================
# Perfil prod: todo por variables de entorno, sin valores por defecto
# ==========================================================
spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASSWORD}
spring.datasource.hikari.maximum-pool-size=20
spring.jpa.show-sql=false
logging.level.root=info
logging.level.es.curso.empresa=info
logging.file.name=logs/empresa-app.log
# En producción se exponen menos endpoints y sin detalles
management.endpoints.web.exposure.include=health,info
management.endpoint.health.show-details=never
⚠️ Importante: Nunca escribas contraseñas reales en ficheros que se suben a Git. En
prodno hay valores por defecto a propósito: si falta la variable, la aplicación no arranca, que es exactamente lo que queremos. Esto es lo que ocurre si arrancas el perfilprodsin definirDB_URL(salida real, recortada):
java -jar target/empresa-app-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
2026-10-04T15:28:18.923+02:00 INFO 1 --- [empresa-app] [ main] es.curso.empresa.EmpresaAppApplication : The following 1 profile is active: "prod"
...
2026-10-04T15:33:45.963+02:00 ERROR 1 --- [empresa-app] [ main] o.s.boot.SpringApplication : Application run failed
...
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/jdbc/autoconfigure/DataSourceConfiguration$Hikari.class]: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception with message: 'url' must start with "jdbc"
...
Caused by: java.lang.IllegalArgumentException: 'url' must start with "jdbc"
at org.springframework.util.Assert.isTrue(Assert.java:117) ~[spring-core-7.0.9.jar!/:7.0.9]
at org.springframework.boot.jdbc.DatabaseDriver.fromJdbcUrl(DatabaseDriver.java:316) ~[spring-boot-jdbc-4.1.1.jar!/:4.1.1]
Fíjate en la causa: como DB_URL no existe y no tiene valor por defecto, Spring deja el texto ${DB_URL} tal cual como URL, y el pool lo rechaza porque no empieza por jdbc. Es un error algo críptico, pero cumple su función: sin configuración, producción no arranca.
Activar perfiles
| Forma | Ejemplo |
|---|---|
| Perfil por defecto (si no se activa ninguno) | spring.profiles.default=dev en application.properties |
| Argumento | java -jar app.jar --spring.profiles.active=prod |
| Variable de entorno | SPRING_PROFILES_ACTIVE=prod |
| Propiedad de la JVM | java -Dspring.profiles.active=prod -jar app.jar |
| Maven | ./mvnw spring-boot:run -Dspring-boot.run.profiles=h2 |
| IntelliJ | Run → Edit Configurations → Active profiles |
| Varios a la vez | --spring.profiles.active=dev,demo |
En empresa-app usamos spring.profiles.default=dev: si no indicas nada, se activa dev (el log lo dice: No active profile set, falling back to 1 default profile: "dev"), y cualquier spring.profiles.active lo sustituye.
💡 Consejo:
spring.profiles.activedentro deapplication.propertiestambién funciona, pero entonces es fácil olvidarse de que está ahí y subir a producción condevactivado.spring.profiles.defaultexpresa mejor la intención: «si no me dices nada, estoy en desarrollo».
Beans que solo existen en un perfil
@Configuration
public class DatosDemoConfig {
@Bean
@Profile("dev") // también admite expresiones: "!prod", "dev | h2"
public CommandLineRunner saludoDesarrollo() {
return args -> System.out.println(">>> Arrancado en modo desarrollo");
}
}
12. Logging con SLF4J y Logback
Spring Boot usa SLF4J como fachada y Logback como implementación (los trae spring-boot-starter-logging, incluido en todos los starters). Ya no se usa System.out.println: el log tiene niveles, marca de tiempo, hilo, clase, y se puede enviar a ficheros o sistemas centralizados.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service
public class EmpleadoService {
private static final Logger log = LoggerFactory.getLogger(EmpleadoService.class);
public Empleado guardar(EmpleadoForm form) {
...
log.info("Empleado guardado: {}", guardado); // {} = marcador, sin concatenar
log.debug("Datos del formulario: {}", form); // solo si el nivel es DEBUG o más fino
...
}
}
⚠️ Importante: Usa siempre los marcadores
{}en lugar de concatenar ("Empleado " + e). Con marcadores, si el nivel está desactivado, el mensaje ni siquiera se construye. Y nunca escribas en el log contraseñas ni datos personales sensibles.
Los niveles, de más a menos detallado, son TRACE, DEBUG, INFO, WARN y ERROR. El nivel por defecto es INFO. Se cambian con logging.level.<paquete o clase>:
logging.level.root=warn # todo a WARN...
logging.level.es.curso.empresa=debug # ...salvo nuestro código
logging.level.org.hibernate.SQL=debug # SQL generado por Hibernate
logging.level.org.hibernate.orm.jdbc.bind=trace # valores de los parámetros ?
logging.level.org.springframework.web=debug # depurar peticiones de Spring MVC
logging.file.name=logs/empresa-app.log # además de consola, a fichero (rota a los 10 MB)
Cada línea del log tiene este formato:
2026-10-04T14:15:13.101+02:00 INFO 1 --- [empresa-app] [nio-8080-exec-7] e.c.empresa.servicio.EmpleadoService : Empleado guardado: ...
└──────── fecha y hora ──────┘ nivel PID aplicación hilo de Tomcat logger (clase abreviada) mensaje
Para necesidades avanzadas (formato JSON, appenders distintos por perfil), crea src/main/resources/logback-spring.xml. El sufijo -spring permite usar la etiqueta <springProfile>:
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<springProfile name="dev">
<root level="INFO"><appender-ref ref="CONSOLE"/></root>
<logger name="es.curso.empresa" level="DEBUG"/>
</springProfile>
<springProfile name="prod">
<root level="WARN"><appender-ref ref="CONSOLE"/></root>
</springProfile>
</configuration>
💡 Consejo: Spring Boot también sabe escribir el log en formato JSON estructurado sin XML:
logging.structured.format.console=ecs(ologstash,gelf). Es lo habitual cuando el log lo procesa una herramienta como Elasticsearch o Grafana Loki.
13. DevTools
spring-boot-devtools mejora la experiencia de desarrollo:
| Función | Qué hace |
|---|---|
| Reinicio automático | Cuando cambian las clases compiladas (target/classes), reinicia la aplicación en uno o dos segundos (usa dos classloaders: uno para tus clases, que se descarta, y otro para las librerías, que se conserva) |
| Recarga en vivo (LiveReload) | Initializr aún la menciona, pero se declaró obsoleta en Boot 3.5 y en nuestras pruebas con Boot 4.1 el servidor LiveReload no llega a iniciarse; para ver los cambios de plantillas basta con recargar la página (la caché está desactivada) |
| Valores por defecto de desarrollo | Desactiva la caché de Thymeleaf, activa la consola de H2, muestra más detalle en errores web |
| Desactivación automática | Si arrancas un JAR empaquetado (java -jar), DevTools se desactiva solo |
Al arrancar con ./mvnw spring-boot:run, el log lo indica:
2026-10-04T15:33:30.684+02:00 INFO 108 --- [empresa-app] [ restartedMain] es.curso.empresa.EmpresaAppApplication : Starting EmpresaAppApplication using Java 25.0.4.1 with PID 108 (/w/target/classes started by root in /w)
2026-10-04T15:33:30.935+02:00 INFO 108 --- [empresa-app] [ restartedMain] es.curso.empresa.EmpresaAppApplication : No active profile set, falling back to 1 default profile: "dev"
2026-10-04T15:33:33.837+02:00 INFO 108 --- [empresa-app] [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2026-10-04T15:33:33.841+02:00 INFO 108 --- [empresa-app] [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
...
2026-10-04T15:38:55.515+02:00 INFO 108 --- [empresa-app] [ restartedMain] o.s.b.h.a.H2ConsoleAutoConfiguration : H2 console available at '/h2-console'. Database available at 'jdbc:mysql://mysql-curso:3306/java_boot'
...
2026-10-04T15:38:59.189+02:00 INFO 108 --- [empresa-app] [ restartedMain] es.curso.empresa.EmpresaAppApplication : Started EmpresaAppApplication in 357.605 seconds (process running for 389.544)
Fíjate en el hilo: restartedMain en lugar de main. Es el hilo del classloader de reinicio de DevTools, y la aplicación se ejecuta desde target/classes, no desde un JAR. También se ve un valor por defecto de DevTools: ha activado la consola web de H2 (¡aunque la base de datos sea MySQL!).
Al recompilar una clase (aquí, tocando EmpresaAppApplication.class), DevTools reinicia solo la parte de la aplicación:
2026-10-04T15:42:25.026+02:00 INFO 108 --- [empresa-app] [ File Watcher] rtingClassPathChangeChangedEventListener : Restarting due to 1 class path change (0 additions, 0 deletions, 1 modification)
...
2026-10-04T15:43:54.521+02:00 INFO 108 --- [empresa-app] [ restartedMain] es.curso.empresa.EmpresaAppApplication : Started EmpresaAppApplication in 87.637 seconds (process running for 696.879)
2026-10-04T15:43:54.582+02:00 INFO 108 --- [empresa-app] [ restartedMain] .ConditionEvaluationDeltaLoggingListener : Condition evaluation unchanged
El reinicio tardó la cuarta parte que el arranque inicial (en un equipo normal, uno o dos segundos frente a varios), porque las librerías ya estaban cargadas. La última línea indica que el informe de condiciones no ha cambiado con el reinicio. Como en el log de arranque, los tiempos absolutos están muy inflados por la carga del equipo en que se tomó la captura.
En IntelliJ, el reinicio se dispara al compilar (Build → Build Project, Ctrl + F9). Para que ocurra al guardar, activa en Settings → Build, Execution, Deployment → Compiler la opción Build project automatically y, en Advanced Settings, Allow auto-make to start even if developed application is currently running.
⚠️ Importante: DevTools está marcado como
<optional>true</optional>y el plugin de Maven lo excluye del JAR final. Nunca lo actives en producción: el reinicio automático y la consola de H2 son un riesgo de seguridad.
14. Actuator
Spring Boot Actuator añade endpoints HTTP para observar la aplicación en ejecución: si está viva, qué versión es, cuánta memoria usa, cuántas peticiones atiende… Es lo que usan los equipos de operaciones, Docker y Kubernetes para saber si una aplicación está sana.
Por seguridad, por HTTP solo se expone health por defecto. En empresa-app (perfil común) exponemos algunos más:
management.endpoints.web.exposure.include=health,info,metrics,env,conditions,beans,mappings
management.endpoint.health.show-details=always
management.endpoint.env.show-values=always
management.info.env.enabled=true
management.info.java.enabled=true
management.info.os.enabled=true
info.app.nombre=${spring.application.name}
info.app.descripcion=Gesti\u00f3n de la base de datos EMPRESA
info.app.curso=Curso de Java Avanzado
Al arrancar, el log confirma: Exposing 7 endpoints beneath base path '/actuator'.
| Endpoint | Qué muestra |
|---|---|
/actuator | Índice con enlaces a los endpoints expuestos |
/actuator/health | Estado: UP/DOWN, con detalle de base de datos, disco, etc. |
/actuator/info | Información de la aplicación (info.*, Java, sistema operativo, build, Git) |
/actuator/metrics | Métricas de Micrometer: JVM, Tomcat, Hikari, peticiones HTTP… |
/actuator/env | Propiedades y su origen (¡cuidado con los secretos!) |
/actuator/conditions | El informe de autoconfiguración |
/actuator/beans | Todos los beans del contexto |
/actuator/mappings | Todas las rutas (@GetMapping…) registradas |
/actuator/loggers | Ver y cambiar niveles de log en caliente |
health
Figura 10. Respuesta de /actuator/health en el navegador. Captura real.
El estado global es UP solo si todos los componentes lo están. El componente db lo añade la autoconfiguración porque hay un DataSource (ejecuta isValid() sobre una conexión). Los grupos liveness y readiness (/actuator/health/liveness, /actuator/health/readiness) son los que consulta Kubernetes. Si paras MySQL, db pasa a DOWN, el global también, y la respuesta HTTP cambia de 200 a 503: así un balanceador deja de enviar tráfico a esa instancia.
info, metrics y env
GET /actuator/info (respuesta real formateada; la sección build se añade en el ejercicio 6):
{
"app": {
"nombre": "empresa-app",
"descripcion": "Gestión de la base de datos EMPRESA",
"curso": "Curso de Java Avanzado"
},
"java": {
"jvm": {
"name": "OpenJDK 64-Bit Server VM",
"vendor": "Eclipse Adoptium",
"version": "25.0.4.1+1-LTS"
},
"runtime": {
"name": "OpenJDK Runtime Environment",
"version": "25.0.4.1+1-LTS"
},
"vendor": {
"name": "Eclipse Adoptium",
"version": "Temurin-25.0.4.1+1"
},
"version": "25.0.4.1"
},
"os": {
"arch": "amd64",
"name": "Linux",
"version": "5.15.167.4-microsoft-standard-WSL2"
}
}
GET /actuator/metrics devuelve los nombres de las 68 métricas disponibles (extracto):
{"names":["application.ready.time", "application.started.time", "disk.free", "disk.total", "executor.active", "executor.completed", "executor.pool.core", "executor.pool.max", ..., "hikaricp.connections.active", ..., "http.server.requests", ..., "jvm.memory.used", ...]}
Y cada métrica se consulta por su nombre. Las peticiones HTTP atendidas hasta ese momento, con sus etiquetas (tags) para filtrar:
{
"availableTags": [
{
"tag": "exception",
"values": [
"none"
]
},
{
"tag": "method",
"values": [
"POST",
"GET"
]
},
{
"tag": "error",
"values": [
"none"
]
},
{
"tag": "uri",
"values": [
"/api/productos",
"/swagger-ui/**",
"/actuator/info",
"/swagger-ui/*index.html",
"/v3/api-docs/swagger-config",
"/v3/api-docs",
"/swagger-ui/*swagger-initializer.js",
"/api/categorias/{id}",
"/actuator/metrics"
]
},
{
"tag": "outcome",
"values": [
"SUCCESS"
]
},
{
"tag": "status",
"values": [
"200",
"201"
]
}
],
"baseUnit": "seconds",
"measurements": [
{
"statistic": "COUNT",
"value": 13.0
},
{
"statistic": "TOTAL_TIME",
"value": 117.22204759299998
},
{
"statistic": "MAX",
"value": 2.432090545
}
],
"name": "http.server.requests"
}
Se puede filtrar por etiqueta: /actuator/metrics/http.server.requests?tag=uri:/api/productos&tag=status:201.
GET /actuator/env/spring.datasource.url dice el valor efectivo de una propiedad y de qué fuente sale:
{
"activeProfiles": [],
"defaultProfiles": [
"dev"
],
"property": {
"source": "Config resource 'class path resource [application-dev.properties]' via location 'optional:classpath:/'",
"value": "jdbc:mysql://mysql-curso:3306/java_boot"
}
}
(se ha omitido la lista propertySources, que enumera todas las fuentes en orden de prioridad). El valor sale de application-dev.properties, pero no es jdbc:mysql://localhost:3306/empresa porque en esta ejecución estaba definida la variable de entorno DB_URL: el placeholder ${DB_URL:...} la ha usado.
⚠️ Importante:
env,beans,conditionsologgersdan información muy valiosa a un atacante. En producción expón solohealtheinfo(como hace nuestro perfilprod) o protégelos con Spring Security (módulo 22). Además, Actuator enmascara por defecto los valores deenv(******);show-values=alwayssolo es aceptable en desarrollo.
15. Ejecutar la aplicación
Con Maven: spring-boot:run
./mvnw spring-boot:run # perfil por defecto (dev)
./mvnw spring-boot:run -Dspring-boot.run.profiles=h2 # con un perfil
./mvnw spring-boot:run -Dspring-boot.run.arguments=--server.port=9090
Compila, resuelve dependencias y ejecuta la clase principal con DevTools activo. Es la forma normal de trabajar en desarrollo (o el botón ▶ de IntelliJ junto al main). Para pararla, Ctrl + C.
Como JAR ejecutable
./mvnw clean package # compila, ejecuta las pruebas y empaqueta
java -jar target/empresa-app-0.0.1-SNAPSHOT.jar
java -jar target/empresa-app-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
El spring-boot-maven-plugin crea un JAR ejecutable (también llamado fat jar o uber jar) con todas las dependencias dentro. El de empresa-app ocupa unos 60 MB y contiene 101 JAR de librerías:
jar tf target/empresa-app-0.0.1-SNAPSHOT.jar | head -8
jar tf target/empresa-app-0.0.1-SNAPSHOT.jar | grep -E "EmpresaAppApplication|hibernate-core"
META-INF/
META-INF/MANIFEST.MF
META-INF/services/
META-INF/services/java.nio.file.spi.FileSystemProvider
org/
org/springframework/
org/springframework/boot/
org/springframework/boot/loader/
BOOT-INF/classes/es/curso/empresa/EmpresaAppApplication.class
BOOT-INF/lib/hibernate-core-7.4.5.Final.jar
Y este es su MANIFEST.MF (salida de unzip -p target/empresa-app-0.0.1-SNAPSHOT.jar META-INF/MANIFEST.MF):
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.5.1
Java-Version: 25
Build-Jdk-Spec: 25
Implementation-Title: empresa-app
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: es.curso.empresa.EmpresaAppApplication
Spring-Boot-Version: 4.1.1
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
| Carpeta | Contenido |
|---|---|
org/springframework/boot/loader/ | El launcher de Spring Boot, que sabe cargar JAR anidados |
BOOT-INF/classes/ | Tus clases y recursos (application.properties, plantillas…) |
BOOT-INF/lib/ | Las dependencias |
META-INF/MANIFEST.MF | Main-Class (el launcher) y Start-Class (tu clase principal) |
💡 Consejo: Si las pruebas aún no funcionan y necesitas el JAR,
./mvnw package -DskipTests. Pero no te acostumbres: en el módulo 23 verás cómo probar bien la aplicación.
16. El banner y el arranque
El banner
Al arrancar, Boot imprime un banner. Puedes sustituirlo creando src/main/resources/banner.txt (genera las letras con cualquier generador de «ASCII art»). Admite variables como ${application.title}, ${application.version} (se leen del MANIFEST.MF, así que solo tienen valor al ejecutar el JAR) y ${spring-boot.version}. El de empresa-app:
_____
| ____|_ __ ___ _ __ _ __ ___ ___ __ _ __ _ _ __ _ __
| _| | '_ ` _ \| '_ \| '__/ _ \/ __|/ _` |_____ / _` | '_ \| '_ \
| |___| | | | | | |_) | | | __/\__ \ (_| |_____| (_| | |_) | |_) |
|_____|_| |_| |_| .__/|_| \___||___/\__,_| \__,_| .__/| .__/
|_| |_| |_|
${application.title} ${application.version} - Spring Boot ${spring-boot.version}
Para quitarlo: spring.main.banner-mode=off.
Leer el log de arranque
Este es el arranque real de empresa-app con el perfil por defecto (dev) contra MySQL, ejecutado como JAR. Se han recortado las trazas DEBUG:
_____
| ____|_ __ ___ _ __ _ __ ___ ___ __ _ __ _ _ __ _ __
| _| | '_ ` _ \| '_ \| '__/ _ \/ __|/ _` |_____ / _` | '_ \| '_ \
| |___| | | | | | |_) | | | __/\__ \ (_| |_____| (_| | |_) | |_) |
|_____|_| |_| |_| .__/|_| \___||___/\__,_| \__,_| .__/| .__/
|_| |_| |_|
empresa-app 0.0.1-SNAPSHOT - Spring Boot 4.1.1
2026-10-04T14:35:43.901+02:00 INFO 1 --- [empresa-app] [ main] es.curso.empresa.EmpresaAppApplication : Starting EmpresaAppApplication v0.0.1-SNAPSHOT using Java 25.0.4.1 with PID 1 (/w/target/empresa-app-0.0.1-SNAPSHOT.jar started by root in /w)
2026-10-04T14:35:44.754+02:00 INFO 1 --- [empresa-app] [ main] es.curso.empresa.EmpresaAppApplication : No active profile set, falling back to 1 default profile: "dev"
2026-10-04T14:39:41.928+02:00 INFO 1 --- [empresa-app] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2026-10-04T14:39:54.657+02:00 INFO 1 --- [empresa-app] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 13048 ms. Found 6 JPA repository interfaces.
2026-10-04T14:42:14.660+02:00 INFO 1 --- [empresa-app] [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2026-10-04T14:42:15.775+02:00 INFO 1 --- [empresa-app] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2026-10-04T14:42:15.777+02:00 INFO 1 --- [empresa-app] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/11.0.24]
2026-10-04T14:42:18.414+02:00 INFO 1 --- [empresa-app] [ main] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 380372 ms
2026-10-04T14:43:06.855+02:00 INFO 1 --- [empresa-app] [ main] org.hibernate.orm.jpa : HHH008540: Processing PersistenceUnitInfo [name: default]
2026-10-04T14:43:19.638+02:00 INFO 1 --- [empresa-app] [ main] org.hibernate.orm.core : HHH000001: Hibernate ORM core version 7.4.5.Final
2026-10-04T14:44:40.609+02:00 INFO 1 --- [empresa-app] [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
2026-10-04T14:44:44.191+02:00 INFO 1 --- [empresa-app] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2026-10-04T14:45:25.961+02:00 INFO 1 --- [empresa-app] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@795131d6
2026-10-04T14:45:26.515+02:00 INFO 1 --- [empresa-app] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2026-10-04T14:45:49.309+02:00 INFO 1 --- [empresa-app] [ main] org.hibernate.orm.connections.pooling : HHH10001005: Database info:
Database JDBC URL [jdbc:mysql://mysql-curso:3306/java_boot]
Database driver: MySQL Connector/J
Database dialect: MySQLDialect
Database version: 8.4.11
Default catalog/schema: java_boot/undefined
Autocommit mode: undefined/unknown
Isolation level: REPEATABLE_READ [default REPEATABLE_READ]
JDBC fetch size: none
Pool: DataSourceConnectionProvider
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2026-10-04T14:47:56.055+02:00 INFO 1 --- [empresa-app] [ main] org.hibernate.orm.core : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2026-10-04T14:48:01.128+02:00 INFO 1 --- [empresa-app] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2026-10-04T14:48:07.957+02:00 INFO 1 --- [empresa-app] [ main] o.s.d.j.r.query.QueryEnhancerFactories : Hibernate is in classpath; If applicable, HQL parser will be used.
2026-10-04T14:52:19.575+02:00 INFO 1 --- [empresa-app] [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 7 endpoints beneath base path '/actuator'
2026-10-04T14:52:28.369+02:00 INFO 1 --- [empresa-app] [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2026-10-04T14:52:28.644+02:00 INFO 1 --- [empresa-app] [ main] es.curso.empresa.EmpresaAppApplication : Started EmpresaAppApplication in 1101.032 seconds (process running for 1268.693)
⚠️ Importante: Esta captura se hizo dentro de un contenedor Docker en un equipo con muchos otros procesos en marcha, por eso los tiempos son enormes (más de 1.000 segundos). En un ordenador normal,
empresa-apparranca en unos pocos segundos. Fíjate también en que la URL de la base de datos esjdbc:mysql://mysql-curso:3306/java_boot: se definió la variable de entornoDB_URL, que sustituye al valor por defectojdbc:mysql://localhost:3306/empresadel perfildev(sección 9).
Línea a línea, lo importante:
| Mensaje | Significado |
|---|---|
Starting EmpresaAppApplication v0.0.1-SNAPSHOT using Java 25... | Versión de tu aplicación y de Java, PID y carpeta |
No active profile set, falling back to 1 default profile: "dev" | Perfiles activos: compruébalo siempre |
Bootstrapping Spring Data JPA repositories... Found 6 JPA repository interfaces | Spring Data ha encontrado tus repositorios (módulo 19) |
Tomcat initialized with port 8080 (http) | Puerto del servidor embebido |
HikariPool-1 - Start completed | El pool de conexiones ha conectado con MySQL |
Database version: 8.4.11 | Hibernate ha detectado la base de datos y el dialecto |
Initialized JPA EntityManagerFactory | JPA listo (y el esquema validado con ddl-auto=validate) |
Exposing 7 endpoints beneath base path '/actuator' | Endpoints de Actuator expuestos |
Tomcat started on port 8080 (http) with context path '/' | Ya acepta peticiones |
Started EmpresaAppApplication in X seconds | Fin del arranque |
💡 Consejo: Si la aplicación no arranca, lee el log de abajo arriba: la causa raíz suele estar en el último
Caused by:o en el bloqueAPPLICATION FAILED TO START, que Spring Boot redacta con una descripción y una acción sugerida (Action:).
17. Ejercicios
Ejercicio 1: Tu primer controlador en empresa-app
Crea en es.curso.empresa.web.api un @RestController con un GET /api/hola que devuelva el texto Hola desde <nombre de la empresa>, tomando el nombre de EmpresaProperties. Pruébalo con curl.
Ver solución
package es.curso.empresa.web.api;
import es.curso.empresa.config.EmpresaProperties;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HolaController {
private final EmpresaProperties props;
public HolaController(EmpresaProperties props) { // inyección por constructor
this.props = props;
}
@GetMapping("/api/hola")
public String hola() {
return "Hola desde " + props.nombre();
}
}
curl http://localhost:8080/api/hola
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Hola desde EMPRESA S.A.
El controlador está en un subpaquete de es.curso.empresa, así que el escaneo de componentes lo encuentra sin configurar nada. Como es un @RestController, el String devuelto se escribe tal cual en el cuerpo de la respuesta (text/plain).
Ejercicio 2: Configuración en YAML
Reescribe en un fichero application-dev.yml el contenido de application-dev.properties (sección 11) y explica qué pasaría si dejaras los dos ficheros en el proyecto.
Ver solución
# Perfil dev: MySQL local (Docker) y trazas detalladas
spring:
datasource:
url: ${DB_URL:jdbc:mysql://localhost:3306/empresa}
username: ${DB_USER:curso}
password: ${DB_PASSWORD:curso}
jpa:
properties:
hibernate:
format_sql: true
logging:
level:
org.hibernate.SQL: debug
org.hibernate.orm.jdbc.bind: trace
es.curso.empresa: debug
Las claves con puntos que son nombres de paquete (org.hibernate.SQL) se escriben en una sola línea bajo level:: para el binding de mapas, Boot las interpreta como claves completas. Si dejas application-dev.properties y application-dev.yml a la vez, Boot carga los dos y, para las claves repetidas, prevalece el .properties: cambiarías el YAML y no verías ningún efecto. Borra uno de los dos.
Ejercicio 3: Propiedades del horario de atención
Añade al proyecto un record HorarioProperties con prefijo empresa.horario y los campos apertura y cierre (LocalTime, obligatorios) y diasLaborables (lista de DayOfWeek, por defecto de lunes a viernes). Valida que existan y muestra el horario en el log al arrancar con un ApplicationRunner.
Ver solución
empresa.horario.apertura=08:30
empresa.horario.cierre=17:30
package es.curso.empresa.config;
import jakarta.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.validation.annotation.Validated;
import java.time.DayOfWeek;
import java.time.LocalTime;
import java.util.List;
@Validated
@ConfigurationProperties(prefix = "empresa.horario")
public record HorarioProperties(
@NotNull LocalTime apertura,
@NotNull LocalTime cierre,
@DefaultValue({"MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"}) List<DayOfWeek> diasLaborables) {
}
package es.curso.empresa.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HorarioConfig {
private static final Logger log = LoggerFactory.getLogger(HorarioConfig.class);
@Bean
ApplicationRunner mostrarHorario(HorarioProperties horario) {
return args -> log.info("Horario de atención: {} a {} ({})",
horario.apertura(), horario.cierre(), horario.diasLaborables());
}
}
Salida real en el arranque:
2026-10-04T15:32:42.403+02:00 INFO 1 --- [empresa-app] [ main] es.curso.empresa.config.HorarioConfig : Horario de atención: 08:30 a 17:30 ([MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY])
@ConfigurationPropertiesScan (en la clase principal) registra el record automáticamente. Boot convierte "08:30" en LocalTime y los nombres de los días en DayOfWeek sin que escribas ningún conversor. Si quitas empresa.horario.apertura, la aplicación no arranca y el informe dice qué propiedad falta.
Ejercicio 4: Un perfil para la presentación en clase
Crea un perfil aula que: use el puerto 9090, desactive el banner, ponga todo el log a WARN salvo el paquete es.curso.empresa (a INFO) y no muestre el SQL. Indica cómo arrancarlo con Maven y con el JAR.
Ver solución
src/main/resources/application-aula.properties:
server.port=9090
spring.main.banner-mode=off
logging.level.root=warn
logging.level.es.curso.empresa=info
logging.level.org.hibernate.SQL=warn
logging.level.org.hibernate.orm.jdbc.bind=warn
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev,aula
java -jar target/empresa-app-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev,aula
Activamos dev,aula: dev aporta la conexión a MySQL y aula sobrescribe lo demás porque el último perfil de la lista gana cuando dos perfiles definen la misma clave. Como aula pone org.hibernate.SQL a warn, también anula la traza SQL de dev.
Ejercicio 5: Un indicador de salud propio
Crea un HealthIndicator llamado empleados que esté UP si hay al menos un empleado en la base de datos, con el número de empleados como detalle, y DOWN en caso contrario. Comprueba que aparece en /actuator/health.
Ver solución
package es.curso.empresa.config;
import es.curso.empresa.repositorio.EmpleadoRepository;
import org.springframework.boot.health.contributor.Health;
import org.springframework.boot.health.contributor.HealthIndicator;
import org.springframework.stereotype.Component;
/** El nombre del bean sin el sufijo HealthIndicator ("empleados") es el nombre del componente. */
@Component
public class EmpleadosHealthIndicator implements HealthIndicator {
private final EmpleadoRepository empleados;
public EmpleadosHealthIndicator(EmpleadoRepository empleados) {
this.empleados = empleados;
}
@Override
public Health health() {
long total = empleados.count();
return total > 0
? Health.up().withDetail("total", total).build()
: Health.down().withDetail("motivo", "No hay empleados").build();
}
}
curl http://localhost:8080/actuator/health
{
"components": {
"db": {
"details": {
"database": "MySQL",
"validationQuery": "isValid()"
},
"status": "UP"
},
"diskSpace": {
"details": {
"total": 1999721480192,
"free": 1368075501568,
"threshold": 10485760,
"path": "/w/.",
"exists": true
},
"status": "UP"
},
"empleados": {
"details": {
"total": 24
},
"status": "UP"
},
"livenessState": {
"status": "UP"
},
"ping": {
"status": "UP"
},
"readinessState": {
"status": "UP"
},
"ssl": {
"details": {
"expiringChains": [],
"invalidChains": [],
"validChains": []
},
"status": "UP"
}
},
"groups": [
"liveness",
"readiness"
],
"status": "UP"
}
El componente empleados aparece junto a los automáticos (db, diskSpace…) y, como está UP, el estado global sigue siendo UP. Si devolviera DOWN, el estado global pasaría a DOWN y la respuesta sería un 503.
⚠️ Importante: En Spring Boot 4 las clases de salud se movieron al módulo
spring-boot-health, paqueteorg.springframework.boot.health.contributor. En tutoriales de Boot 2 y 3 verásorg.springframework.boot.actuate.health, que ya no existe.
Ejercicio 6: Información de la build en /actuator/info
Haz que /actuator/info muestre la versión, la fecha de construcción y el artefacto de empresa-app sin escribirlos a mano.
Ver solución
Añade el objetivo build-info al plugin de Spring Boot en el pom.xml:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
Al compilar, el plugin genera META-INF/build-info.properties; Boot lo detecta, crea un bean BuildProperties y el BuildInfoContributor lo añade a /actuator/info:
{
"app": {
"nombre": "empresa-app",
"descripcion": "Gestión de la base de datos EMPRESA",
"curso": "Curso de Java Avanzado"
},
"build": {
"artifact": "empresa-app",
"name": "empresa-app",
"time": "2026-10-04T13:08:07.511Z",
"version": "0.0.1-SNAPSHOT",
"group": "es.curso"
}
}
(Se muestran solo las secciones app y build; además aparecen java y os.) La hora de build.time está en UTC.
Además puedes inyectar BuildProperties en tu código (por ejemplo, para mostrar la versión en el pie de la web).
Ejercicio 7 (propuesto): Jetty en lugar de Tomcat
Cambia el servidor embebido de empresa-app por Jetty excluyendo spring-boot-starter-tomcat. Comprueba en el log de arranque qué servidor se inicia y busca en el informe --debug qué autoconfiguración ha cambiado de Negative a Positive matches.
Ejercicio 8 (propuesto): Log a fichero solo en producción
Crea un logback-spring.xml que, con el perfil prod, escriba en logs/empresa-app.log con rotación diaria y conserve 7 días, y con cualquier otro perfil escriba solo en consola.
Ejercicio 9 (propuesto): Un contador de métricas
Inyecta MeterRegistry en EmpleadoService y crea un contador empresa.empleados.altas que se incremente cada vez que se da de alta un empleado. Consulta su valor en /actuator/metrics/empresa.empleados.altas.
Ejercicio 10 (propuesto): Grupos de perfiles
Define con spring.profiles.group.local=dev,aula un grupo local que active a la vez los perfiles dev y aula del ejercicio 4. Arranca con --spring.profiles.active=local y comprueba en el log qué perfiles quedan activos.
Ejercicio 11 (propuesto): Excluir una autoconfiguración
Crea un proyecto nuevo con Initializr con Spring Web y MySQL Driver (sin JPA). Arráncalo sin configurar ninguna base de datos: ¿qué error obtienes? Arréglalo de dos formas: excluyendo DataSourceAutoConfiguration y configurando un spring.datasource.url.
Ejercicio 12 (propuesto): Banner con tus propiedades
Modifica banner.txt para que muestre también el nombre de la empresa (${empresa.nombre}) y el perfil activo. Averigua por qué ${application.version} aparece vacío al ejecutar con ./mvnw spring-boot:run y no con java -jar.
Resumen
| Concepto | Sintaxis / ejemplo |
|---|---|
| Generar proyecto | start.spring.io, IntelliJ (New Project → Spring Boot), curl https://start.spring.io/starter.zip -d ... |
| Padre y versiones | spring-boot-starter-parent 4.1.1 (BOM): dependencias sin <version> |
| Clase principal | @SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan |
| Starters | spring-boot-starter-webmvc, -data-jpa, -validation, -thymeleaf, -actuator… |
| Autoconfiguración | META-INF/spring/...AutoConfiguration.imports + @ConditionalOnClass, @ConditionalOnMissingBean, @ConditionalOnProperty |
| Informe de condiciones | --debug o /actuator/conditions |
| Excluir | @SpringBootApplication(exclude = ...) o spring.autoconfigure.exclude |
| Servidor | Tomcat embebido, server.port, server.servlet.context-path |
| Configuración | application.properties (ISO-8859-1) o application.yml, ${VAR:defecto} |
| Prioridad | argumentos > variables de entorno > application-{perfil} > application |
| Propiedades propias | @ConfigurationProperties(prefix="empresa") record ... + @ConfigurationPropertiesScan + @Validated |
| Perfiles | application-dev.properties, spring.profiles.active, spring.profiles.default, @Profile("dev") |
| Logging | LoggerFactory.getLogger(...), log.info("... {}", x), logging.level.paquete=debug |
| DevTools | reinicio automático, desactivado en el JAR |
| Actuator | management.endpoints.web.exposure.include=health,info,metrics,env |
| Ejecutar | ./mvnw spring-boot:run, ./mvnw package + java -jar target/*.jar |
| Banner | src/main/resources/banner.txt, spring.main.banner-mode=off |