CicloUrbana és plena de números escrits a foc: 0.50 de desbloqueig, 0.12 per minut, 15 minuts gratuïts per a estudiants, 8 ancoratges mínims per estació. Canviar qualsevol d'ells exigeix avui recompilar i tornar a desplegar. Això no és acceptable: l'ajuntament de Ribalta canvia les seves tarifes cada temporada i el port del servidor no és el mateix al teu portàtil que al servidor de producció. La solució és externalitzar la configuració, i Spring Boot té per a això un dels mecanismes més complets —i més incompresos— de l'ecosistema Java. En aquesta lliçó veurem què és l'Environment i d'on treu els seus valors, l'ordre exacte de precedència entre les fonts, les diferències reals entre .properties i .yaml, com Spring relaxa els noms de les propietats perquè CICLOURBANA_TARIFA_BASE i ciclourbana.tarifa-base siguin el mateix, com llegir valors amb @Value, i per què una credencial no pot ser mai al repositori.
Contingut
- L'
Environmenti elsPropertySource - L'ordre de precedència de les fonts
- Demostrar la precedència a la pràctica
.propertiesdavant de.yaml- Relaxació de noms (relaxed binding)
- Llegir propietats amb
@Value - SpEL i valors per defecte
- Les propietats que utilitza CicloUrbana
- Configuració externa:
spring.config.importispring.config.location - Secrets i credencials
- Errors Comuns i Consells
- Exercicis
- L'
Environment i els PropertySource
Environment i els PropertySourceA la lliçó 01-05 vam veure que una de les primeres fases de l'arrencada és "preparar l'Environment". Ara podem concretar què significa.
L'Environment és un bean de Spring que respon a dues preguntes: quin valor té aquesta propietat? i quins perfils estan actius? (els perfils s'estudien a la lliçó 07-02). Internament no desa cap valor: manté una llista ordenada de PropertySource i, quan li preguntes per una clau, els recorre en ordre i retorna el primer que la tingui.
flowchart TD
E["Environment.getProperty('server.port')"] --> PS["MutablePropertySources<br/>(llista ORDENADA)"]
PS --> P1["1. commandLineArgs<br/>--server.port=9090"]
P1 -->|no la té| P2["2. systemEnvironment<br/>SERVER_PORT"]
P2 -->|no la té| P3["3. systemProperties<br/>-Dserver.port"]
P3 -->|no la té| P4["4. applicationConfig:<br/>application.properties"]
P4 -->|la té: 8080| R["Retorna 8080<br/>i deixa de cercar"]
La conseqüència és fonamental: el primer que respon guanya. Tota la lògica de precedència de Spring Boot es redueix a en quin ordre es col·loquen els PropertySource en aquesta llista.
Pots inspeccionar la llista completa a la teva pròpia aplicació:
package com.ciclourbana.comu;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.stereotype.Component;
/**
* Eina de diagnòstic: bolca les fonts de propietats
* en l'ordre real de precedència i resol algunes claus clau.
*/
@Component
public class InspectorConfiguracio implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(InspectorConfiguracio.class);
private final ConfigurableEnvironment entorn;
public InspectorConfiguracio(ConfigurableEnvironment entorn) {
this.entorn = entorn;
}
@Override
public void run(String... args) {
log.info("=== Fonts de propietats, de major a menor prioritat ===");
int posicio = 1;
for (var font : entorn.getPropertySources()) {
log.info(" {}. {}", posicio++, font.getName());
}
log.info("server.port resolt a: {}", entorn.getProperty("server.port"));
log.info("spring.application.name resolt a: {}",
entorn.getProperty("spring.application.name"));
}
}Sortida típica a CicloUrbana:
=== Fonts de propietats, de major a menor prioritat ===
1. configurationProperties
2. commandLineArgs
3. servletConfigInitParams
4. servletContextInitParams
5. systemProperties
6. systemEnvironment
7. random
8. Config resource 'class path resource [application.properties]'
server.port resolt a: 8080
spring.application.name resolt a: ciclourbanaAquest InspectorConfiguracio és una eina que val la pena tenir a mà: quan una propietat "no es llegeix", el primer és veure quina font la està guanyant.
- L'ordre de precedència de les fonts
Spring Boot defineix un ordre complet i documentat. De major a menor prioritat, i quedant-nos amb el que importa a la pràctica:
| # | Font | Exemple | Ús habitual |
|---|---|---|---|
| 1 | Propietats de DevTools (~/.config/spring-boot) |
— | Desenvolupament local |
| 2 | @TestPropertySource i properties de @SpringBootTest |
@SpringBootTest(properties = "server.port=0") |
Proves (mòdul 6) |
| 3 | Arguments de línia d'ordres | --server.port=9090 |
Arrencada puntual, contenidors |
| 4 | SPRING_APPLICATION_JSON |
SPRING_APPLICATION_JSON='{"server":{"port":9090}}' |
Plataformes cloud |
| 5 | Paràmetres del ServletContext/ServletConfig |
— | Desplegament en WAR |
| 6 | Atributs JNDI | — | Servidors d'aplicacions clàssics |
| 7 | Propietats de sistema Java | -Dserver.port=9090 |
Scripts d'arrencada |
| 8 | Variables d'entorn | SERVER_PORT=9090 |
Docker, Kubernetes, CI |
| 9 | application-{perfil}.properties fora del jar |
./config/application-prod.properties |
Configuració per entorn |
| 10 | application-{perfil}.properties dins del jar |
application-dev.properties |
Perfils (lliçó 07-02) |
| 11 | application.properties fora del jar |
./config/application.properties |
Ajustos de l'operador |
| 12 | application.properties dins del jar |
src/main/resources/application.properties |
Valors per defecte del projecte |
| 13 | @PropertySource en classes @Configuration |
@PropertySource("classpath:tarifes.properties") |
Fitxers addicionals |
| 14 | Valors per defecte (SpringApplication.setDefaultProperties) |
— | Últim recurs |
Tres regles que resumeixen la taula i que convé memoritzar:
- El més extern guanya. Com més a prop del moment d'arrencada s'especifica un valor, més prioritat té. Un
--server.port=9090a la línia d'ordres venç qualsevol fitxer. - Fora del jar guanya a dins del jar. És el que permet empaquetar valors per defecte raonables i que l'operador els ajusti sense reconstruir res.
- Amb perfil guanya a sense perfil.
application-prod.propertiessobreescriuapplication.properties.
Les tres files que utilitzaràs el 95 % del temps són la 3 (arguments), la 8 (variables d'entorn) i la 12 (application.properties del projecte). La resta convé conèixer-la per no endur-se sorpreses.
- Demostrar la precedència a la pràctica
Res no convenç com veure-ho funcionar. Comencem amb el fitxer del projecte:
# src/main/resources/application.properties
spring.application.name=ciclourbana
server.port=8080
ciclourbana.ciutat=RibaltaArrenca normalment:
Ara sobreescriu amb una variable d'entorn (fila 8):
Ara amb una propietat de sistema (fila 7, que guanya la variable d'entorn):
I finalment amb un argument de línia d'ordres (fila 3, el que els guanya a tots):
SERVER_PORT=8081 java -Dserver.port=8082 -jar target/ciclourbana-0.0.1-SNAPSHOT.jar --server.port=8083Fixa't en la diferència sintàctica, que confon molta gent:
| Sintaxi | Què és | Posició |
|---|---|---|
-Dclau=valor |
Propietat de sistema de la JVM | Abans de -jar |
--clau=valor |
Argument de l'aplicació | Després del jar |
CLAU=valor (davant de l'ordre) |
Variable d'entorn | Abans de tot |
Posar --server.port=9090 abans de -jar no funciona: la JVM ho interpretaria com una opció seva i fallaria. I -Dserver.port després del jar arriba com un argument més de l'aplicació, que Spring no reconeix com a propietat.
Amb Maven, per passar arguments cal utilitzar la propietat del plugin:
.properties davant de .yaml
.properties davant de .yamlSpring Boot admet tots dos formats, amb les mateixes capacitats. L'elecció és d'estil... fins que l'estructura creix.
La mateixa configuració de CicloUrbana en els dos formats:
# src/main/resources/application.properties
spring.application.name=ciclourbana
server.port=8080
server.servlet.context-path=/
ciclourbana.ciutat=Ribalta
ciclourbana.tarifa.desbloqueig=0.50
ciclourbana.tarifa.preu-minut=0.12
ciclourbana.xarxa.capacitat-minima=8
ciclourbana.xarxa.llindar-bateria=20
ciclourbana.estacions-destacades[0]=Plaça Major
ciclourbana.estacions-destacades[1]=Universitat
ciclourbana.tarifes-per-usuari.estandard=0.12
ciclourbana.tarifes-per-usuari.estudiant=0.08
ciclourbana.tarifes-per-usuari.jubilat=0.05
logging.level.com.ciclourbana=DEBUG
logging.level.org.springframework.web=INFO# src/main/resources/application.yaml
spring:
application:
name: ciclourbana
server:
port: 8080
servlet:
context-path: /
ciclourbana:
ciutat: Ribalta
tarifa:
desbloqueig: 0.50
preu-minut: 0.12
xarxa:
capacitat-minima: 8
llindar-bateria: 20
estacions-destacades:
- Plaça Major
- Universitat
tarifes-per-usuari:
estandard: 0.12
estudiant: 0.08
jubilat: 0.05
logging:
level:
com.ciclourbana: DEBUG
org.springframework.web: INFOComparats:
| Criteri | .properties |
.yaml |
|---|---|---|
| Jerarquia | Repetitiva: cada línia completa | Imbricada, sense repetició |
| Llistes | clau[0], clau[1]... |
- element (natural) |
| Mapes | clau.subclau=valor |
Imbricat (natural) |
| Sensible a la indentació | No | Sí, i és el seu gran problema |
| Tabuladors | Irrellevants | Prohibits: trenquen el fitxer |
| Comentaris | # |
# |
Cerca d'una clau amb grep |
Trivial: la clau completa és a la línia | Difícil: la clau està repartida |
| Diversos documents en un fitxer | No (s'utilitza #---) |
Sí, amb --- |
| Valors multilínia | Amb \ al final |
Sí, amb | i > |
| Precedència si existeixen tots dos | Guanya .properties |
— |
Recomanació pràctica: tria'n un i sigues coherent. Si la teva configuració és plana i curta, .properties és més difícil de trencar. Tan bon punt apareixen llistes, mapes i tres nivells d'imbricació —el cas de CicloUrbana així que arribem a la lliçó 02-05—, YAML és clarament més llegible. En aquest curs utilitzarem YAML a partir d'aquí.
No tinguis mai els dos fitxers alhora: application.properties guanya, i passaràs una tarda sencera preguntant-te per què el teu YAML no es llegeix.
Els errors d'indentació de YAML
Són el peatge del format, i tots són silenciosos: el fitxer es llegeix, però les propietats queden en un altre lloc.
# MALAMENT: 'port' penja de l'arrel, no pas de 'server'
server:
port: 8080
# MALAMENT: tabulador en lloc d'espais (aquí no es veu, però trenca l'arrencada)
server:
port: 8080
# MALAMENT: 'context-path' amb menys indentació de la que necessita
server:
servlet:
context-path: /api # 3 espais on el bloc n'utilitza 2 o 4: inconsistent
# BÉ
server:
port: 8080
servlet:
context-path: /El primer cas produeix un error d'arrencada clar (server no admet un valor escalar), però variants més subtils simplement deixen la propietat òrfena i l'aplicació arrenca amb el valor per defecte. Regla: dos espais per nivell, mai tabuladors, i activa al teu IDE la visualització de caràcters invisibles.
- Relaxació de noms (relaxed binding)
Spring Boot no exigeix que el nom de la propietat s'escrigui exactament igual arreu. Aplica un algorisme de relaxació que considera equivalents diverses formes:
| Forma | Exemple | On s'utilitza |
|---|---|---|
| kebab-case | ciclourbana.tarifa-base |
Recomanada als fitxers |
| camelCase | ciclourbana.tarifaBase |
Admesa als fitxers |
| snake_case | ciclourbana.tarifa_base |
Admesa |
| MAJÚSCULES amb guió baix | CICLOURBANA_TARIFABASE |
Variables d'entorn |
Totes quatre es resolen a la mateixa propietat. Això és el que permet que en un docker-compose.yml o en un Deployment de Kubernetes escriguis:
environment:
CICLOURBANA_TARIFA_DESBLOQUEIG: "0.60"
CICLOURBANA_XARXA_CAPACITATMINIMA: "10"
SERVER_PORT: "8080"i que aquests valors arribin a ciclourbana.tarifa.desbloqueig, ciclourbana.xarxa.capacitatMinima i server.port.
Les regles per traduir una propietat a variable d'entorn són tres:
- Els punts (
.) es converteixen en guions baixos (_). - Els guions (
-) s'eliminen. - Tot en majúscules.
Així, ciclourbana.xarxa.capacitat-minima → CICLOURBANA_XARXA_CAPACITATMINIMA. Aquest segon punt és el que més maldecaps dona: molta gent escriu CICLOURBANA_XARXA_CAPACITAT_MINIMA i no funciona, perquè aquest nom correspondria a ciclourbana.xarxa.capacitat.minima, amb un punt de més.
Dues limitacions importants de la relaxació:
- Només s'aplica a l'enllaçat de
@ConfigurationProperties(lliçó 02-05) i a les propietats del mateix Spring Boot. Amb@Valuela coincidència és exacta:@Value("${ciclourbana.tarifa-base}")no troba una propietat escritaciclourbana.tarifaBase. - Les claus d'un
Mapno es relaxen: si defineixesciclourbana.tarifes-per-usuari.estudiant-becat, la clau del mapa serà literalmentestudiant-becat.
Convenció recomanada: escriu sempre en kebab-case als fitxers. És la forma canònica, la que apareix a la documentació de Spring Boot i la que evita ambigüitats.
- Llegir propietats amb
@Value
@Value@Value injecta el valor d'una propietat en un camp o paràmetre. Traurem per fi de les constants els preus de TarifaEstandard:
package com.ciclourbana.lloguers;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
@Component
@Primary
public class TarifaEstandard implements CalculadoraTarifa {
private final BigDecimal desbloqueig;
private final BigDecimal perMinut;
// @Value en paràmetres del constructor: manté els camps final
public TarifaEstandard(
@Value("${ciclourbana.tarifa.desbloqueig}") BigDecimal desbloqueig,
@Value("${ciclourbana.tarifa.preu-minut}") BigDecimal perMinut) {
this.desbloqueig = desbloqueig;
this.perMinut = perMinut;
}
@Override
public BigDecimal calcular(Duration durada) {
BigDecimal minuts = BigDecimal.valueOf(Math.max(1, durada.toMinutes()));
return desbloqueig
.add(perMinut.multiply(minuts))
.setScale(2, RoundingMode.HALF_UP);
}
@Override
public String nom() {
return "estandard";
}
}Observa que @Value va als paràmetres del constructor, no pas als camps. Així els camps continuen sent final i la classe continua sent construïble en un test amb new TarifaEstandard(new BigDecimal("0.50"), new BigDecimal("0.12")), sense Spring pel mig. És la mateixa lògica de la lliçó 02-02 aplicada a la configuració.
Spring converteix automàticament el text al tipus del paràmetre:
@Value("${server.port}") int port; // 8080
@Value("${ciclourbana.ciutat}") String ciutat; // "Ribalta"
@Value("${ciclourbana.tarifa.desbloqueig}") BigDecimal desbloqueig; // 0.50
@Value("${ciclourbana.xarxa.manteniment}") boolean enManteniment; // true/false
@Value("${ciclourbana.estacions-destacades}") List<String> destacades; // separades per comes
@Value("${ciclourbana.sessio.durada}") Duration durada; // "30m" -> PT30MPerquè List<String> funcioni amb @Value, el valor ha de ser una cadena separada per comes, no pas una llista YAML:
ciclourbana:
estacions-destacades: # NO serveix per a @Value
- Plaça Major # sí que serveix per a @ConfigurationProperties
- UniversitatAquesta és ja la primera esquerda de @Value, i no serà la darrera.
- SpEL i valors per defecte
Valors per defecte
Si una propietat no existeix, @Value falla a l'arrencada:
Could not resolve placeholder 'ciclourbana.tarifa.desbloqueig' in value
"${ciclourbana.tarifa.desbloqueig}"S'evita amb la sintaxi ${clau:valorPerDefecte}:
@Value("${ciclourbana.tarifa.desbloqueig:0.50}") BigDecimal desbloqueig; // 0.50 si falta
@Value("${ciclourbana.xarxa.llindar-bateria:20}") int llindarBateria;
@Value("${ciclourbana.missatge-manteniment:}") String missatge; // cadena buida
@Value("${ciclourbana.contacte:#{null}}") String contacte; // null explícitCompte amb un cas especial: si el valor per defecte conté : (una URL, per exemple), cal tenir present que només compta el primer : com a separador, així que @Value("${url:http://localhost:8080}") funciona correctament i el valor per defecte és la URL completa.
SpEL: el llenguatge d'expressions de Spring
@Value admet també expressions SpEL amb la sintaxi #{...} (coixinet, no pas dòlar):
// Aritmètica sobre una propietat
@Value("#{${ciclourbana.tarifa.preu-minut} * 60}")
BigDecimal preuHora;
// Cridar un mètode d'un altre bean
@Value("#{selectorTarifa.disponibles().size()}")
int nombreDeTarifes;
// Llegir de l'Environment amb lògica
@Value("#{environment['ciclourbana.ciutat'] ?: 'desconeguda'}")
String ciutat;
// Convertir una cadena separada per comes en llista (útil de debò)
@Value("#{'${ciclourbana.estacions-destacades}'.split(',')}")
List<String> destacades;
// Propietats del sistema
@Value("#{systemProperties['user.timezone']}")
String zonaHoraria;I una que sí que és realment pràctica: valors aleatoris, per a ports o identificadors en proves.
@Value("${random.int(1000,9999)}") int codiSessio;
@Value("${random.uuid}") String identificadorArrencada;| Sintaxi | Nom | Què fa |
|---|---|---|
${...} |
Marcador de propietat | Substitueix pel valor de la propietat |
#{...} |
Expressió SpEL | Avalua una expressió (pot contenir ${...} a dins) |
Les limitacions de @Value
@Value és còmode per a un o dos valors solts, però es queda curt tan bon punt la configuració creix. Els seus problemes:
| Limitació | Conseqüència |
|---|---|
| Sense relaxació de noms | La clau s'ha d'escriure exactament igual. |
| Sense validació | Un valor absurd (-5 de capacitat) s'accepta sense protestar. |
| Sense estructures | No enllaça llistes YAML ni mapes de manera natural. |
| Sense metadades | L'IDE no autocompleta ni documenta les propietats. |
| Errors dispersos | Si falten cinc propietats, l'arrencada falla per la primera, una a una. |
| Configuració escampada | Les claus apareixen en vint classes: ningú no sap què configura l'aplicació. |
| Difícil d'agrupar i reutilitzar | No hi ha un objecte que representi "la configuració de tarifes". |
Totes elles les resol @ConfigurationProperties, que és el tema de la lliçó següent. La regla que aplicarem a CicloUrbana: @Value per a valors aïllats i ocasionals; @ConfigurationProperties per a tota la resta.
- Les propietats que utilitza CicloUrbana
Aquesta és la configuració base del projecte, amb explicació de cada bloc:
# src/main/resources/application.yaml
spring:
application:
name: ciclourbana # apareix als logs, a Actuator i a les traces (mòdul 9)
server:
port: 8080 # 0 = port aleatori lliure (molt útil en proves)
servlet:
context-path: / # prefix de TOTES les rutes
shutdown: graceful # aturada ordenada, vista a la lliçó 01-05
logging:
level:
root: INFO
com.ciclourbana: DEBUG # el nostre codi, amb detall
org.springframework.web: INFO
org.springframework.beans.factory: INFO # a DEBUG per depurar el cicle de vida
pattern:
console: "%d{HH:mm:ss.SSS} %-5level [%logger{20}] - %msg%n"
ciclourbana:
ciutat: Ribalta
tarifa:
desbloqueig: 0.50
preu-minut: 0.12
xarxa:
capacitat-minima: 8
llindar-bateria: 20Les propietats del servidor i de l'aplicació més útils:
| Propietat | Valor típic | Per a què serveix |
|---|---|---|
server.port |
8080, 0 |
Port HTTP. 0 n'assigna un de lliure. |
server.servlet.context-path |
/, /ciclourbana |
Prefix de totes les rutes. |
server.shutdown |
graceful |
Espera que acabin les peticions en curs. |
server.error.include-message |
always |
Inclou el missatge d'error a la resposta (mòdul 3). |
server.compression.enabled |
true |
Comprimeix les respostes grans. |
spring.application.name |
ciclourbana |
Identifica l'app als logs, mètriques i traces. |
spring.main.banner-mode |
off, console |
Controla el bàner (lliçó 01-05). |
spring.main.web-application-type |
servlet, none |
Força el tipus d'aplicació. |
logging.level.<paquet> |
DEBUG |
Nivell de log per paquet. S'estudia a fons a 09-05. |
logging.file.name |
logs/ciclourbana.log |
Escriu el log també a fitxer. |
Un avís sobre server.servlet.context-path: si el poses a /ciclourbana, la URL del nostre endpoint passa a ser http://localhost:8080/ciclourbana/api/v1/estacions. És un canvi que trenca tots els clients i tots els fitxers .http de la lliçó 01-02, així que a CicloUrbana el deixarem a /.
Verifica que la configuració s'aplica:
- Configuració externa:
spring.config.import i spring.config.location
spring.config.import i spring.config.locationTot l'anterior viu dins del jar. En un desplegament real cal configuració que no s'empaqueti.
Fitxers externs per convenció
Spring Boot busca application.yaml automàticament, i per aquest ordre de prioritat, a:
./config/(subdirectoriconfigal costat del jar)./(directori actual)classpath:/config/classpath:/(dins del jar)
És a dir, n'hi ha prou amb deixar un fitxer al costat del jar per sobreescriure els valors empaquetats:
target/
├── ciclourbana-0.0.1-SNAPSHOT.jar
└── config/
└── application.yaml # sobreescriu el que porti el jar# target/config/application.yaml — configuració de l'operador
server:
port: 9090
ciclourbana:
tarifa:
desbloqueig: 0.60 # l'ajuntament ha apujat el desbloqueigspring.config.import
Permet incloure altres fitxers des de la configuració principal. És la forma moderna i preferida davant de @PropertySource:
# src/main/resources/application.yaml
spring:
config:
import:
- optional:file:./config/tarifes-ribalta.yaml # opcional: no falla si no existeix
- optional:file:/etc/ciclourbana/secrets.yaml # secrets del servidorEl prefix optional: és clau: sense ell, si el fitxer no existeix l'arrencada falla. Això té la seva lògica —vols saber si falta un fitxer imprescindible— però en desenvolupament és incòmode, i per això els fitxers propis d'un entorn concret es marquen gairebé sempre com a opcionals.
spring.config.import admet també altres orígens:
spring:
config:
import:
- optional:file:./config/ # un directori sencer
- optional:configtree:/run/secrets/ # secrets de Docker/Kubernetes
- optional:classpath:tarifes-per-defecte.yaml # un altre fitxer del jarEl format configtree: mereix una nota: a Kubernetes i a Docker Swarm els secrets es munten com a fitxers, un per clau, dins d'un directori. Amb configtree: Spring Boot llegeix aquest arbre i converteix cada fitxer en una propietat. És el mecanisme idiomàtic per a secrets en contenidors, i el reprendrem a la lliçó 07-04.
spring.config.location
Substitueix del tot les ubicacions per defecte, en lloc d'afegir-s'hi:
I spring.config.additional-location afegeix ubicacions conservant les predeterminades, que sol ser el que realment vols:
| Opció | Efecte |
|---|---|
spring.config.location |
Reemplaça les ubicacions per defecte. Control total, risc de perdre valors. |
spring.config.additional-location |
Afegeix ubicacions amb més prioritat que les predeterminades. Més segur. |
spring.config.import |
Inclou fitxers concrets des de la mateixa configuració. Declaratiu. |
- Secrets i credencials
Aquest apartat no és opcional. És la part de la lliçó amb conseqüències més greus si s'ignora.
Mai, sota cap circumstància, no escriguis contrasenyes, claus d'API, tokens o certificats en un fitxer que vagi al repositori de codi.
El que no has de fer, encara que ho vegis en tutorials:
# MALAMENT: això acabarà a Git i a l'historial per sempre
spring:
datasource:
url: jdbc:postgresql://bd.ribalta.example:5432/ciclourbana
username: ciclourbana_app
password: Sup3rS3cr3t0! # ← catàstrofe
ciclourbana:
passarela-pagament:
api-key: sk_live_9f3a2b1c8d7e # ← catàstrofePer què és tan greu, més enllà del que és evident:
- Git no oblida. Esborrar la línia en un commit posterior no elimina el secret: continua a l'historial, accessible amb
git log -p. Rotar la credencial és obligatori, no pas opcional. - El repositori es copia. Forks, clons en portàtils, còpies de seguretat, integracions de CI, l'ordinador d'un becari. Un secret a Git és, a la pràctica, en molts més llocs dels que et penses.
- Els repositoris canvien de visibilitat. Un repositori privat que es fa públic filtra tot el seu historial de cop. És un accident sorprenentment freqüent.
- Els robots rastregen. Existeixen bots que escanegen GitHub buscant patrons de claus; una clau d'un proveïdor cloud publicada per error s'explota en minuts.
Què fer en lloc seu:
1. Variables d'entorn amb marcador i sense valor per defecte. El fitxer versionat declara què necessita, no pas quant val:
# src/main/resources/application.yaml — sí que va al repositori
spring:
datasource:
url: ${CICLOURBANA_BD_URL:jdbc:h2:mem:ribalta}
username: ${CICLOURBANA_BD_USUARI:sa}
password: ${CICLOURBANA_BD_PASSWORD:} # buit en local, obligatori fora
ciclourbana:
passarela-pagament:
api-key: ${CICLOURBANA_PASSARELA_API_KEY} # sense defecte: falla si no hi ésFixa't en el detall: la clau de la passarel·la no té valor per defecte. Si algú desplega sense definir-la, l'arrencada falla immediatament amb un missatge clar. Això és molt millor que arrencar i fallar al primer pagament.
export CICLOURBANA_BD_PASSWORD='la-de-veritat'
export CICLOURBANA_PASSARELA_API_KEY='sk_live_...'
java -jar ciclourbana.jar2. Fitxer extern fora de l'arbre del projecte, amb permisos restringits:
sudo mkdir -p /etc/ciclourbana
sudo tee /etc/ciclourbana/secrets.yaml > /dev/null <<'EOF'
spring:
datasource:
password: la-de-veritat
EOF
sudo chmod 600 /etc/ciclourbana/secrets.yaml
sudo chown ciclourbana:ciclourbana /etc/ciclourbana/secrets.yaml3. Un gestor de secrets, que és el que és correcte en producció seriosa: HashiCorp Vault, AWS Secrets Manager, Azure Key Vault o els Secret de Kubernetes muntats com a configtree:. Es tracten al mòdul 8.
4. Protegeix el repositori. Afegeix al .gitignore qualsevol fitxer local de secrets i considera un hook de pre-commit que detecti patrons sospitosos:
5. Si un secret es filtra, rota'l. No l'esborris del fitxer i continuïs. Invalida la credencial i emet-ne una de nova. És l'única acció que realment tanca el forat.
Una darrera nota: a la lliçó 02-05 veurem com evitar a més que un secret acabi accidentalment als logs, i al mòdul 7, com Actuator amaga els valors sensibles a l'endpoint /actuator/env.
Errors Comuns i Consells
Tenir alhora application.properties i application.yaml. Guanya el .properties i el YAML sembla ignorat. Esborra'n un.
Escriure malament el nom de la variable d'entorn. CICLOURBANA_XARXA_CAPACITAT_MINIMA no és ciclourbana.xarxa.capacitat-minima: els guions s'eliminen, no es converteixen en guió baix. El nom correcte és CICLOURBANA_XARXA_CAPACITATMINIMA.
Utilitzar tabuladors a YAML. El fitxer es rebutja amb un error d'anàlisi que no sempre assenyala la línia correcta. Configura el teu editor perquè insereixi espais.
Posar --server.port abans del -jar. La JVM no ho entén. Els -- van després del jar; els -D van abans.
Esperar relaxació de noms amb @Value. No n'hi ha. Amb @Value la clau ha de coincidir caràcter a caràcter.
Confondre ${...} amb #{...}. El primer resol propietats; el segon avalua SpEL. @Value("#{ciclourbana.ciutat}") intenta avaluar ciclourbana.ciutat com a expressió i falla; el correcte és ${ciclourbana.ciutat}.
Posar una contrasenya a application.yaml. Repetit expressament: és l'error més car d'aquesta lliçó.
Consell: anomena les teves propietats amb un prefix propi. Tot el de CicloUrbana comença per ciclourbana.. Així no col·lisiones mai amb una propietat de Spring Boot o d'una llibreria, i grep -r "ciclourbana\." src/ et diu d'un cop d'ull què configura l'aplicació.
Consell: utilitza server.port=0 a les proves. Assigna un port lliure i evita fallades quan dues proves s'executen alhora. Hi tornarem al mòdul 6.
Consell: documenta cada propietat amb un comentari. Qui desplegui la teva aplicació d'aquí a un any t'ho agrairà, i no necessitarà llegir el codi per saber si llindar-bateria va en percentatge o en volts.
Consell: en desenvolupament, no depenguis de la línia d'ordres. Un application-dev.yaml amb perfil (lliçó 07-02) és més reproduïble que una ordre llarga que només és a la teva memòria.
Exercicis
Exercici 1: demostrar la precedència
Defineix ciclourbana.ciutat=Ribalta a application.yaml. Escriu un component AvisCiutat que registri el seu valor en arrencar. Després arrenca l'aplicació quatre vegades —normal, amb variable d'entorn, amb propietat de sistema i amb argument de línia d'ordres— donant un valor diferent a cadascuna, i anota què guanya. Afegeix al component el bolcat de la font que ha aportat el valor.
Exercici 2: externalitzar la tarifa d'estudiant
TarifaEstudiant encara té les seves constants escrites a foc. Externalitza-les a ciclourbana.tarifa.estudiant.preu-minut i ciclourbana.tarifa.estudiant.minuts-gratis, amb valors per defecte al mateix @Value perquè l'aplicació arrenqui encara que faltin. Verifica que l'import d'un lloguer de 45 minuts canvia en sobreescriure les propietats per línia d'ordres.
Exercici 3: configuració d'operador amb fitxer extern
Prepara un desplegament realista: empaqueta el jar, crea un directori config/ al seu costat amb un application.yaml que canviï el port a 9090, apugi el desbloqueig a 0,60 € i llegeixi la contrasenya de la base de dades d'una variable d'entorn sense valor per defecte. Comprova que l'aplicació falla en arrencar si la variable no està definida, i que arrenca correctament quan sí que ho està.
Solucions
Solució 1
package com.ciclourbana.comu;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.stereotype.Component;
@Component
public class AvisCiutat implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(AvisCiutat.class);
private static final String CLAU = "ciclourbana.ciutat";
private final String ciutat;
private final ConfigurableEnvironment entorn;
public AvisCiutat(@Value("${ciclourbana.ciutat}") String ciutat,
ConfigurableEnvironment entorn) {
this.ciutat = ciutat;
this.entorn = entorn;
}
@Override
public void run(String... args) {
log.info("Ciutat de la xarxa: {}", ciutat);
log.info("Aportada per la font: {}", fontQueGuanya());
}
/** Recorre les fonts en ordre i retorna la primera que té la clau. */
private String fontQueGuanya() {
for (var font : entorn.getPropertySources()) {
if (font instanceof EnumerablePropertySource<?> enumerable
&& enumerable.containsProperty(CLAU)) {
return font.getName() + " -> " + enumerable.getProperty(CLAU);
}
}
return "cap (valor per defecte)";
}
}Les quatre execucions:
# 1. Només el fitxer
java -jar target/ciclourbana-0.0.1-SNAPSHOT.jar
# Ciutat de la xarxa: Ribalta
# Aportada per la font: Config resource 'class path resource [application.yaml]' -> Ribalta
# 2. Variable d'entorn
CICLOURBANA_CIUTAT=Ribalta-Nord java -jar target/ciclourbana-0.0.1-SNAPSHOT.jar
# Ciutat de la xarxa: Ribalta-Nord
# Aportada per la font: systemEnvironment -> Ribalta-Nord
# 3. Propietat de sistema (guanya la variable d'entorn)
CICLOURBANA_CIUTAT=Ribalta-Nord \
java -Dciclourbana.ciutat=Ribalta-Sud -jar target/ciclourbana-0.0.1-SNAPSHOT.jar
# Ciutat de la xarxa: Ribalta-Sud
# Aportada per la font: systemProperties -> Ribalta-Sud
# 4. Argument de línia d'ordres (els guanya a tots)
CICLOURBANA_CIUTAT=Ribalta-Nord \
java -Dciclourbana.ciutat=Ribalta-Sud -jar target/ciclourbana-0.0.1-SNAPSHOT.jar \
--ciclourbana.ciutat=Ribalta-Centre
# Ciutat de la xarxa: Ribalta-Centre
# Aportada per la font: commandLineArgs -> Ribalta-CentreComentari: el mètode fontQueGuanya() implementa literalment l'algorisme de l'Environment descrit a l'apartat 1 —recórrer la llista ordenada i quedar-se amb la primera coincidència— i per això el seu resultat coincideix sempre amb el valor injectat. És un bon diagnòstic per tenir a mà.
Nota sobre la font configurationProperties que apareix la primera al llistat: és una font sintètica que Spring Boot utilitza internament per a l'enllaçat; no aporta valors propis.
Solució 2
# src/main/resources/application.yaml
ciclourbana:
tarifa:
desbloqueig: 0.50
preu-minut: 0.12
estudiant:
preu-minut: 0.08
minuts-gratis: 15package com.ciclourbana.lloguers;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
@Component("tarifaEstudiant")
public class TarifaEstudiant implements CalculadoraTarifa {
private final BigDecimal perMinut;
private final long minutsGratis;
public TarifaEstudiant(
// Valor per defecte després dels dos punts: l'app arrenca encara que faltin
@Value("${ciclourbana.tarifa.estudiant.preu-minut:0.08}") BigDecimal perMinut,
@Value("${ciclourbana.tarifa.estudiant.minuts-gratis:15}") long minutsGratis) {
this.perMinut = perMinut;
this.minutsGratis = minutsGratis;
}
@Override
public BigDecimal calcular(Duration durada) {
long facturables = Math.max(0, durada.toMinutes() - minutsGratis);
return perMinut
.multiply(BigDecimal.valueOf(facturables))
.setScale(2, RoundingMode.HALF_UP);
}
@Override
public String nom() {
return "estudiant";
}
}Verificació amb el DemostracioTarifes de la lliçó 02-02:
# Amb els valors del fitxer: (45-15) * 0,08 = 2,40 €
java -jar target/ciclourbana-0.0.1-SNAPSHOT.jar
# Lloguer de 45 min amb tarifa 'estudiant': 2.40 €
# Conveni ampliat: 30 minuts gratis i 0,06 €/min -> (45-30) * 0,06 = 0,90 €
java -jar target/ciclourbana-0.0.1-SNAPSHOT.jar \
--ciclourbana.tarifa.estudiant.minuts-gratis=30 \
--ciclourbana.tarifa.estudiant.preu-minut=0.06
# Lloguer de 45 min amb tarifa 'estudiant': 0.90 €Comentari i error freqüent: si escrius @Value("${ciclourbana.tarifa.estudiant.minutsGratis:15}") —en camelCase— no enllaçarà amb la propietat minuts-gratis del fitxer: prendrà sempre el valor per defecte 15, en silenci i sense cap error. És exactament l'absència de relaxació de noms de l'apartat 5, i és una de les raons de pes per migrar a @ConfigurationProperties a la lliçó vinent.
Consell: fixa't que els preus es declaren com a BigDecimal, no pas com a double. En diners, double produeix errors d'arrodoniment inacceptables (0.1 + 0.2 no és 0.3). És una regla que no s'ha de trencar mai en un domini amb imports.
Solució 3
# src/main/resources/application.yaml — versionat, sense secrets
spring:
application:
name: ciclourbana
datasource:
url: ${CICLOURBANA_BD_URL:jdbc:h2:mem:ribalta}
username: ${CICLOURBANA_BD_USUARI:sa}
# Sense valor per defecte: si falta la variable, l'arrencada falla
password: ${CICLOURBANA_BD_PASSWORD}
server:
port: 8080
ciclourbana:
ciutat: Ribalta
tarifa:
desbloqueig: 0.50
preu-minut: 0.12# Empaquetar
./mvnw clean package -DskipTests
# Preparar la configuració de l'operador al costat del jar
mkdir -p target/config
cat > target/config/application.yaml <<'EOF'
# Configuració de producció de la xarxa de Ribalta.
# Aquest fitxer NO és al repositori: el gestiona l'operador.
server:
port: 9090
ciclourbana:
tarifa:
desbloqueig: 0.60 # tarifa de temporada alta aprovada per l'ajuntament
EOFPrimer intent, sense la variable d'entorn:
***************************
APPLICATION FAILED TO START
***************************
Description:
Could not resolve placeholder 'CICLOURBANA_BD_PASSWORD' in value
"${CICLOURBANA_BD_PASSWORD}"Segon intent, amb ella definida:
Comentari: la fallada del primer intent és desitjable. Un marcador sense valor per defecte converteix un oblit de desplegament en un error immediat i evident, en lloc d'una fallada silenciosa a les tres de la matinada quan algú intenti pagar. És la mateixa filosofia de "fallar aviat i en veu alta" que vam veure amb la creació anticipada de singletons a la lliçó 02-03.
Consell de desplegament: en un servidor real, la contrasenya no s'escriu a la línia d'ordres —quedaria visible a ps aux i a l'historial de la shell— sinó al fitxer d'unitat de systemd, a l'EnvironmentFile corresponent amb permisos 600, o al gestor de secrets de la plataforma. A Docker es passa per variable d'entorn del contenidor o, millor, per secret muntat com a fitxer i llegit amb configtree: (lliçó 07-04).
Conclusió
La configuració de CicloUrbana ha sortit del codi. Saps que l'Environment no desa valors sinó una llista ordenada de PropertySource, i que tota la lògica de precedència es redueix a en quin ordre és aquesta llista: el més extern guanya, fora del jar guanya a dins, i amb perfil guanya a sense perfil. Ho has comprovat tu mateix arrencant la mateixa aplicació amb fitxer, variable d'entorn, propietat de sistema i argument de línia d'ordres, i saps distingir la sintaxi de cadascun. Coneixes les diferències reals entre .properties i YAML —i per què a partir d'aquí el curs utilitza YAML— juntament amb el parany de la indentació. Entens la relaxació de noms i les tres regles que tradueixen ciclourbana.xarxa.capacitat-minima a CICLOURBANA_XARXA_CAPACITATMINIMA, inclosa l'eliminació dels guions que trenca tants desplegaments. Saps llegir propietats amb @Value, donar-los valors per defecte, utilitzar SpEL quan aporta alguna cosa... i també coneixes les set limitacions que fan de @Value una eina d'ús puntual. Saps importar configuració externa amb spring.config.import i spring.config.additional-location. I, sobretot, saps que una credencial no va mai al repositori, per què el dany és permanent i quines són les alternatives reals.
Els preus de la xarxa de Ribalta ja es poden canviar sense recompilar. Però la solució té esquerdes evidents: les claus estan escampades per diverses classes, un camelCase mal escrit falla en silenci, ningú no valida que el preu per minut sigui positiu i l'IDE no ajuda a escriure les propietats. La lliçó següent, Propietats de Spring Boot, resol les quatre coses amb @ConfigurationProperties: configuració tipada, agrupada en objectes immutables, validada amb Bean Validation, amb conversió automàtica de Duration i DataSize, i amb autocompletat a l'IDE. Convertirem tota la configuració de tarifes i de la xarxa de Ribalta a aquesta forma, que és la que el projecte mantindrà fins al final del curs.
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
