Entre que prems Run i veus a la consola Started CicloUrbanaApplication in 2.174 seconds passen moltíssimes coses en un ordre molt concret. Conèixer aquest ordre deixa de ser curiositat acadèmica tan bon punt necessites precarregar dades, executar una comprovació en arrencar, alliberar recursos en aturar o entendre per què un bean no està llest quan el fas servir. En aquesta lliçó obrirem SpringApplication.run(...), aprendrem a llegir el log d'arrencada real de CicloUrbana, personalitzarem el bàner, enganxarem codi als esdeveniments del cicle de vida i configurarem una aturada ordenada que no talli lloguers a mitges.
Contingut
- Què fa exactament
SpringApplication.run(...) - Les fases de l'arrencada, pas a pas
- El bàner i com personalitzar-lo
- Interpretar el log d'arrencada de CicloUrbana
- Els esdeveniments del cicle de vida
- Un listener d'
ApplicationReadyEvent CommandLineRunneriApplicationRunner- Personalitzar l'arrencada amb
SpringApplicationBuilder - Aturada ordenada i hooks de tancament
- Errors Habituals i Consells
- Exercicis
- Què fa exactament
SpringApplication.run(...)
SpringApplication.run(...)Recordem la línia:
public static void main(String[] args) {
SpringApplication.run(CicloUrbanaApplication.class, args);
}Aquest mètode estàtic és una drecera equivalent a:
public static void main(String[] args) {
SpringApplication aplicacio = new SpringApplication(CicloUrbanaApplication.class);
ConfigurableApplicationContext context = aplicacio.run(args);
// El context queda viu: mentre Tomcat escolti, el procés no acaba
}SpringApplication és una classe de Spring Boot —no de Spring Framework— la responsabilitat de la qual és orquestrar l'arrencada: decidir quin tipus de context cal crear, preparar l'entorn, carregar la configuració, publicar esdeveniments i arrencar el servidor si escau.
Retorna un ConfigurableApplicationContext: el contenidor amb tots els beans ja construïts. El pots desar si necessites consultar-lo, encara que a la pràctica gairebé mai no cal.
- Les fases de l'arrencada, pas a pas
Aquest és el recorregut complet, en ordre:
flowchart TD
A["main() invoca SpringApplication.run()"] --> B["1. Crear SpringApplication<br/>Deduir el tipus d'aplicació<br/>(SERVLET / REACTIVE / NONE)"]
B --> C["2. Carregar ApplicationContextInitializer<br/>i ApplicationListener des de spring.factories"]
C --> D["3. Publicar ApplicationStartingEvent"]
D --> E["4. Preparar l'Environment<br/>args, variables d'entorn,<br/>application.properties, perfils"]
E --> F["5. Publicar ApplicationEnvironmentPreparedEvent"]
F --> G["6. Imprimir el bàner"]
G --> H["7. Crear l'ApplicationContext<br/>AnnotationConfigServletWebServerApplicationContext"]
H --> I["8. Aplicar els ApplicationContextInitializer"]
I --> J["9. Publicar ApplicationContextInitializedEvent"]
J --> K["10. Carregar les definicions de beans<br/>@ComponentScan des de com.ciclourbana"]
K --> L["11. Publicar ApplicationPreparedEvent"]
L --> M["12. refresh(): processar l'autoconfiguració<br/>i crear els beans singleton"]
M --> N["13. Arrencar el servidor web incrustat<br/>Tomcat al port 8080"]
N --> O["14. Publicar ApplicationStartedEvent"]
O --> P["15. Executar ApplicationRunner<br/>i CommandLineRunner"]
P --> Q["16. Publicar ApplicationReadyEvent"]
Q --> R["Aplicació en marxa"]
Ens aturarem als passos que tenen més conseqüències pràctiques.
Pas 1: deduir el tipus d'aplicació
Spring Boot inspecciona el classpath i decideix:
| Què troba | Tipus deduït | Conseqüència |
|---|---|---|
DispatcherServlet (Spring MVC) |
SERVLET |
Arrenca Tomcat; el procés queda viu |
WebFlux sense Spring MVC |
REACTIVE |
Arrenca Netty |
| Cap dels dos | NONE |
No hi ha servidor; el procés acaba en acabar el main |
CicloUrbana té spring-boot-starter-web, així que és SERVLET. Això explica per què a la lliçó 01-03 dèiem que sense la dependència web l'aplicació arrenca i mor: el tipus deduït és NONE i res no manté viu el procés.
Pas 4: preparar l'Environment
L'Environment és l'objecte que respon a "quant val la propietat X?". Es construeix combinant múltiples fonts amb prioritats. Les principals, de major a menor prioritat:
- Arguments de línia d'ordres (
--server.port=9090) - Propietats del sistema Java (
-Dserver.port=9090) - Variables d'entorn (
SERVER_PORT=9090) application-{perfil}.propertiesapplication.properties- Valors per defecte del codi
Per això java -jar ciclourbana.jar --server.port=9090 guanya sobre el que hi ha escrit a application.properties: els arguments són més amunt a la llista. El detall complet del sistema de propietats és el tema de la lliçó 02-05.
Pas 10: carregar les definicions de beans
Aquí actua el @ComponentScan que vam estudiar a la lliçó 01-04: Spring recorre com.ciclourbana i els seus subpaquets buscant anotacions. Troba EstacioController i el registra com a definició de bean. Compte: registrar la definició no és crear l'objecte; això passa al pas següent.
Pas 12: refresh() i la creació de beans
És el pas més llarg i on es concentra el temps d'arrencada. Hi passen dues coses:
- Es processen les classes d'autoconfiguració: centenars de classes condicionals decideixen quins beans cal registrar segons el que hi hagi al classpath i a les propietats. Aquí és on es crea el
DispatcherServlet, el convertidor JSON de Jackson, elTomcatServletWebServerFactory... Com funcionen aquestes condicions es veu a la lliçó 02-06. - S'instancien els beans singleton: Spring construeix els objectes resolent l'ordre de dependències. Si
LloguerServicenecessitaEstacioService, aquest es crea primer.
Pas 13: arrencar Tomcat
Amb els beans llestos, s'arrenca el servidor i s'obre el port. A partir d'aquí l'aplicació ja accepta peticions HTTP, encara que quedin passos per executar. És un matís important: els CommandLineRunner del pas 15 s'executen amb el port ja obert.
- El bàner i com personalitzar-lo
El primer que apareix a la consola és el bàner de Spring. No compleix cap funció tècnica, però és un bon lloc per posar-hi el nom i la versió de la teva aplicació, cosa que s'agraeix quan tens diverses instàncies obertes.
Crea src/main/resources/banner.txt:
_____ _ _ _ _ _
/ ____(_) | | | | | | | |
| | _ ___| | ___ | | | |_ __| |__ __ _ _ __ __ _
| | | |/ __| |/ _ \| | | | '__| '_ \ / _` | '_ \ / _` |
| |____| | (__| | (_) | |_| | | | |_) | (_| | | | | (_| |
\_____|_|\___|_|\___/ \___/|_| |_.__/ \__,_|_| |_|\__,_|
Xarxa municipal de bicicletes electriques de Ribalta
Versio de l'aplicacio : ${application.version:sense-versio}
Spring Boot : ${spring-boot.version}
Perfils actius : ${spring.profiles.active:cap}Spring Boot substitueix aquests marcadors:
| Marcador | Valor |
|---|---|
${application.version} |
La version del pom.xml, disponible en executar el JAR |
${application.title} |
El name del projecte |
${spring-boot.version} |
La versió de Spring Boot |
${spring.profiles.active} |
Els perfils actius (mòdul 7) |
${AnsiColor.GREEN} |
Color ANSI en terminals compatibles |
La sintaxi ${clau:valorPerDefecte} evita que aparegui el literal quan la propietat no existeix. És útil perquè application.version només té valor si s'executa des del JAR (el llegeix del MANIFEST.MF), no amb spring-boot:run.
Per controlar el bàner:
# Desactivar-lo del tot
spring.main.banner-mode=off
# Enviar-lo al log en lloc de a la consola
spring.main.banner-mode=log
# Fer servir un fitxer amb un altre nom o ubicació
spring.banner.location=classpath:banners/ciclourbana.txtEn producció l'habitual és off o log: el bàner embruta els agregadors de logs estructurats.
- Interpretar el log d'arrencada de CicloUrbana
Aquesta és una arrencada real, línia a línia:
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)Anatomia d'una línia 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
└──────── data i hora ───────┘ └nivell┘ └PID┘ └ app ─┘ └── fil ──┘ └──── classe (abreujada) ────┘ └─ missatge ─┘- PID: l'identificador de procés del sistema operatiu. Serveix per a
killo per localitzar el procés. - Nom de l'aplicació: ve de
spring.application.name, que vam fixar a la lliçó anterior. - Fil:
restartedMainindica que DevTools està actiu. Sense DevTools seriamain. Quan arribi una petició HTTP veuràshttp-nio-8080-exec-1. - Classe abreujada:
o.s.b.w.embedded.tomcat.TomcatWebServerésorg.springframework.boot.web.embedded.tomcat.TomcatWebServer. Spring abreuja els paquets perquè hi càpiga el missatge.
Les línies que donen més informació
| Línia | Què t'està dient |
|---|---|
Starting ... using Java 21.0.5 with PID |
Confirma la versió de Java real i el directori de treball |
No active profile set |
No hi ha perfil actiu; es fa servir default. Molt útil per detectar desplegaments mal configurats |
Devtools property defaults active! |
DevTools està aplicant valors de desenvolupament. Mai no ha d'aparèixer en producció |
Tomcat initialized with port 8080 |
El servidor s'ha creat, però encara no escolta |
Root WebApplicationContext: initialization completed in 1920 ms |
Temps de creació de tots els beans: si l'arrencada és lenta, aquí hi ha la major part |
Tomcat started on port 8080 |
Ara sí que accepta peticions |
Started ... in 2.174 seconds (process running for 2.512) |
2,174 s des de SpringApplication.run(); 2,512 s des que va arrencar la JVM. La diferència és l'arrencada de la JVM mateixa |
Per veure més detall durant un diagnòstic:
# Mostra un informe de quines autoconfiguracions s'han aplicat i quines no
debug=true
# Traça el procés d'arrencada de Spring
logging.level.org.springframework.boot.autoconfigure=DEBUG
- Els esdeveniments del cicle de vida
Spring Boot publica esdeveniments en moments concrets de l'arrencada i del tancament. T'hi pots subscriure per executar codi en l'instant exacte que necessites.
| Esdeveniment | Quan es publica | Hi ha context? | Ús típic |
|---|---|---|---|
ApplicationStartingEvent |
Al principi de tot | No | Registrar listeners molt primerencs |
ApplicationEnvironmentPreparedEvent |
Environment llest, context encara no |
No (sí Environment) |
Modificar o validar propietats |
ApplicationContextInitializedEvent |
Context creat, beans sense carregar | Sí (buit) | Registrar beans mitjançant programació |
ApplicationPreparedEvent |
Definicions carregades, beans sense crear | Sí | Última oportunitat de modificar definicions |
ApplicationStartedEvent |
Context refrescat, runners encara no executats | Sí | Comprovacions prèvies a la càrrega de dades |
ApplicationReadyEvent |
Tot llest, runners executats | Sí | Precàrrega, avisos, tasques d'arrencada |
ApplicationFailedEvent |
L'arrencada ha fallat | Potser sí | Alertes, diagnòstic |
ContextClosedEvent |
El context s'està tancant | Sí | Alliberar recursos, avisar de l'aturada |
La distinció clau és entre ApplicationStartedEvent i ApplicationReadyEvent:
ApplicationStartedEvent: l'aplicació funciona, però elsCommandLineRunnerencara no s'han executat.ApplicationReadyEvent: tot ha acabat, inclosos els runners. És l'esdeveniment que has de fer servir en el 90 % dels casos.
Per als esdeveniments previs a l'existència del context (ApplicationStartingEvent, ApplicationEnvironmentPreparedEvent) no n'hi ha prou d'anotar un mètode: el contenidor encara no existeix i no hi ha beans. Cal registrar-los mitjançant programació (apartat 8) o declarar-los a META-INF/spring.factories.
- Un listener d'
ApplicationReadyEvent
ApplicationReadyEventAfegirem a CicloUrbana un listener que, tan bon punt l'aplicació estigui llesta, registri al log quantes estacions té carregades la xarxa de Ribalta.
Primer necessitem que les estacions deixin d'estar dins del controlador i visquin en un component propi. Creem src/main/java/com/ciclourbana/estacions/MagatzemEstacions.java:
package com.ciclourbana.estacions;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* Magatzem en memòria de les estacions de la xarxa de Ribalta.
* És una solució provisional fins al mòdul 4, on es
* substituirà per un repositori de Spring Data JPA.
*/
@Component
public class MagatzemEstacions {
private final List<Estacio> estacions = new ArrayList<>();
public List<Estacio> llistarTotes() {
return List.copyOf(estacions); // còpia immutable: ningú no la modifica des de fora
}
public void afegir(Estacio estacio) {
estacions.add(estacio);
}
public int comptar() {
return estacions.size();
}
public int capacitatTotal() {
return estacions.stream()
.mapToInt(Estacio::capacitat)
.sum();
}
}@Component li indica a Spring que ha de crear una única instància d'aquesta classe i gestionar-la com a bean. Com que és a com.ciclourbana.estacions, l'escaneig la troba. El detall dels estereotips i la injecció de dependències es veu al mòdul 2; aquí el fem servir com a eina.
Ara el listener, a src/main/java/com/ciclourbana/comu/AvisArrencada.java:
package com.ciclourbana.comu;
import com.ciclourbana.estacions.MagatzemEstacions;
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 al log un resum de l'estat de la xarxa
* tan bon punt CicloUrbana acaba d'arrencar.
*/
@Component
public class AvisArrencada {
private static final Logger log = LoggerFactory.getLogger(AvisArrencada.class);
private final MagatzemEstacions magatzemEstacions;
private final Environment entorn;
// Injecció per constructor: Spring passa els beans necessaris en crear aquesta classe
public AvisArrencada(MagatzemEstacions magatzemEstacions, Environment entorn) {
this.magatzemEstacions = magatzemEstacions;
this.entorn = entorn;
}
@EventListener(ApplicationReadyEvent.class)
public void enEstarLlesta() {
String port = entorn.getProperty("server.port", "8080");
log.info("================================================");
log.info(" CicloUrbana llesta - xarxa municipal de Ribalta");
log.info(" Estacions carregades : {}", magatzemEstacions.comptar());
log.info(" Capacitat total : {} ancoratges", magatzemEstacions.capacitatTotal());
log.info(" API disponible a : http://localhost:{}/api/v1/estacions", port);
log.info("================================================");
}
}Punts a destacar:
@EventListener(ApplicationReadyEvent.class)n'hi ha prou per subscriure-s'hi. És més net que implementar la interfícieApplicationListener, i el mètode es pot dir com vulguis.- El logger de SLF4J és la manera correcta de registrar missatges a Spring Boot; no facis servir mai
System.out.println. SLF4J i Logback vénen inclosos aspring-boot-starter. - Les claus
{}són marcadors de posició de SLF4J. Són preferibles a la concatenació amb+perquè només es resolen si el nivell de log està actiu. - La injecció per constructor (rebre les dependències com a paràmetres) és la pràctica recomanada. Mòdul 2, lliçó 02-02.
CommandLineRunner i ApplicationRunner
CommandLineRunner i ApplicationRunnerTotes dues són interfícies funcionals que Spring Boot executa després que el context estigui llest i abans de publicar ApplicationReadyEvent. Serveixen per executar codi d'inicialització.
| Aspecte | CommandLineRunner |
ApplicationRunner |
|---|---|---|
| Signatura | void run(String... args) |
void run(ApplicationArguments args) |
| Arguments | Array de cadenes sense processar | Objecte que distingeix opcions (--clau=valor) d'arguments solts |
| Mètodes útils | Els d'un array | getOptionNames(), getOptionValues("x"), getNonOptionArgs() |
| Quan triar-lo | No necessites llegir arguments, o són simples | Necessites interpretar opcions amb nom |
| Moment d'execució | Després de l'arrencada, abans d'ApplicationReadyEvent |
El mateix |
| Ordre entre diversos | Amb @Order o la interfície Ordered |
Igual |
Precarregar les estacions de demostració
Creem src/main/java/com/ciclourbana/estacions/CarregadorEstacionsDemo.java:
package com.ciclourbana.estacions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* Carrega les estacions inicials de la xarxa de Ribalta en arrencar.
* Provisional fins al mòdul 4, on les dades vindran de la
* base de dades mitjançant migracions de Flyway.
*/
@Component
@Order(1) // s'executa abans que altres runners amb @Order superior
public class CarregadorEstacionsDemo implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(CarregadorEstacionsDemo.class);
private final MagatzemEstacions magatzemEstacions;
public CarregadorEstacionsDemo(MagatzemEstacions magatzemEstacions) {
this.magatzemEstacions = magatzemEstacions;
}
@Override
public void run(String... args) {
log.info("Carregant estacions de demostració de Ribalta...");
magatzemEstacions.afegir(new Estacio(1L, "Plaça Major",
"Plaça Major, 1", 24, 40.4168, -3.7038));
magatzemEstacions.afegir(new Estacio(2L, "Estació Nord",
"Avinguda Estació 3", 30, 40.4290, -3.7020));
magatzemEstacions.afegir(new Estacio(3L, "Parc del Riu",
"Passeig Fluvial 12", 18, 40.4105, -3.6950));
magatzemEstacions.afegir(new Estacio(4L, "Universitat",
"Campus Sud, accés B", 36, 40.4402, -3.7255));
log.info("Carregades {} estacions", magatzemEstacions.comptar());
}
}I actualitzem el controlador perquè faci servir el magatzem en lloc de la seva llista fixa:
package com.ciclourbana.estacions;
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/estacions")
public class EstacioController {
private final MagatzemEstacions magatzemEstacions;
public EstacioController(MagatzemEstacions magatzemEstacions) {
this.magatzemEstacions = magatzemEstacions;
}
@GetMapping
public List<Estacio> llistarEstacions() {
return magatzemEstacions.llistarTotes();
}
}En arrencar, el log mostra ara l'ordre real dels esdeveniments:
... o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http)
... c.c.e.CarregadorEstacionsDemo : Carregant estacions de demostració de Ribalta...
... c.c.e.CarregadorEstacionsDemo : Carregades 4 estacions
... c.c.CicloUrbanaApplication : Started CicloUrbanaApplication in 2.301 seconds
... c.c.comu.AvisArrencada : ================================================
... c.c.comu.AvisArrencada : CicloUrbana llesta - xarxa municipal de Ribalta
... c.c.comu.AvisArrencada : Estacions carregades : 4
... c.c.comu.AvisArrencada : Capacitat total : 108 ancoratges
... c.c.comu.AvisArrencada : API disponible a : http://localhost:8080/api/v1/estacions
... c.c.comu.AvisArrencada : ================================================Observa la seqüència: Tomcat arrenca primer, després el CommandLineRunner, després el missatge Started i per últim el listener d'ApplicationReadyEvent. Coincideix exactament amb els passos 13 a 16 del diagrama.
Exemple amb ApplicationRunner
Quan necessites llegir opcions amb nom:
package com.ciclourbana.comu;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
/**
* Exemple de lectura d'opcions de línia d'ordres.
* Ús: java -jar ciclourbana.jar --mode=manteniment --verbose
*/
@Component
public class LectorArguments implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(LectorArguments.class);
@Override
public void run(ApplicationArguments args) {
// Opcions amb nom, de l'estil --clau=valor
log.info("Opcions rebudes: {}", args.getOptionNames());
if (args.containsOption("mode")) {
// getOptionValues retorna una llista: una opció es pot repetir
String mode = args.getOptionValues("mode").get(0);
log.info("Arrencant en mode: {}", mode);
}
// Arguments solts, sense el prefix --
log.info("Arguments sense nom: {}", args.getNonOptionArgs());
}
}Amb CommandLineRunner rebries l'array cru ["--mode=manteniment", "--verbose"] i hauries de trossejar les cadenes a mà. Aquesta és tota la diferència entre les dues interfícies.
Un avís important: si un runner llança una excepció, l'arrencada falla i l'aplicació s'atura. És un comportament desitjable —si la precàrrega de dades crítica falla, més val no arrencar— però convé tenir-ho present i capturar allò que no hagi de ser fatal.
- Personalitzar l'arrencada amb
SpringApplicationBuilder
SpringApplicationBuilderSpringApplicationBuilder ofereix una API fluida per configurar l'arrencada mitjançant programació, útil quan la configuració no es pot expressar en propietats.
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)
// Sense bàner: els agregadors de logs ho agraeixen
.bannerMode(Banner.Mode.OFF)
// Forçar el tipus d'aplicació en lloc de deduir-lo del classpath
.web(WebApplicationType.SERVLET)
// Perfil actiu per defecte si no se n'indica cap altre (mòdul 7)
.profiles("default")
// Propietats per defecte: les de menor prioritat de totes
.properties("spring.application.name=ciclourbana")
// Listener per a un esdeveniment anterior a la creació del context
.listeners(esdeveniment -> {
if (esdeveniment instanceof org.springframework.boot.context.event.ApplicationStartingEvent) {
System.out.println(">> Arrencant CicloUrbana...");
}
})
// Registrar el hook d'aturada de la JVM (actiu per defecte)
.registerShutdownHook(true)
.run(args);
}
}Repàs de les opcions més útils:
| Mètode | Efecte |
|---|---|
bannerMode(Mode.OFF / LOG / CONSOLE) |
Controla el bàner des del codi |
web(WebApplicationType.NONE) |
Arrenca sense servidor: processos per lots o eines CLI |
profiles("dev", "local") |
Activa perfils addicionals (mòdul 7) |
properties("clau=valor") |
Propietats per defecte, amb la prioritat més baixa |
listeners(...) |
Registra listeners d'esdeveniments primerencs, abans que existeixi el context |
logStartupInfo(false) |
Omet les línies Starting... i Started... |
parent(...) / child(...) |
Jerarquies de contextos, poc freqüents |
Un cas realista per a CicloUrbana: una eina que genera l'informe mensual d'ús, reutilitzant tot el codi del projecte però sense arrencar el servidor web.
new SpringApplicationBuilder(CicloUrbanaApplication.class)
.web(WebApplicationType.NONE) // sense Tomcat: el procés acaba en acabar
.bannerMode(Banner.Mode.OFF)
.run(args);En ser de tipus NONE, el main acaba quan acaben els runners i la JVM es tanca sola. Exactament el que vols en una tasca programada.
- Aturada ordenada i hooks de tancament
Aturar una aplicació de cop en producció significa tallar peticions a mitges. A CicloUrbana això podria ser un lloguer que s'inicia però que no es registra mai.
Aturada ordenada (graceful shutdown)
# src/main/resources/application.properties
# En rebre el senyal d'aturada, deixar d'acceptar peticions noves
# i esperar que acabin les que estan en curs
server.shutdown=graceful
# Temps màxim d'espera abans de forçar el tancament (per defecte 30s)
spring.lifecycle.timeout-per-shutdown-phase=20sAmb server.shutdown=graceful, en rebre un SIGTERM (el que envia docker stop o Kubernetes) passa això:
sequenceDiagram
participant OS as Sistema operatiu
participant SB as Spring Boot
participant T as Tomcat
participant P as Peticions en curs
OS->>SB: SIGTERM
SB->>T: Deixar d'acceptar connexions noves
Note over T: Les peticions noves es rebutgen
SB->>P: Esperar fins a 20 s
P-->>SB: Peticions acabades
SB->>SB: Publicar ContextClosedEvent
SB->>SB: Destruir beans singleton
SB->>OS: Procés finalitzat
Al log veurà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 per defecte és immediate, que talla a l'instant. En qualsevol desplegament real has d'activar graceful.
Reaccionar al tancament
Pots executar codi abans que l'aplicació desaparegui:
package com.ciclourbana.comu;
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 AvisAturada {
private static final Logger log = LoggerFactory.getLogger(AvisAturada.class);
@EventListener(ContextClosedEvent.class)
public void enTancar() {
log.info("CicloUrbana s'està aturant. Tancant recursos oberts...");
// Aquí alliberaries connexions externes, bolcaries memòries cau pendents, etc.
}
}Existeix una alternativa a nivell de bean, l'anotació @PreDestroy, que Spring invoca just abans de destruir cada objecte. Forma part del cicle de vida dels beans i s'estudia a la lliçó 02-03.
El hook de tancament de la JVM
Spring Boot registra automàticament un shutdown hook a la JVM. És el que garanteix que el context es tanqui netament quan:
- Prems
Ctrl+Cal terminal. - L'orquestrador envia
SIGTERM(docker stop,kubectl delete pod). - El procés acaba de manera normal.
Important: kill -9 (SIGKILL) no es pot interceptar. El procés mor a l'instant sense executar cap hook. Per això Kubernetes envia primer SIGTERM i només recorre a SIGKILL si l'aplicació no acaba dins del termini de gràcia.
Per desactivar el hook —molt rarament necessari—:
Errors Habituals i Consells
- Fer servir
ApplicationStartedEventquan es voliaApplicationReadyEvent. Amb el primer, elsCommandLineRunnerencara no s'han executat, així que les dades que esperes precarregades encara no hi són. En cas de dubte, fes servirApplicationReadyEvent. - Posar lògica pesada en un
CommandLineRunner. Tot el que hi facis endarrereix l'arrencada, i a Kubernetes una arrencada lenta pot provocar reinicis en bucle per fallada de la sonda de disponibilitat. Per a feina llarga, llança una tasca asíncrona (mòdul 7). - Subscriure's amb
@EventListenera esdeveniments anteriors al context.ApplicationStartingEventiApplicationEnvironmentPreparedEventes publiquen quan el contenidor encara no existeix: un bean anotat no els rebrà mai. Registra'ls ambSpringApplicationBuilder.listeners(...). - Confondre el temps
Started ... in X secondsamb el temps total. El parèntesi(process running for Y)inclou l'arrencada de la JVM. La diferència entre tots dos no depèn del teu codi. - Oblidar
server.shutdown=gracefulen producció. És una línia que evita errors intermitents durant cada desplegament, difícils de reproduir i de diagnosticar. - No entendre
restartedMain. Aquest nom de fil indica que DevTools està actiu. Si apareix en un entorn que no és el teu portàtil, tens un problema d'empaquetat. - Consell — arrenca amb
debug=trueun cop. L'informe d'autoconfiguració que imprimeix ensenya moltíssim sobre el que Spring Boot està decidint per tu. - Consell — logs d'arrencada informatius. Un
AvisArrencadaque resumeixi l'estat del sistema estalvia hores de diagnòstic quan alguna cosa va malament en un servidor remot.
Exercicis
Exercici 1
Crea un banner.txt personalitzat per a CicloUrbana que mostri el nom de l'aplicació, la versió de Spring Boot i el perfil actiu. Comprova després que spring.main.banner-mode=off el desactiva.
Exercici 2
Implementa un CommandLineRunner anomenat VerificadorArrencada que comprovi en iniciar que la xarxa té com a mínim 3 estacions i que cap no té capacitat 0. Si alguna comprovació falla, ha d'impedir l'arrencada amb un missatge clar. Explica per què és preferible fallar en arrencar abans que descobrir el problema a la primera petició.
Exercici 3
Afegeix a CicloUrbana un listener d'ApplicationReadyEvent que registri el temps total d'arrencada i un listener de ContextClosedEvent que registri quant de temps ha estat l'aplicació en marxa. Activa a més l'aturada ordenada i verifica'n el funcionament al log.
Solucions
Solució 1
# src/main/resources/banner.txt
____ _ _ _ _ _
/ ___(_) ___| | ___ | | | |_ __| |__ __ _ _ __ __ _
| | | |/ __| |/ _ \ | | | | '__| '_ \ / _` | '_ \ / _` |
| |___| | (__| | (_) || |_| | | | |_) | (_| | | | | (_| |
\____|_|\___|_|\___/ \___/|_| |_.__/ \__,_|_| |_|\__,_|
Aplicacio : ${application.title:ciclourbana}
Versio : ${application.version:desenvolupament}
Boot : ${spring-boot.version}
Perfil : ${spring.profiles.active:default}
Xarxa municipal de bicicletes electriques de RibaltaVerificació:
./mvnw spring-boot:run
# Mostra el bàner amb Boot 3.3.5 i perfil "default"
# Desactivar-lo temporalment sense tocar el fitxer de propietats
./mvnw spring-boot:run -Dspring-boot.run.arguments=--spring.main.banner-mode=off
# O de manera permanent a application.properties
echo "spring.main.banner-mode=off" >> src/main/resources/application.propertiesNota: ${application.version} apareixerà com a desenvolupament en executar amb spring-boot:run, perquè aquesta propietat es llegeix del MANIFEST.MF i només existeix en executar el JAR empaquetat. Per això convé posar sempre un valor per defecte després dels dos punts.
Solució 2
package com.ciclourbana.comu;
import com.ciclourbana.estacions.MagatzemEstacions;
import com.ciclourbana.estacions.Estacio;
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;
/**
* Comprova en arrencar que la xarxa de Ribalta és en un estat vàlid.
* S'executa DESPRÉS del carregador d'estacions gràcies a @Order(2).
*/
@Component
@Order(2)
public class VerificadorArrencada implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(VerificadorArrencada.class);
private static final int MINIM_ESTACIONS = 3;
private final MagatzemEstacions magatzemEstacions;
public VerificadorArrencada(MagatzemEstacions magatzemEstacions) {
this.magatzemEstacions = magatzemEstacions;
}
@Override
public void run(String... args) {
log.info("Verificant la configuració de la xarxa de Ribalta...");
List<Estacio> estacions = magatzemEstacions.llistarTotes();
// Comprovació 1: nombre mínim d'estacions
if (estacions.size() < MINIM_ESTACIONS) {
throw new IllegalStateException(
"La xarxa necessita com a mínim " + MINIM_ESTACIONS
+ " estacions per operar, però només n'hi ha " + estacions.size()
+ ". Reviseu el carregador de dades.");
}
// Comprovació 2: cap estació sense ancoratges
List<String> senseCapacitat = estacions.stream()
.filter(estacio -> estacio.capacitat() <= 0)
.map(Estacio::nom)
.toList();
if (!senseCapacitat.isEmpty()) {
throw new IllegalStateException(
"Hi ha estacions amb capacitat 0, cosa que impedeix retornar bicicletes: "
+ String.join(", ", senseCapacitat));
}
log.info("Verificació superada: {} estacions vàlides, {} ancoratges en total",
estacions.size(), magatzemEstacions.capacitatTotal());
}
}L'@Order(2) és imprescindible: sense ell, l'ordre d'execució entre els dos runners no està garantit i el verificador es podria executar abans que el carregador, trobant la xarxa buida.
Comprovació de la fallada: elimina dues estacions del carregador i arrenca. Veuràs:
... APPLICATION FAILED TO START
...
java.lang.IllegalStateException: La xarxa necessita com a mínim 3 estacions per operar,
però només n'hi ha 2. Reviseu el carregador de dades.Per què és preferible fallar en arrencar: és el principi de fail fast. Una fallada a l'arrencada es detecta al desplegament, amb logs clars, i l'orquestrador pot revertir a la versió anterior automàticament sense que cap usuari se n'adoni. Si, en canvi, el problema apareix a la primera petició, el desplegament es dona per bo, els usuaris reben errors 500 intermitents i el diagnòstic es complica moltíssim. La regla és: valida la configuració crítica en arrencar, mai en temps de petició.
Solució 3
package com.ciclourbana.comu;
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 temps d'arrencada i el temps total d'activitat
* de CicloUrbana.
*/
@Component
public class CronometreAplicacio {
private static final Logger log = LoggerFactory.getLogger(CronometreAplicacio.class);
// Moment en què es construeix el bean: durant el refresh del context
private final Instant instantCreacio = Instant.now();
private Instant instantLlest;
@EventListener(ApplicationReadyEvent.class)
public void enEstarLlesta(ApplicationReadyEvent esdeveniment) {
this.instantLlest = Instant.now();
// El mateix esdeveniment sap quan va arrencar SpringApplication
long millisegonsTotals = esdeveniment.getTimeTaken().toMillis();
log.info("CicloUrbana llesta. Arrencada completa en {} ms", millisegonsTotals);
log.info("Des de la creació dels beans fins a l'arrencada: {} ms",
Duration.between(instantCreacio, instantLlest).toMillis());
}
@EventListener(ContextClosedEvent.class)
public void enTancar() {
if (instantLlest == null) {
log.warn("CicloUrbana s'atura abans d'haver acabat d'arrencar");
return;
}
Duration activitat = Duration.between(instantLlest, Instant.now());
log.info("CicloUrbana s'atura després de {} hores, {} minuts i {} segons de servei",
activitat.toHours(),
activitat.toMinutesPart(),
activitat.toSecondsPart());
}
}Detalls de la solució:
ApplicationReadyEvent.getTimeTaken()retorna directament la durada de l'arrencada, sense necessitat de cronometrar a mà.- La comprovació d'
instantLlest == nullcobreix el cas d'una arrencada fallida:ContextClosedEventes pot publicar sense que s'hagi arribat aApplicationReadyEvent. toMinutesPart()itoSecondsPart()(Java 9+) donen la resta dins de la unitat superior, evitant càlculs manuals amb mòduls.
Configuració de l'aturada ordenada:
# 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ó:
# Terminal 1
java -jar target/ciclourbana-0.0.1-SNAPSHOT.jar
# Terminal 2: enviar SIGTERM al procés (equival a docker stop)
kill $(pgrep -f ciclourbana)Al terminal 1 veuràs la seqüència completa:
... o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
... c.c.comu.CronometreAplicacio : CicloUrbana s'atura després de 0 hores, 1 minuts i 34 segons de servei
... o.s.b.w.e.tomcat.GracefulShutdown : Graceful shutdown completeProva a comparar-ho amb kill -9 $(pgrep -f ciclourbana): el procés desapareix sense imprimir res, perquè SIGKILL no es pot interceptar i els hooks de tancament no s'arriben a executar.
Conclusió
Ja no hi ha res opac entre el main i el missatge Started CicloUrbanaApplication. Saps que SpringApplication.run(...) dedueix el tipus d'aplicació, prepara l'Environment amb les seves fonts de propietats prioritzades, crea el context, escaneja com.ciclourbana, processa l'autoconfiguració, instancia els beans, arrenca Tomcat i va publicant esdeveniments pel camí. Saps llegir cada camp del log d'arrencada, personalitzar el bàner i —el més útil en el dia a dia— enganxar el teu propi codi en el moment exacte: un CommandLineRunner amb @Order per precarregar les estacions de Ribalta, un listener d'ApplicationReadyEvent per informar de l'estat de la xarxa, i un tancament ordenat amb server.shutdown=graceful que no talla lloguers a mitges. CicloUrbana ja té el seu primer component gestionat per Spring, el MagatzemEstacions, injectat per constructor al controlador i als runners.
I aquí, precisament, hi ha la porta al mòdul 2: Conceptes Bàsics de Spring Boot. Hem fet servir @Component, @RestController i la injecció per constructor com a eines, sense explicar per què funcionen. Al mòdul 2 obrirem el contenidor: què significa cada anotació de Spring Boot, com es resol la injecció de dependències, quins àmbits i cicle de vida tenen els beans, com es configura l'aplicació mitjançant propietats tipades i, finalment, com l'autoconfiguració decideix —condició a condició— quins beans cal registrar. En acabar-lo deixaràs de fer servir Spring Boot per imitació i començaràs a fer-lo servir amb criteri.
Curs de Spring Boot
Mòdul 1: Introducció a Spring Boot
- Què és Spring Boot?
- Configuració del teu entorn de desenvolupament
- Creant la teva primera aplicació Spring Boot
- Entenent l'estructura del projecte
- L'arrencada i el cicle de vida de l'aplicació
Mòdul 2: Conceptes bàsics de Spring Boot
- Anotacions de Spring Boot
- Injecció de dependències a Spring Boot
- Àmbit i cicle de vida dels beans
- Configuració de Spring Boot
- Propietats de Spring Boot
- Autoconfiguració i starters per dins
Mòdul 3: Construint serveis web RESTful
- Introducció als serveis web RESTful
- Creant controladors REST
- Gestió dels mètodes HTTP
- Validació de dades d'entrada
- DTOs i mapatge entre capes
- Gestió d'excepcions a REST
- Documentar l'API amb OpenAPI
Mòdul 4: Accés a dades amb Spring Boot
- Introducció a Spring Data JPA
- Configuració de fonts de dades
- Creació d'entitats JPA
- Relacions entre entitats
- Ús de repositoris de Spring Data
- Mètodes de consulta a Spring Data JPA
- Transaccions i gestió de la persistència
- Migracions d'esquema amb Flyway
Mòdul 5: Seguretat a Spring Boot
- Introducció a Spring Security
- Configuració de Spring Security
- Autenticació i autorització d'usuaris
- Implementació d'autenticació JWT
- Seguretat a nivell de mètode i enduriment de l'API
Mòdul 6: Proves a Spring Boot
- Introducció a les proves
- Proves unitàries amb JUnit
- Simulació amb Mockito
- Proves d'integració
- Proves amb Testcontainers
Mòdul 7: Funcions avançades de Spring Boot
- Spring Boot Actuator
- Perfils de Spring Boot
- Tasques programades i execució asíncrona
- Spring Boot amb Docker
- Spring Boot i microserveis
- Comunicació entre serveis i tolerància a fallades
Mòdul 8: Desplegament d'aplicacions Spring Boot
- Introducció al desplegament
- Desplegant a Heroku
- Desplegant a AWS
- Desplegant a Kubernetes
- Integració i lliurament continus
Mòdul 9: Rendiment i monitoratge
- Ajust de rendiment
- Memòria cau amb Spring Cache
- Monitoratge amb Spring Boot Actuator
- Ús de Prometheus i Grafana
- Gestió de registres i logs
- Traçabilitat distribuïda
