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 @SpringBootApplication y 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.properties o application.yml: puerto, base de datos MySQL, JPA y logging
  • Crear tus propias propiedades tipadas con @ConfigurationProperties y 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:run y 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.

Comparación entre configurar Spring a mano y usar Spring Boot 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-web pasa a llamarse spring-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.sql y 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:

Spring Initializr con los datos de empresa-app: Maven, Java, Spring Boot 4.1.1, grupo es.curso, artefacto empresa-app, Java 25 y las dependencias elegidas 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:

Diálogo de búsqueda de dependencias de Spring Initializr filtrando por «spring data» 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:

Vista Explore de Spring Initializr con el árbol del zip empresa-app y el pom.xml generado 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:

Esquema del asistente New Project de IntelliJ IDEA con el generador Spring Boot seleccionado y los campos de empresa-app Figura 5. Esquema del asistente New Project de IntelliJ IDEA (generador Spring Boot), primer paso.

  1. Elige el generador Spring Boot (el campo Server URL indica que usará start.spring.io).
  2. Rellena Group, Artifact y Package name como en la web; Name y Location deciden el nombre y la carpeta del proyecto en IntelliJ.
  3. En JDK elige un JDK 25 instalado (o descárgalo desde el propio desplegable) y en Java la versión 25.
  4. Pulsa Next.

En el segundo paso eliges la versión de Spring Boot y las dependencias:

Esquema del segundo paso del asistente: versión 4.1.1 de Spring Boot, árbol de dependencias y lista de dependencias añadidas Figura 6. Esquema del asistente de IntelliJ IDEA, segundo paso: versión y dependencias.

  1. Versión de Spring Boot: 4.1.1.
  2. Marca las dependencias navegando por las categorías o con el buscador.
  3. Comprueba la lista Added dependencies.
  4. 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 curl un proyecto hola-boot con la única dependencia web y compara su pom.xml con el de empresa-app. ¿Qué starters de pruebas aparecen en cada uno?


4. Estructura del proyecto generado

Árbol de carpetas del proyecto empresa-app con la explicación de cada elemento 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-parent hereda de spring-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.cmd y la carpeta .mvn al repositorio Git. El .gitignore generado ya excluye target/ 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(...):

  1. Prepara el Environment leyendo propiedades, variables de entorno, argumentos y perfiles.
  2. Muestra el banner.
  3. Crea el ApplicationContext adecuado (uno web, porque hay Spring MVC en el classpath).
  4. Registra tus beans (escaneo) y los autoconfigurados, y los crea.
  5. Arranca el servidor embebido (Tomcat) y ejecuta los CommandLineRunner/ApplicationRunner que 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 en es.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-jpa trae 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-web sigue existiendo en Boot 4, pero su descripción en Maven Central dice «deprecated in favor of spring-boot-starter-webmvc». En proyectos nuevos usa siempre spring-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 @Configuration especiales) que Spring Boot incluye en sus módulos. Cada una declara los beans de una tecnología (un DataSource, un EntityManagerFactory, un ObjectMapper…) 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…

Flujo de la autoconfiguración: @EnableAutoConfiguration, lectura de AutoConfiguration.imports, evaluación de condiciones y registro de beans Figura 8. Cómo decide Spring Boot qué beans crear.

Cómo funciona

  1. @EnableAutoConfiguration busca en cada JAR del classpath el fichero META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, que lista sus clases de autoconfiguración. Por ejemplo, el del módulo spring-boot-jdbc-4.1.1.jar contiene:
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
  1. Para cada candidata evalúa sus condiciones. Si todas se cumplen, procesa la clase como una @Configuration más; si no, la descarta sin crear nada.
  2. Los beans creados leen su configuración de propiedades (spring.datasource.*, spring.jpa.*…), que tú controlas desde application.properties.

💡 Consejo: En Boot 4 cada tecnología tiene su propio módulo con su propio fichero .imports (antes casi todo estaba en un único spring-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:

  • HibernateJpaConfiguration se aplica porque hay un único DataSource (@ConditionalOnSingleCandidate).
  • EmbeddedDatabaseConfiguration no se aplica porque hemos escrito spring.datasource.url: si no la escribiéramos, Boot crearía una base de datos H2 en memoria por su cuenta.
  • El localeResolver de 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 en empresa-app), muy cómodo para buscar con Ctrl + F en 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 .properties en ISO-8859-1. Si escribes info.app.descripcion=Gestión de la base de datos EMPRESA en UTF-8, el endpoint /actuator/info devuelve literalmente "Gestión de la base de datos EMPRESA" (lo hemos comprobado). Escribe los caracteres no ASCII como Gesti\u00f3n o 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=validate hace 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 (update o create está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:

Prioridad de las fuentes de configuración: argumentos, variables de entorno, ficheros de perfil, application.properties, @PropertySource y valores por defecto 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.properties colocado 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 componente emailContacto. También valdrían emailContacto o EMPRESA_EMAILCONTACTO como variable de entorno.
  • @DefaultValue da valores por defecto; en Paginacion sin valor (@DefaultValue vacío) hace que el grupo se cree aunque no haya ninguna propiedad empresa.paginacion.*.
  • @Validated aplica 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 propiedades empresa.*, 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 prod no 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 perfil prod sin definir DB_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.active dentro de application.properties también funciona, pero entonces es fácil olvidarse de que está ahí y subir a producción con dev activado. spring.profiles.default expresa 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 (o logstash, 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

JSON de /actuator/health de empresa-app en el navegador: status UP con los componentes db (MySQL), diskSpace, livenessState, ping, readinessState y ssl 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, conditions o loggers dan información muy valiosa a un atacante. En producción expón solo health e info (como hace nuestro perfil prod) o protégelos con Spring Security (módulo 22). Además, Actuator enmascara por defecto los valores de env (******); show-values=always solo 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-app arranca en unos pocos segundos. Fíjate también en que la URL de la base de datos es jdbc:mysql://mysql-curso:3306/java_boot: se definió la variable de entorno DB_URL, que sustituye al valor por defecto jdbc:mysql://localhost:3306/empresa del perfil dev (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 bloque APPLICATION 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, paquete org.springframework.boot.health.contributor. En tutoriales de Boot 2 y 3 verás org.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