Entre que pulsas Run y ves en consola Started CicloUrbanaApplication in 2.174 seconds ocurren muchísimas cosas en un orden muy concreto. Conocer ese orden deja de ser curiosidad académica en cuanto necesitas precargar datos, ejecutar una comprobación al arrancar, liberar recursos al parar o entender por qué un bean no está listo cuando lo usas. En esta lección abriremos SpringApplication.run(...), aprenderemos a leer el log de arranque real de CicloUrbana, personalizaremos el banner, engancharemos código a los eventos del ciclo de vida y configuraremos un apagado ordenado que no corte alquileres a medias.
Contenido
- Qué hace exactamente
SpringApplication.run(...) - Las fases del arranque, paso a paso
- El banner y cómo personalizarlo
- Interpretar el log de arranque de CicloUrbana
- Los eventos del ciclo de vida
- Un listener de
ApplicationReadyEvent CommandLineRunneryApplicationRunner- Personalizar el arranque con
SpringApplicationBuilder - Apagado ordenado y hooks de cierre
- Errores Comunes y Consejos
- Ejercicios
- Qué hace exactamente
SpringApplication.run(...)
SpringApplication.run(...)Recordemos la línea:
public static void main(String[] args) {
SpringApplication.run(CicloUrbanaApplication.class, args);
}Ese método estático es un atajo equivalente a:
public static void main(String[] args) {
SpringApplication aplicacion = new SpringApplication(CicloUrbanaApplication.class);
ConfigurableApplicationContext contexto = aplicacion.run(args);
// El contexto queda vivo: mientras Tomcat escuche, el proceso no termina
}SpringApplication es una clase de Spring Boot —no de Spring Framework— cuya responsabilidad es orquestar el arranque: decidir qué tipo de contexto crear, preparar el entorno, cargar la configuración, publicar eventos y arrancar el servidor si procede.
Devuelve un ConfigurableApplicationContext: el contenedor con todos los beans ya construidos. Puedes guardarlo si necesitas consultarlo, aunque en la práctica casi nunca hace falta.
- Las fases del arranque, paso a paso
Este es el recorrido completo, en orden:
flowchart TD
A["main() invoca SpringApplication.run()"] --> B["1. Crear SpringApplication<br/>Deducir el tipo de aplicación<br/>(SERVLET / REACTIVE / NONE)"]
B --> C["2. Cargar ApplicationContextInitializer<br/>y ApplicationListener desde spring.factories"]
C --> D["3. Publicar ApplicationStartingEvent"]
D --> E["4. Preparar el Environment<br/>args, variables de entorno,<br/>application.properties, perfiles"]
E --> F["5. Publicar ApplicationEnvironmentPreparedEvent"]
F --> G["6. Imprimir el banner"]
G --> H["7. Crear el ApplicationContext<br/>AnnotationConfigServletWebServerApplicationContext"]
H --> I["8. Aplicar los ApplicationContextInitializer"]
I --> J["9. Publicar ApplicationContextInitializedEvent"]
J --> K["10. Cargar las definiciones de beans<br/>@ComponentScan desde com.ciclourbana"]
K --> L["11. Publicar ApplicationPreparedEvent"]
L --> M["12. refresh(): procesar la autoconfiguración<br/>y crear los beans singleton"]
M --> N["13. Arrancar el servidor web embebido<br/>Tomcat en el puerto 8080"]
N --> O["14. Publicar ApplicationStartedEvent"]
O --> P["15. Ejecutar ApplicationRunner<br/>y CommandLineRunner"]
P --> Q["16. Publicar ApplicationReadyEvent"]
Q --> R["Aplicación en marcha"]
Vamos a detenernos en los pasos que más consecuencias prácticas tienen.
Paso 1: deducir el tipo de aplicación
Spring Boot inspecciona el classpath y decide:
| Qué encuentra | Tipo deducido | Consecuencia |
|---|---|---|
DispatcherServlet (Spring MVC) |
SERVLET |
Arranca Tomcat; el proceso queda vivo |
WebFlux sin Spring MVC |
REACTIVE |
Arranca Netty |
| Ninguno de los dos | NONE |
No hay servidor; el proceso termina al acabar el main |
CicloUrbana tiene spring-boot-starter-web, así que es SERVLET. Esto explica por qué en la lección 01-03 dijimos que sin la dependencia web la aplicación arranca y muere: el tipo deducido es NONE y nada mantiene vivo el proceso.
Paso 4: preparar el Environment
El Environment es el objeto que responde a "¿cuánto vale la propiedad X?". Se construye combinando múltiples fuentes con prioridades. Las principales, de mayor a menor prioridad:
- Argumentos de línea de comandos (
--server.port=9090) - Propiedades del sistema Java (
-Dserver.port=9090) - Variables de entorno (
SERVER_PORT=9090) application-{perfil}.propertiesapplication.properties- Valores por defecto del código
Por eso java -jar ciclourbana.jar --server.port=9090 gana sobre lo escrito en application.properties: los argumentos están más arriba en la lista. El detalle completo del sistema de propiedades es el tema de la lección 02-05.
Paso 10: cargar las definiciones de beans
Aquí actúa el @ComponentScan que estudiamos en la lección 01-04: Spring recorre com.ciclourbana y sus subpaquetes buscando anotaciones. Encuentra EstacionController, lo registra como definición de bean. Ojo: registrar la definición no es crear el objeto; eso pasa en el paso siguiente.
Paso 12: refresh() y la creación de beans
Es el paso más largo y donde se concentra el tiempo de arranque. Ocurren dos cosas:
- Se procesan las clases de autoconfiguración: cientos de clases condicionales deciden qué beans registrar según lo que haya en el classpath y en las propiedades. Aquí es donde se crea el
DispatcherServlet, el conversor JSON de Jackson, elTomcatServletWebServerFactory... Cómo funcionan esas condiciones se ve en la lección 02-06. - Se instancian los beans singleton: Spring construye los objetos resolviendo el orden de dependencias. Si
AlquilerServicenecesitaEstacionService, este se crea primero.
Paso 13: arrancar Tomcat
Con los beans listos, se arranca el servidor y se abre el puerto. A partir de aquí la aplicación ya acepta peticiones HTTP, aunque todavía queden pasos por ejecutar. Es un matiz importante: los CommandLineRunner del paso 15 se ejecutan con el puerto ya abierto.
- El banner y cómo personalizarlo
Lo primero que aparece en consola es el banner de Spring. No cumple ninguna función técnica, pero es un buen sitio para poner el nombre y la versión de tu aplicación, algo agradecido cuando tienes varias instancias abiertas.
Crea src/main/resources/banner.txt:
_____ _ _ _ _ _
/ ____(_) | | | | | | | |
| | _ ___| | ___ | | | |_ __| |__ __ _ _ __ __ _
| | | |/ __| |/ _ \| | | | '__| '_ \ / _` | '_ \ / _` |
| |____| | (__| | (_) | |_| | | | |_) | (_| | | | | (_| |
\_____|_|\___|_|\___/ \___/|_| |_.__/ \__,_|_| |_|\__,_|
Red municipal de bicicletas electricas de Ribalta
Version de la aplicacion : ${application.version:sin-version}
Spring Boot : ${spring-boot.version}
Perfiles activos : ${spring.profiles.active:ninguno}Spring Boot sustituye estos marcadores:
| Marcador | Valor |
|---|---|
${application.version} |
La version del pom.xml, disponible al ejecutar el JAR |
${application.title} |
El name del proyecto |
${spring-boot.version} |
La versión de Spring Boot |
${spring.profiles.active} |
Los perfiles activos (módulo 7) |
${AnsiColor.GREEN} |
Color ANSI en terminales compatibles |
La sintaxis ${clave:valorPorDefecto} evita que aparezca el literal cuando la propiedad no existe. Es útil porque application.version solo tiene valor si se ejecuta desde el JAR (lo lee del MANIFEST.MF), no con spring-boot:run.
Para controlar el banner:
# Desactivarlo por completo
spring.main.banner-mode=off
# Enviarlo al log en lugar de a la consola
spring.main.banner-mode=log
# Usar un fichero con otro nombre o ubicación
spring.banner.location=classpath:banners/ciclourbana.txtEn producción lo habitual es off o log: el banner ensucia los agregadores de logs estructurados.
- Interpretar el log de arranque de CicloUrbana
Este es un arranque real, línea a línea:
2026-08-31T10:22:40.812+02:00 INFO 18422 --- [ciclourbana] [ restartedMain] c.c.CicloUrbanaApplication : Starting CicloUrbanaApplication using Java 21.0.5 with PID 18422 (/home/joan/ciclourbana/target/classes started by joan in /home/joan/ciclourbana)
2026-08-31T10:22:40.815+02:00 INFO 18422 --- [ciclourbana] [ restartedMain] c.c.CicloUrbanaApplication : No active profile set, falling back to 1 default profile: "default"
2026-08-31T10:22:41.104+02:00 INFO 18422 --- [ciclourbana] [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2026-08-31T10:22:42.688+02:00 INFO 18422 --- [ciclourbana] [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2026-08-31T10:22:42.703+02:00 INFO 18422 --- [ciclourbana] [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2026-08-31T10:22:42.704+02:00 INFO 18422 --- [ciclourbana] [ restartedMain] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.31]
2026-08-31T10:22:42.760+02:00 INFO 18422 --- [ciclourbana] [ restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2026-08-31T10:22:42.761+02:00 INFO 18422 --- [ciclourbana] [ restartedMain] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1920 ms
2026-08-31T10:22:42.905+02:00 INFO 18422 --- [ciclourbana] [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2026-08-31T10:22:42.918+02:00 INFO 18422 --- [ciclourbana] [ restartedMain] c.c.CicloUrbanaApplication : Started CicloUrbanaApplication in 2.174 seconds (process running for 2.512)Anatomía de una línea de log
2026-08-31T10:22:42.905+02:00 INFO 18422 --- [ciclourbana] [restartedMain] o.s.b.w.e.tomcat.TomcatWebServer : Tomcat started on port 8080
└──────── fecha y hora ──────┘ └nivel┘ └PID┘ └ app ─┘ └── hilo ──┘ └──── clase (abreviada) ────┘ └─ mensaje ─┘- PID: el identificador de proceso del sistema operativo. Sirve para
killo para localizar el proceso. - Nombre de la aplicación: viene de
spring.application.name, que fijamos en la lección anterior. - Hilo:
restartedMainindica que DevTools está activo. Sin DevTools seríamain. Cuando llegue una petición HTTP veráshttp-nio-8080-exec-1. - Clase abreviada:
o.s.b.w.embedded.tomcat.TomcatWebServeresorg.springframework.boot.web.embedded.tomcat.TomcatWebServer. Spring abrevia los paquetes para que quepa el mensaje.
Las líneas que más información dan
| Línea | Qué te está diciendo |
|---|---|
Starting ... using Java 21.0.5 with PID |
Confirma la versión de Java real y el directorio de trabajo |
No active profile set |
No hay perfil activo; se usa default. Muy útil para detectar despliegues mal configurados |
Devtools property defaults active! |
DevTools está aplicando valores de desarrollo. Nunca debe aparecer en producción |
Tomcat initialized with port 8080 |
El servidor se ha creado, pero aún no escucha |
Root WebApplicationContext: initialization completed in 1920 ms |
Tiempo de creación de todos los beans: si el arranque es lento, aquí está la mayor parte |
Tomcat started on port 8080 |
Ahora sí acepta peticiones |
Started ... in 2.174 seconds (process running for 2.512) |
2,174 s desde SpringApplication.run(); 2,512 s desde que arrancó la JVM. La diferencia es el arranque de la propia JVM |
Para ver más detalle durante un diagnóstico:
# Muestra un informe de qué autoconfiguraciones se aplicaron y cuáles no
debug=true
# Traza el proceso de arranque de Spring
logging.level.org.springframework.boot.autoconfigure=DEBUG
- Los eventos del ciclo de vida
Spring Boot publica eventos en momentos concretos del arranque y del cierre. Puedes suscribirte a ellos para ejecutar código en el instante exacto que necesitas.
| Evento | Cuándo se publica | ¿Hay contexto? | Uso típico |
|---|---|---|---|
ApplicationStartingEvent |
Al principio de todo | No | Registrar listeners muy tempranos |
ApplicationEnvironmentPreparedEvent |
Environment listo, contexto aún no |
No (sí Environment) |
Modificar o validar propiedades |
ApplicationContextInitializedEvent |
Contexto creado, beans sin cargar | Sí (vacío) | Registrar beans mediante programación |
ApplicationPreparedEvent |
Definiciones cargadas, beans sin crear | Sí | Última oportunidad de modificar definiciones |
ApplicationStartedEvent |
Contexto refrescado, runners aún no ejecutados | Sí | Comprobaciones previas a la carga de datos |
ApplicationReadyEvent |
Todo listo, runners ejecutados | Sí | Precarga, avisos, tareas de arranque |
ApplicationFailedEvent |
El arranque ha fallado | Puede que sí | Alertas, diagnóstico |
ContextClosedEvent |
El contexto se está cerrando | Sí | Liberar recursos, avisar de la parada |
La distinción clave es entre ApplicationStartedEvent y ApplicationReadyEvent:
ApplicationStartedEvent: la aplicación funciona, pero losCommandLineRunnertodavía no se han ejecutado.ApplicationReadyEvent: todo ha terminado, incluidos los runners. Es el evento que debes usar en el 90 % de los casos.
Para los eventos previos a la existencia del contexto (ApplicationStartingEvent, ApplicationEnvironmentPreparedEvent) no basta con anotar un método: el contenedor aún no existe y no hay beans. Hay que registrarlos mediante programación (apartado 8) o declararlos en META-INF/spring.factories.
- Un listener de
ApplicationReadyEvent
ApplicationReadyEventVamos a añadir a CicloUrbana un listener que, en cuanto la aplicación esté lista, registre en el log cuántas estaciones tiene cargadas la red de Ribalta.
Primero necesitamos que las estaciones dejen de estar dentro del controlador y vivan en un componente propio. Creamos src/main/java/com/ciclourbana/estaciones/AlmacenEstaciones.java:
package com.ciclourbana.estaciones;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* Almacén en memoria de las estaciones de la red de Ribalta.
* Es una solución provisional hasta el módulo 4, donde se
* sustituirá por un repositorio de Spring Data JPA.
*/
@Component
public class AlmacenEstaciones {
private final List<Estacion> estaciones = new ArrayList<>();
public List<Estacion> listarTodas() {
return List.copyOf(estaciones); // copia inmutable: nadie la modifica desde fuera
}
public void agregar(Estacion estacion) {
estaciones.add(estacion);
}
public int contar() {
return estaciones.size();
}
public int capacidadTotal() {
return estaciones.stream()
.mapToInt(Estacion::capacidad)
.sum();
}
}@Component le indica a Spring que debe crear una única instancia de esta clase y gestionarla como bean. Al estar en com.ciclourbana.estaciones, el escaneo la encuentra. El detalle de los estereotipos y la inyección de dependencias se ve en el módulo 2; aquí lo usamos como herramienta.
Ahora el listener, en src/main/java/com/ciclourbana/comun/AvisoArranque.java:
package com.ciclourbana.comun;
import com.ciclourbana.estaciones.AlmacenEstaciones;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
/**
* Registra en el log un resumen del estado de la red
* en cuanto CicloUrbana termina de arrancar.
*/
@Component
public class AvisoArranque {
private static final Logger log = LoggerFactory.getLogger(AvisoArranque.class);
private final AlmacenEstaciones almacenEstaciones;
private final Environment entorno;
// Inyección por constructor: Spring pasa los beans necesarios al crear esta clase
public AvisoArranque(AlmacenEstaciones almacenEstaciones, Environment entorno) {
this.almacenEstaciones = almacenEstaciones;
this.entorno = entorno;
}
@EventListener(ApplicationReadyEvent.class)
public void alEstarLista() {
String puerto = entorno.getProperty("server.port", "8080");
log.info("================================================");
log.info(" CicloUrbana lista - red municipal de Ribalta");
log.info(" Estaciones cargadas : {}", almacenEstaciones.contar());
log.info(" Capacidad total : {} anclajes", almacenEstaciones.capacidadTotal());
log.info(" API disponible en : http://localhost:{}/api/v1/estaciones", puerto);
log.info("================================================");
}
}Puntos a destacar:
@EventListener(ApplicationReadyEvent.class)basta para suscribirse. Es más limpio que implementar la interfazApplicationListener, y el método puede llamarse como quieras.- El logger de SLF4J es la forma correcta de registrar mensajes en Spring Boot; nunca uses
System.out.println. SLF4J y Logback vienen incluidos enspring-boot-starter. - Las llaves
{}son marcadores de posición de SLF4J. Son preferibles a la concatenación con+porque solo se resuelven si el nivel de log está activo. - La inyección por constructor (recibir las dependencias como parámetros) es la práctica recomendada. Módulo 2, lección 02-02.
CommandLineRunner y ApplicationRunner
CommandLineRunner y ApplicationRunnerAmbas son interfaces funcionales que Spring Boot ejecuta después de que el contexto esté listo y antes de publicar ApplicationReadyEvent. Sirven para ejecutar código de inicialización.
| Aspecto | CommandLineRunner |
ApplicationRunner |
|---|---|---|
| Firma | void run(String... args) |
void run(ApplicationArguments args) |
| Argumentos | Array de cadenas sin procesar | Objeto que distingue opciones (--clave=valor) de argumentos sueltos |
| Métodos útiles | Los de un array | getOptionNames(), getOptionValues("x"), getNonOptionArgs() |
| Cuándo elegirlo | No necesitas leer argumentos, o son simples | Necesitas interpretar opciones con nombre |
| Momento de ejecución | Tras el arranque, antes de ApplicationReadyEvent |
El mismo |
| Orden entre varios | Con @Order o la interfaz Ordered |
Igual |
Precargar las estaciones de demostración
Creamos src/main/java/com/ciclourbana/estaciones/CargadorEstacionesDemo.java:
package com.ciclourbana.estaciones;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* Carga las estaciones iniciales de la red de Ribalta al arrancar.
* Provisional hasta el módulo 4, donde los datos vendrán de la
* base de datos mediante migraciones de Flyway.
*/
@Component
@Order(1) // se ejecuta antes que otros runners con @Order superior
public class CargadorEstacionesDemo implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(CargadorEstacionesDemo.class);
private final AlmacenEstaciones almacenEstaciones;
public CargadorEstacionesDemo(AlmacenEstaciones almacenEstaciones) {
this.almacenEstaciones = almacenEstaciones;
}
@Override
public void run(String... args) {
log.info("Cargando estaciones de demostración de Ribalta...");
almacenEstaciones.agregar(new Estacion(1L, "Plaza Mayor",
"Plaza Mayor, 1", 24, 40.4168, -3.7038));
almacenEstaciones.agregar(new Estacion(2L, "Estación Norte",
"Avenida de la Estación 3", 30, 40.4290, -3.7020));
almacenEstaciones.agregar(new Estacion(3L, "Parque del Río",
"Paseo Fluvial 12", 18, 40.4105, -3.6950));
almacenEstaciones.agregar(new Estacion(4L, "Universidad",
"Campus Sur, acceso B", 36, 40.4402, -3.7255));
log.info("Cargadas {} estaciones", almacenEstaciones.contar());
}
}Y actualizamos el controlador para que use el almacén en lugar de su lista fija:
package com.ciclourbana.estaciones;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/v1/estaciones")
public class EstacionController {
private final AlmacenEstaciones almacenEstaciones;
public EstacionController(AlmacenEstaciones almacenEstaciones) {
this.almacenEstaciones = almacenEstaciones;
}
@GetMapping
public List<Estacion> listarEstaciones() {
return almacenEstaciones.listarTodas();
}
}Al arrancar, el log muestra ahora el orden real de los acontecimientos:
... o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http)
... c.c.e.CargadorEstacionesDemo : Cargando estaciones de demostración de Ribalta...
... c.c.e.CargadorEstacionesDemo : Cargadas 4 estaciones
... c.c.CicloUrbanaApplication : Started CicloUrbanaApplication in 2.301 seconds
... c.c.comun.AvisoArranque : ================================================
... c.c.comun.AvisoArranque : CicloUrbana lista - red municipal de Ribalta
... c.c.comun.AvisoArranque : Estaciones cargadas : 4
... c.c.comun.AvisoArranque : Capacidad total : 108 anclajes
... c.c.comun.AvisoArranque : API disponible en : http://localhost:8080/api/v1/estaciones
... c.c.comun.AvisoArranque : ================================================Observa la secuencia: Tomcat arranca primero, luego el CommandLineRunner, luego el mensaje Started y por último el listener de ApplicationReadyEvent. Coincide exactamente con los pasos 13 a 16 del diagrama.
Ejemplo con ApplicationRunner
Cuando necesitas leer opciones con nombre:
package com.ciclourbana.comun;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
/**
* Ejemplo de lectura de opciones de línea de comandos.
* Uso: java -jar ciclourbana.jar --modo=mantenimiento --verbose
*/
@Component
public class LectorArgumentos implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(LectorArgumentos.class);
@Override
public void run(ApplicationArguments args) {
// Opciones con nombre, del estilo --clave=valor
log.info("Opciones recibidas: {}", args.getOptionNames());
if (args.containsOption("modo")) {
// getOptionValues devuelve una lista: una opción puede repetirse
String modo = args.getOptionValues("modo").get(0);
log.info("Arrancando en modo: {}", modo);
}
// Argumentos sueltos, sin el prefijo --
log.info("Argumentos sin nombre: {}", args.getNonOptionArgs());
}
}Con CommandLineRunner recibirías el array crudo ["--modo=mantenimiento", "--verbose"] y tendrías que trocear las cadenas a mano. Esa es toda la diferencia entre ambas interfaces.
Un aviso importante: si un runner lanza una excepción, el arranque falla y la aplicación se detiene. Es un comportamiento deseable —si la precarga de datos crítica falla, mejor no arrancar— pero conviene tenerlo presente y capturar lo que no deba ser fatal.
- Personalizar el arranque con
SpringApplicationBuilder
SpringApplicationBuilderSpringApplicationBuilder ofrece una API fluida para configurar el arranque mediante programación, útil cuando la configuración no puede expresarse en propiedades.
package com.ciclourbana;
import org.springframework.boot.Banner;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
@SpringBootApplication
public class CicloUrbanaApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(CicloUrbanaApplication.class)
// Sin banner: los agregadores de logs lo agradecen
.bannerMode(Banner.Mode.OFF)
// Forzar el tipo de aplicación en lugar de deducirlo del classpath
.web(WebApplicationType.SERVLET)
// Perfil activo por defecto si no se indica otro (módulo 7)
.profiles("default")
// Propiedades por defecto: las de menor prioridad de todas
.properties("spring.application.name=ciclourbana")
// Listener para un evento anterior a la creación del contexto
.listeners(evento -> {
if (evento instanceof org.springframework.boot.context.event.ApplicationStartingEvent) {
System.out.println(">> Arrancando CicloUrbana...");
}
})
// Registrar el hook de apagado de la JVM (activo por defecto)
.registerShutdownHook(true)
.run(args);
}
}Repaso de las opciones más útiles:
| Método | Efecto |
|---|---|
bannerMode(Mode.OFF / LOG / CONSOLE) |
Controla el banner desde el código |
web(WebApplicationType.NONE) |
Arranca sin servidor: procesos por lotes o herramientas CLI |
profiles("dev", "local") |
Activa perfiles adicionales (módulo 7) |
properties("clave=valor") |
Propiedades por defecto, con la prioridad más baja |
listeners(...) |
Registra listeners de eventos tempranos, antes de que exista el contexto |
logStartupInfo(false) |
Omite las líneas Starting... y Started... |
parent(...) / child(...) |
Jerarquías de contextos, poco frecuentes |
Un caso realista para CicloUrbana: una herramienta que genera el informe mensual de uso, reutilizando todo el código del proyecto pero sin arrancar el servidor web.
new SpringApplicationBuilder(CicloUrbanaApplication.class)
.web(WebApplicationType.NONE) // sin Tomcat: el proceso termina al acabar
.bannerMode(Banner.Mode.OFF)
.run(args);Al ser de tipo NONE, el main termina cuando acaban los runners y la JVM se cierra sola. Exactamente lo que quieres en una tarea programada.
- Apagado ordenado y hooks de cierre
Parar una aplicación de golpe en producción significa cortar peticiones a medias. En CicloUrbana eso podría ser un alquiler que se inicia pero nunca se registra.
Apagado ordenado (graceful shutdown)
# src/main/resources/application.properties
# Al recibir la señal de parada, dejar de aceptar peticiones nuevas
# y esperar a que terminen las que están en curso
server.shutdown=graceful
# Tiempo máximo de espera antes de forzar el cierre (por defecto 30s)
spring.lifecycle.timeout-per-shutdown-phase=20sCon server.shutdown=graceful, al recibir un SIGTERM (lo que envía docker stop o Kubernetes) ocurre esto:
sequenceDiagram
participant OS as Sistema operativo
participant SB as Spring Boot
participant T as Tomcat
participant P as Peticiones en curso
OS->>SB: SIGTERM
SB->>T: Dejar de aceptar conexiones nuevas
Note over T: Las peticiones nuevas se rechazan
SB->>P: Esperar hasta 20 s
P-->>SB: Peticiones terminadas
SB->>SB: Publicar ContextClosedEvent
SB->>SB: Destruir beans singleton
SB->>OS: Proceso finalizado
En el log verás:
... o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
... o.s.b.w.e.tomcat.GracefulShutdown : Graceful shutdown completeEl valor por defecto es immediate, que corta al instante. En cualquier despliegue real debes activar graceful.
Reaccionar al cierre
Puedes ejecutar código antes de que la aplicación desaparezca:
package com.ciclourbana.comun;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class AvisoApagado {
private static final Logger log = LoggerFactory.getLogger(AvisoApagado.class);
@EventListener(ContextClosedEvent.class)
public void alCerrar() {
log.info("CicloUrbana se está deteniendo. Cerrando recursos abiertos...");
// Aquí liberarías conexiones externas, volcarías caches pendientes, etc.
}
}Existe una alternativa a nivel de bean, la anotación @PreDestroy, que Spring invoca justo antes de destruir cada objeto. Forma parte del ciclo de vida de los beans y se estudia en la lección 02-03.
El hook de cierre de la JVM
Spring Boot registra automáticamente un shutdown hook en la JVM. Es lo que garantiza que el contexto se cierre limpiamente cuando:
- Pulsas
Ctrl+Cen la terminal. - El orquestador envía
SIGTERM(docker stop,kubectl delete pod). - El proceso termina de forma normal.
Importante: kill -9 (SIGKILL) no puede interceptarse. El proceso muere al instante sin ejecutar ningún hook. Por eso Kubernetes envía primero SIGTERM y solo recurre a SIGKILL si la aplicación no termina dentro del plazo de gracia.
Para desactivar el hook —muy raramente necesario—:
Errores Comunes y Consejos
- Usar
ApplicationStartedEventcuando se queríaApplicationReadyEvent. Con el primero, losCommandLineRunneraún no se han ejecutado, así que los datos que esperas precargados todavía no están. Ante la duda, usaApplicationReadyEvent. - Poner lógica pesada en un
CommandLineRunner. Todo lo que hagas ahí retrasa el arranque, y en Kubernetes un arranque lento puede provocar reinicios en bucle por fallo de la sonda de disponibilidad. Para trabajo largo, lanza una tarea asíncrona (módulo 7). - Suscribirse con
@EventListenera eventos anteriores al contexto.ApplicationStartingEventyApplicationEnvironmentPreparedEventse publican cuando el contenedor aún no existe: un bean anotado nunca los recibirá. Regístralos conSpringApplicationBuilder.listeners(...). - Confundir el tiempo
Started ... in X secondscon el tiempo total. El paréntesis(process running for Y)incluye el arranque de la JVM. La diferencia entre ambos no depende de tu código. - Olvidar
server.shutdown=gracefulen producción. Es una línea que evita errores intermitentes durante cada despliegue, difíciles de reproducir y de diagnosticar. - No entender
restartedMain. Ese nombre de hilo indica que DevTools está activo. Si aparece en un entorno que no es tu portátil, tienes un problema de empaquetado. - Consejo — arranca con
debug=trueuna vez. El informe de autoconfiguración que imprime enseña muchísimo sobre lo que Spring Boot está decidiendo por ti. - Consejo — logs de arranque informativos. Un
AvisoArranqueque resuma el estado del sistema ahorra horas de diagnóstico cuando algo va mal en un servidor remoto.
Ejercicios
Ejercicio 1
Crea un banner.txt personalizado para CicloUrbana que muestre el nombre de la aplicación, la versión de Spring Boot y el perfil activo. Comprueba después que spring.main.banner-mode=off lo desactiva.
Ejercicio 2
Implementa un CommandLineRunner llamado VerificadorArranque que compruebe al iniciar que la red tiene al menos 3 estaciones y que ninguna tiene capacidad 0. Si alguna comprobación falla, debe impedir el arranque con un mensaje claro. Explica por qué es preferible fallar al arrancar antes que descubrir el problema en la primera petición.
Ejercicio 3
Añade a CicloUrbana un listener de ApplicationReadyEvent que registre el tiempo total de arranque y un listener de ContextClosedEvent que registre cuánto tiempo ha estado la aplicación en marcha. Activa además el apagado ordenado y verifica su funcionamiento en el log.
Soluciones
Solución 1
# src/main/resources/banner.txt
____ _ _ _ _ _
/ ___(_) ___| | ___ | | | |_ __| |__ __ _ _ __ __ _
| | | |/ __| |/ _ \ | | | | '__| '_ \ / _` | '_ \ / _` |
| |___| | (__| | (_) || |_| | | | |_) | (_| | | | | (_| |
\____|_|\___|_|\___/ \___/|_| |_.__/ \__,_|_| |_|\__,_|
Aplicacion : ${application.title:ciclourbana}
Version : ${application.version:desarrollo}
Boot : ${spring-boot.version}
Perfil : ${spring.profiles.active:default}
Red municipal de bicicletas electricas de RibaltaVerificación:
./mvnw spring-boot:run
# Muestra el banner con Boot 3.3.5 y perfil "default"
# Desactivarlo temporalmente sin tocar el fichero de propiedades
./mvnw spring-boot:run -Dspring-boot.run.arguments=--spring.main.banner-mode=off
# O de forma permanente en application.properties
echo "spring.main.banner-mode=off" >> src/main/resources/application.propertiesNota: ${application.version} aparecerá como desarrollo al ejecutar con spring-boot:run, porque esa propiedad se lee del MANIFEST.MF y solo existe al ejecutar el JAR empaquetado. Por eso conviene siempre poner un valor por defecto tras los dos puntos.
Solución 2
package com.ciclourbana.comun;
import com.ciclourbana.estaciones.AlmacenEstaciones;
import com.ciclourbana.estaciones.Estacion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* Comprueba al arrancar que la red de Ribalta está en un estado válido.
* Se ejecuta DESPUÉS del cargador de estaciones gracias a @Order(2).
*/
@Component
@Order(2)
public class VerificadorArranque implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(VerificadorArranque.class);
private static final int MINIMO_ESTACIONES = 3;
private final AlmacenEstaciones almacenEstaciones;
public VerificadorArranque(AlmacenEstaciones almacenEstaciones) {
this.almacenEstaciones = almacenEstaciones;
}
@Override
public void run(String... args) {
log.info("Verificando la configuración de la red de Ribalta...");
List<Estacion> estaciones = almacenEstaciones.listarTodas();
// Comprobación 1: número mínimo de estaciones
if (estaciones.size() < MINIMO_ESTACIONES) {
throw new IllegalStateException(
"La red necesita al menos " + MINIMO_ESTACIONES
+ " estaciones para operar, pero solo hay " + estaciones.size()
+ ". Revise el cargador de datos.");
}
// Comprobación 2: ninguna estación sin anclajes
List<String> sinCapacidad = estaciones.stream()
.filter(estacion -> estacion.capacidad() <= 0)
.map(Estacion::nombre)
.toList();
if (!sinCapacidad.isEmpty()) {
throw new IllegalStateException(
"Hay estaciones con capacidad 0, lo que impide devolver bicicletas: "
+ String.join(", ", sinCapacidad));
}
log.info("Verificación superada: {} estaciones válidas, {} anclajes en total",
estaciones.size(), almacenEstaciones.capacidadTotal());
}
}El @Order(2) es imprescindible: sin él, el orden de ejecución entre los dos runners no está garantizado y el verificador podría ejecutarse antes que el cargador, encontrando la red vacía.
Comprobación del fallo: elimina dos estaciones del cargador y arranca. Verás:
... APPLICATION FAILED TO START
...
java.lang.IllegalStateException: La red necesita al menos 3 estaciones para operar,
pero solo hay 2. Revise el cargador de datos.Por qué es preferible fallar al arrancar: es el principio de fail fast. Un fallo en el arranque se detecta en el despliegue, con logs claros, y el orquestador puede revertir a la versión anterior automáticamente sin que ningún usuario lo note. Si en cambio el problema aparece en la primera petición, el despliegue se da por bueno, los usuarios reciben errores 500 intermitentes y el diagnóstico se complica muchísimo. La regla es: valida la configuración crítica al arrancar, nunca en tiempo de petición.
Solución 3
package com.ciclourbana.comun;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
/**
* Registra el tiempo de arranque y el tiempo total de actividad
* de CicloUrbana.
*/
@Component
public class CronometroAplicacion {
private static final Logger log = LoggerFactory.getLogger(CronometroAplicacion.class);
// Momento en que se construye el bean: durante el refresh del contexto
private final Instant instanteCreacion = Instant.now();
private Instant instanteListo;
@EventListener(ApplicationReadyEvent.class)
public void alEstarLista(ApplicationReadyEvent evento) {
this.instanteListo = Instant.now();
// El propio evento sabe cuándo arrancó SpringApplication
long milisegundosTotales = evento.getTimeTaken().toMillis();
log.info("CicloUrbana lista. Arranque completo en {} ms", milisegundosTotales);
log.info("Desde la creación de los beans hasta el arranque: {} ms",
Duration.between(instanteCreacion, instanteListo).toMillis());
}
@EventListener(ContextClosedEvent.class)
public void alCerrar() {
if (instanteListo == null) {
log.warn("CicloUrbana se detiene antes de haber terminado de arrancar");
return;
}
Duration actividad = Duration.between(instanteListo, Instant.now());
log.info("CicloUrbana se detiene tras {} horas, {} minutos y {} segundos de servicio",
actividad.toHours(),
actividad.toMinutesPart(),
actividad.toSecondsPart());
}
}Detalles de la solución:
ApplicationReadyEvent.getTimeTaken()devuelve directamente la duración del arranque, sin necesidad de cronometrar a mano.- La comprobación de
instanteListo == nullcubre el caso de un arranque fallido:ContextClosedEventpuede publicarse sin que se haya llegado aApplicationReadyEvent. toMinutesPart()ytoSecondsPart()(Java 9+) dan el resto dentro de la unidad superior, evitando cálculos manuales con módulos.
Configuración del apagado ordenado:
# src/main/resources/application.properties
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=20s
logging.level.org.springframework.boot.web.embedded.tomcat.GracefulShutdown=INFOVerificación:
# Terminal 1
java -jar target/ciclourbana-0.0.1-SNAPSHOT.jar
# Terminal 2: enviar SIGTERM al proceso (equivale a docker stop)
kill $(pgrep -f ciclourbana)En la terminal 1 verás la secuencia completa:
... o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
... c.c.comun.CronometroAplicacion : CicloUrbana se detiene tras 0 horas, 1 minutos y 34 segundos de servicio
... o.s.b.w.e.tomcat.GracefulShutdown : Graceful shutdown completePrueba a comparar con kill -9 $(pgrep -f ciclourbana): el proceso desaparece sin imprimir nada, porque SIGKILL no puede interceptarse y los hooks de cierre no llegan a ejecutarse.
Conclusión
Ya no hay nada opaco entre el main y el mensaje Started CicloUrbanaApplication. Sabes que SpringApplication.run(...) deduce el tipo de aplicación, prepara el Environment con sus fuentes de propiedades priorizadas, crea el contexto, escanea com.ciclourbana, procesa la autoconfiguración, instancia los beans, arranca Tomcat y va publicando eventos por el camino. Sabes leer cada campo del log de arranque, personalizar el banner y —lo más útil en el día a día— enganchar tu propio código en el momento exacto: un CommandLineRunner con @Order para precargar las estaciones de Ribalta, un listener de ApplicationReadyEvent para informar del estado de la red, y un cierre ordenado con server.shutdown=graceful que no corta alquileres a medias. CicloUrbana tiene ya su primer componente gestionado por Spring, el AlmacenEstaciones, inyectado por constructor en el controlador y en los runners.
Y ahí, precisamente, está la puerta al módulo 2: Conceptos Básicos de Spring Boot. Hemos usado @Component, @RestController y la inyección por constructor como herramientas, sin explicar por qué funcionan. En el módulo 2 abriremos el contenedor: qué significa cada anotación de Spring Boot, cómo se resuelve la inyección de dependencias, qué ámbitos y ciclo de vida tienen los beans, cómo se configura la aplicación mediante propiedades tipadas y, finalmente, cómo la autoconfiguración decide —condición a condición— qué beans registrar. Al terminarlo dejarás de usar Spring Boot por imitación y empezarás a usarlo con criterio.
Curso de Spring Boot
Módulo 1: Introducción a Spring Boot
- ¿Qué es Spring Boot?
- Configuración de tu Entorno de Desarrollo
- Creando tu Primera Aplicación Spring Boot
- Entendiendo la Estructura del Proyecto
- El Arranque y el Ciclo de Vida de la Aplicación
Módulo 2: Conceptos Básicos de Spring Boot
- Anotaciones de Spring Boot
- Inyección de Dependencias en Spring Boot
- Ámbito y Ciclo de Vida de los Beans
- Configuración de Spring Boot
- Propiedades de Spring Boot
- Autoconfiguración y Starters por Dentro
Módulo 3: Construyendo Servicios Web RESTful
- Introducción a los Servicios Web RESTful
- Creando Controladores REST
- Manejo de Métodos HTTP
- Validación de Datos de Entrada
- DTOs y Mapeo entre Capas
- Manejo de Excepciones en REST
- Documentar la API con OpenAPI
Módulo 4: Acceso a Datos con Spring Boot
- Introducción a Spring Data JPA
- Configuración de Fuentes de Datos
- Creación de Entidades JPA
- Relaciones entre Entidades
- Uso de Repositorios de Spring Data
- Métodos de Consulta en Spring Data JPA
- Transacciones y Gestión de la Persistencia
- Migraciones de Esquema con Flyway
Módulo 5: Seguridad en Spring Boot
- Introducción a Spring Security
- Configuración de Spring Security
- Autenticación y Autorización de Usuarios
- Implementación de Autenticación JWT
- Seguridad a Nivel de Método y Endurecimiento de la API
Módulo 6: Pruebas en Spring Boot
- Introducción a las Pruebas
- Pruebas Unitarias con JUnit
- Simulación con Mockito
- Pruebas de Integración
- Pruebas con Testcontainers
Módulo 7: Funciones Avanzadas de Spring Boot
- Spring Boot Actuator
- Perfiles de Spring Boot
- Tareas Programadas y Ejecución Asíncrona
- Spring Boot con Docker
- Spring Boot y Microservicios
- Comunicación entre Servicios y Tolerancia a Fallos
Módulo 8: Despliegue de Aplicaciones Spring Boot
- Introducción al Despliegue
- Desplegando en Heroku
- Desplegando en AWS
- Desplegando en Kubernetes
- Integración y Entrega Continua
Módulo 9: Rendimiento y Monitoreo
- Ajuste de Rendimiento
- Caché con Spring Cache
- Monitoreo con Spring Boot Actuator
- Uso de Prometheus y Grafana
- Gestión de Registros y Logs
- Trazabilidad Distribuida
