La lliçó anterior va acabar amb un problema al descobert: tota la configuració d'Actuator —què s'exposa, quants detalls es mostren, si shutdown està habilitat— ha de ser diferent al portàtil d'un desenvolupador i al servidor de l'ajuntament de Ribalta. I no només Actuator: la base de dades (H2 davant de PostgreSQL), el nivell de log, Swagger, ddl-auto, CORS, la caducitat del JWT i fins i tot quines dades de prova es carreguen en arrencar. Fins ara els hem anat canviant a mà en un únic application.yml, que és exactament com una configuració de desenvolupament acaba desplegada en producció.
Aquesta lliçó ho resol amb els perfils, el mecanisme que permet que el mateix JAR —byte a byte, el que va aprovar ./mvnw verify— es comporti de manera diferent segons on s'executi. Veurem com s'activen, com s'organitzen els fitxers per entorn i què guanya quan dos defineixen la mateixa propietat, com condicionar beans amb @Profile, quan un perfil és l'eina equivocada, com es comporten a les proves i com s'injecta la configuració des de fora de l'artefacte en un contenidor.
Contingut
- Construir una vegada, desplegar en molts llocs
- Què és un perfil i com s'activa
- Fitxers per perfil: què guanya quan hi ha conflicte
- El mapa de configuració de CicloUrbana
- Documents multiperfil en un sol YAML
- Grups de perfils
@Profileen beans i classes de configuració- Quan
@Profileés una olor de disseny - Perfils i proves
- Comprovar quin perfil està actiu
- Configuració externa: fitxers,
spring.config.importi variables d'entorn - Secrets per entorn
- Errors clàssics i com detectar-los aviat
- Errors Comuns i Consells
- Exercicis
- Construir una vegada, desplegar en molts llocs
El principi s'enuncia en una frase: l'artefacte que es prova és l'artefacte que es desplega. Si per passar de preproducció a producció cal recompilar canviant un valor, el que arriba a Ribalta no és el que es va validar, sinó una cosa semblant. És un principi explícit de la metodologia 12-Factor App (factor III, «configuració») i la raó de ser dels perfils.
| Enfocament | Com canvia d'entorn | Problema |
|---|---|---|
| Recompilar per entorn | mvn -Pprod package amb filtratge de recursos |
El binari de producció no s'ha provat mai; el de proves no es desplega mai |
| Editar el YAML abans de desplegar | Algú canvia la URL a mà | Error humà garantit; sense traçabilitat de qui va canviar què |
| Un artefacte + configuració externa | El mateix JAR, configuració diferent en arrencar | Cap dels anteriors; és el que fem aquí |
Un corol·lari incòmode però important: els perfils de Maven i els perfils d'Spring són coses diferents i no s'han de confondre. Un perfil de Maven decideix què es compila i s'empaqueta —decisió de construcció—; un perfil d'Spring decideix com es comporta allò ja empaquetat —decisió d'execució—. Fer servir perfils de Maven per separar entorns trenca el principi d'aquesta lliçó.
- Què és un perfil i com s'activa
Un perfil és simplement una etiqueta amb nom que pot estar activa o no. Spring llegeix aquesta llista en arrencar i la fa servir per a dues coses: triar quins fitxers de configuració carrega i decidir quins beans registra.
| Mecanisme | Exemple | Ús típic |
|---|---|---|
Propietat a application.yml |
spring.profiles.active: dev |
Valor per defecte per a desenvolupament |
| Variable d'entorn | SPRING_PROFILES_ACTIVE=prod |
La forma estàndard en contenidors (07-04) |
| Argument de línia d'ordres | java -jar app.jar --spring.profiles.active=pre |
Arrencades manuals i scripts |
| Propietat de sistema | -Dspring.profiles.active=prod |
Servidors d'aplicacions i arrencades heretades |
| Anotació a les proves | @ActiveProfiles("test") |
Suites de JUnit (06-04) |
| Programàtic | new SpringApplicationBuilder().profiles("dev") |
Arrencades incrustades poc freqüents |
Se'n poden activar diversos alhora, separats per comes: SPRING_PROFILES_ACTIVE=prod,metriques,ue. I la precedència és la de l'ordre de propietats de 02-04: la línia d'ordres guanya a la variable d'entorn, que guanya al YAML empaquetat. Aquesta cadena és justament el que permet que el valor dev escrit a l'application.yml del repositori sigui un valor còmode per defecte que producció sobreescriu sense tocar el fitxer.
Existeixen dues propietats addicionals menys conegudes:
spring.profiles.defaultcanvia el perfil implícit quan no se n'activa cap. Sense ella valdefault, cosa que significa que un@Profile("default")s'aplica només si ningú no ha activat res.spring.profiles.includeafegeix perfils als ja actius, sense substituir-los. És útil per a complements transversals (spring.profiles.include: auditoria) i no per construir jerarquies: per a això hi ha els grups de l'apartat 6.
- Fitxers per perfil: què guanya quan hi ha conflicte
La convenció és application-<perfil>.yml a src/main/resources. CicloUrbana en tindrà quatre:
src/main/resources/ ├── application.yml # comú a tots els entorns ├── application-dev.yml # portàtil del desenvolupador ├── application-test.yml # proves automatitzades ├── application-pre.yml # preproducció de l'ajuntament └── application-prod.yml # producció
Amb spring.profiles.active=prod, Spring carrega application.yml primer i application-prod.yml després, i el segon guanya propietat a propietat. És important entendre que no se substitueix un fitxer per un altre: es combinen.
# application.yml — allò comú
spring:
application:
name: ciclourbana
jpa:
open-in-view: false
ciclourbana:
xarxa:
ciutat: Ribalta
capacitat-minima: 8
durada-maxima-lloguer: 2h# application-prod.yml — només allò que canvia
spring:
jpa:
hibernate:
ddl-auto: validate
ciclourbana:
xarxa:
durada-maxima-lloguer: 4hEn producció, ciutat continua valent Ribalta (ve del comú), durada-maxima-lloguer val 4h (guanya el perfil) i capacitat-minima continua sent 8. Tres regles que eviten sorpreses:
- La fusió és per clau, no per document. Definir
ciclourbana.xarxaal fitxer de perfil no esborra les claus que no menciona. - Les llistes i els mapes NO es fusionen, se substitueixen sencers. Si
application.ymldeclaraestacions-destacades: [Plaça Major, Universitat]iapplication-prod.ymldeclaraestacions-destacades: [Estació Nord], en producció la llista té un element. És la causa d'errors desconcertants amb propietats de col·lecció. - Amb diversos perfils actius, guanya l'últim de la llista. Amb
active=prod,ue,application-ue.ymlsobreescriuapplication-prod.yml.
I una regla d'estil que estalvia molt manteniment: a application.yml hi va tot allò comú i als fitxers de perfil només les diferències. Duplicar el fitxer sencer per entorn garanteix que, tard o d'hora, un canvi s'apliqui a tres dels quatre.
- El mapa de configuració de CicloUrbana
Aquest és el repartiment que adoptem, i serveix de guia per a qualsevol projecte:
| Aspecte | dev |
test |
pre |
prod |
|---|---|---|---|---|
| Base de dades | H2 en memòria | H2 o Testcontainers (06-05) | PostgreSQL 16 a Docker | PostgreSQL 16 gestionat |
ddl-auto |
create-drop |
create-drop |
validate |
validate |
| Flyway | Actiu amb db/migration/dev |
Desactivat | Actiu | Actiu, sense clean |
Nivell de log de com.ciclourbana |
DEBUG |
INFO |
INFO |
INFO |
| SQL d'Hibernate | DEBUG + format_sql |
WARN |
WARN |
WARN |
| Swagger UI (03-07) | Habilitat | Deshabilitat | Habilitat, amb clau | Deshabilitat |
| Consola H2 (04-02) | Habilitada | Deshabilitada | — | — |
| CORS (05-05) | http://localhost:* |
— | Domini de pre | Només https://ciclourbana.ribalta.example |
| Expiració del JWT | 8h, còmode per depurar |
1m, per provar la caducitat |
15m |
15m |
| Actuator exposat | * |
health |
Llista explícita | Llista mínima, port 8081 |
show-details de salut |
always |
never |
when-authorized |
when-authorized |
| Dades de demostració | CarregadorDadesDemo actiu |
Dades per prova | Sense carregador | Sense carregador |
| Correu de confirmació | Servei fals que escriu al log | Simulat amb Mockito | Real, a bústia de proves | Real |
| Passarel·la de pagaments | Simulador local | WireMock (07-06) | Entorn de proves de la passarel·la | Passarel·la real |
Val la pena aturar-se en dues files. L'expiració del JWT a test és d'un minut a propòsit: és l'única manera de provar la caducitat sense esperar. I ddl-auto: validate a pre i prod és la garantia de 04-08: si una entitat es desalinea de les migracions, l'arrencada falla a preproducció i no a Ribalta.
- Documents multiperfil en un sol YAML
Un fitxer YAML pot contenir diversos documents separats per ---, cadascun condicionat a un perfil:
spring:
application:
name: ciclourbana
jpa:
open-in-view: false
---
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:ciclourbana;MODE=PostgreSQL
h2:
console:
enabled: true
logging:
level:
com.ciclourbana: DEBUG
---
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:postgresql://bd-ribalta:5432/ciclourbana
password: ${POSTGRES_PASSWORD}spring.config.activate.on-profile substitueix l'antic spring.profiles de Boot 1.x, i admet les mateixes expressions que @Profile (!prod, dev | test). La seva germana spring.config.activate.on-cloud-platform: kubernetes condiciona un document a la plataforma detectada, cosa que reprendrem a 08-04.
Els seus dos límits són els que decideixen quan fer-la servir:
spring.profiles.activeno es pot declarar dins d'un document condicionat. És lògic: seria activar un perfil des d'un bloc que només es llegeix si aquest perfil ja és actiu. Spring llançaInvalidConfigDataPropertyExceptionen arrencar.- El fitxer creix molt de pressa. Amb quatre entorns i trenta propietats cadascun, un sol YAML de tres-centes línies és més difícil de revisar que quatre de setanta, i en una revisió de codi ningú no veu d'un cop d'ull què canvia entre
preiprod.
La recomanació per a CicloUrbana: fitxers separats per entorn, amb application.yml per a allò comú. El format multidocument es reserva per a dues o tres diferències petites o per a configuracions que han de viatjar juntes per força, com un application.yml extern muntat en un contenidor.
- Grups de perfils
Quan els perfils comencen a combinar-se —prod + postgres + correu-real + metriques— activar quatre noms a mà és fràgil. Un grup els agrupa sota un àlies:
spring:
profiles:
group:
prod: postgres, correu-real, metriques, cors-estricte
dev: h2, correu-fals, dades-demoAmb SPRING_PROFILES_ACTIVE=prod, tots cinc queden actius. L'avantatge és que els beans es condicionen a la capacitat, no a l'entorn: @Profile("correu-real") descriu què fa el bean, mentre que @Profile("prod") només descriu on viu. Si demà preproducció també ha d'enviar correu real, n'hi ha prou d'afegir correu-real al grup pre, sense tocar ni una línia de Java.
@Profile en beans i classes de configuració
@Profile en beans i classes de configuració@Profile decideix si un bean —o tota una classe de configuració— arriba a registrar-se al context.
package com.ciclourbana.estacions;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Component
@Profile("dades-demo")
public class CarregadorDadesDemo implements CommandLineRunner {
private final EstacioService estacioService;
private final BicicletaService bicicletaService; // constructor omès
@Override
public void run(String... args) {
if (estacioService.comptarTotes() > 0) {
return; // idempotent: no duplica si ja hi ha dades
}
estacioService.crear("Plaça Major", 24);
estacioService.crear("Estació Nord", 30);
estacioService.crear("Parc del Riu", 18);
estacioService.crear("Universitat", 36);
bicicletaService.altaLot("RB-0142", 40);
log.info("Dades de demostració carregades per a la xarxa de Ribalta");
}
}Dos detalls de l'exemple. El perfil és dades-demo, no dev, seguint la idea de l'apartat anterior: descriu la capacitat. I el run és idempotent, perquè a dev amb H2 en memòria s'executa a cada arrencada, però si algú activa el perfil contra una base de dades persistent no ha de duplicar les quatre estacions.
L'anotació admet expressions lògiques:
| Expressió | Es registra quan... |
|---|---|
@Profile("dev") |
dev està actiu |
@Profile("!prod") |
prod no està actiu (útil per a eines de desenvolupament) |
@Profile({"dev", "test"}) |
dev o test (l'array és un OR) |
@Profile("dev | test") |
El mateix, amb la sintaxi d'expressió |
@Profile("prod & !manteniment") |
prod actiu i manteniment no |
Dues aplicacions més a CicloUrbana. Un servei de correu amb dues implementacions darrere de la mateixa interfície:
public interface ServeiCorreu {
void enviarConfirmacio(Long lloguerId, String destinatari);
}
@Service
@Profile("correu-fals")
public class ServeiCorreuRegistrat implements ServeiCorreu {
@Override
public void enviarConfirmacio(Long lloguerId, String destinatari) {
log.info("[CORREU SIMULAT] Confirmació del lloguer {} a {}", lloguerId, destinatari);
}
}
@Service
@Profile("correu-real")
public class ServeiCorreuSmtp implements ServeiCorreu {
// fa servir JavaMailSender amb les credencials de l'entorn
}I una classe de configuració completa condicionada, que és la forma preferible quan el perfil afecta diversos beans alhora:
@Configuration
@Profile("cors-estricte")
public class ConfiguracioCorsProduccio {
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration cors = new CorsConfiguration();
cors.setAllowedOrigins(List.of("https://ciclourbana.ribalta.example"));
cors.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
cors.setAllowedHeaders(List.of("Authorization", "Content-Type"));
cors.setAllowCredentials(true);
cors.setMaxAge(Duration.ofHours(1));
UrlBasedCorsConfigurationSource font = new UrlBasedCorsConfigurationSource();
font.registerCorsConfiguration("/api/**", cors);
return font;
}
}El parany clàssic: que quedi registrat exactament un dels beans candidats. Si ningú no activa correu-real ni correu-fals, no hi ha cap ServeiCorreu i l'arrencada falla amb NoSuchBeanDefinitionException; si s'activen tots dos, falla amb NoUniqueBeanDefinitionException. La xarxa de seguretat és marcar-ne un com a @ConditionalOnMissingBean (02-06) o, millor, incloure'ls sempre als grups de l'apartat 6 perquè cap entorn no es pugui quedar sense implementació.
- Quan
@Profile és una olor de disseny
@Profile és una olor de disseny@Profile és còmode i per això se n'abusa. El símptoma és una classe amb @Profile("prod") el nom de la qual no diu res de l'entorn, o una condició que en realitat no és «on soc» sinó «què està activat». Aquest segon cas demana una propietat i @ConditionalOnProperty (02-06):
@Bean
@ConditionalOnProperty(prefix = "ciclourbana.lloguers", name = "caducador.actiu",
havingValue = "true", matchIfMissing = true)
CaducadorLloguers caducadorLloguers(LloguerService servei) {
return new CaducadorLloguers(servei);
}| Criteri | Fes servir @Profile |
Fes servir @ConditionalOnProperty |
|---|---|---|
| La decisió depèn de l'entorn | Sí | No |
| La decisió és una opció funcional que pot canviar dins del mateix entorn | No | Sí |
| Cal poder desactivar-ho en una instància concreta sense canviar de perfil | No | Sí |
| Substitueix una implementació per una altra | Raonable | Possible, però més verbós |
| Ha de poder canviar-se sense recompilar ni tornar a desplegar | No (el perfil es fixa en arrencar) | Sí (variable d'entorn, configuració externa) |
| Nombre de combinacions esperades | Poques i estables | Moltes i independents entre si |
El cas de l'apartat 7 ho il·lustra bé: quina implementació de ServeiCorreu es fa servir és una decisió d'entorn i encaixa en un perfil; si el caducador de lloguers s'executa en aquesta instància concreta (07-03) no ho és, perquè en escalar voldrem apagar-lo a totes menys una sense canviar-los el perfil.
La regla pràctica: si et descobreixes escrivint @Profile("prod | pre | demo-client"), la condició va deixar de ser l'entorn fa temps. I un if (entorn.equals("prod")) dins d'un mètode de negoci sempre és un error: la lògica de negoci no ha de saber en quina màquina s'executa.
- Perfils i proves
A les proves, el perfil s'activa amb @ActiveProfiles (06-04):
@SpringBootTest
@ActiveProfiles("test")
class LloguerFluxCompletIT extends ProvaIntegracioBase {
// ...
}Tres coses que convé tenir clares. @ActiveProfiles substitueix, no afegeix: allò que hi hagués a spring.profiles.active s'ignora. Per afegir sense substituir existeix @ActiveProfiles(profiles = "extra", inheritProfiles = true) en jerarquies de classes de prova.
Cada combinació diferent de perfils crea un context diferent. És la lliçó de la memòria cau de contextos de 06-04: la clau de memòria cau inclou la llista de perfils actius, així que una classe amb @ActiveProfiles("test") i una altra amb @ActiveProfiles({"test", "correu-fals"}) arrenquen dos contextos complets. Estandarditzar els perfils de la suite a la classe base ProvaIntegracioBase no és cosmètica: és la diferència entre una suite de dos minuts i una de vint.
I el perfil test ha d'existir de debò. Un application-test.yml amb la base de dades de proves, Flyway desactivat, logs en INFO i el JWT d'un minut evita que les proves heretin per accident la configuració de dev —inclosa la consola H2 i el carregador de dades de demostració, que trencarien els asserts sobre les quatre estacions—.
- Comprovar quin perfil està actiu
La primera comprovació és gratis i és al log d'arrencada:
Quan no n'hi ha cap, el missatge és diferent i convé reconèixer-lo, perquè és l'avís que producció arrencarà amb la configuració per defecte:
Les altres dues vies són d'Actuator (07-01): /actuator/env mostra activeProfiles juntament amb les fonts de propietats carregades —entre elles applicationConfig: [classpath:/application-prod.yml], que confirma quin fitxer es va llegir—, i /actuator/configprops mostra els valors efectius de XarxaProperties i companyia. I des del codi, Environment ho exposa directament:
@Component
public class RegistreArrencada {
public RegistreArrencada(Environment entorn) {
log.info("Perfils actius: {}", Arrays.toString(entorn.getActiveProfiles()));
if (entorn.getActiveProfiles().length == 0) {
log.warn("Cap perfil actiu: es farà servir la configuració per defecte");
}
}
}
- Configuració externa: fitxers,
spring.config.import i variables d'entorn
spring.config.import i variables d'entornEls perfils resolen quina configuració es fa servir; falta d'on ve. Spring busca application.yml en diverses ubicacions, i les externes guanyen a les empaquetades:
| Ordre (guanya l'últim) | Ubicació |
|---|---|
| 1 | classpath:/application.yml (dins del JAR) |
| 2 | classpath:/config/application.yml |
| 3 | ./application.yml (al costat del JAR) |
| 4 | ./config/application.yml |
Aquesta cadena permet desplegar el JAR i deixar-hi al costat un config/application-prod.yml amb els valors de l'ajuntament, sense tornar a construir res. Per importar explícitament altres fonts hi ha spring.config.import:
spring:
config:
import:
- optional:file:./config/ # directori extern, si existeix
- optional:file:/etc/ciclourbana/secrets.yml
- optional:configtree:/run/secrets/ # secrets muntats com a fitxersEl prefix optional: és el que evita que l'aplicació no arrenqui quan el fitxer no existeix —imprescindible si la mateixa configuració val per al portàtil i per al servidor—; sense ell, un recurs absent és un error d'arrencada, que de vegades és justament el que es vol en producció. El prefix configtree: llegeix un directori on cada fitxer és una propietat i el seu contingut el valor, que és exactament el format en què Docker i Kubernetes munten els secrets (07-04, 08-04).
En contenidors, però, la via preferida són les variables d'entorn, per tres raons pràctiques: no requereixen muntar volums, les gestionen de manera nativa totes les plataformes de desplegament, i encaixen amb el factor III de 12-Factor App, que demana desar la configuració a l'entorn i no al codi. La traducció de noms és mecànica:
| Propietat d'Spring | Variable d'entorn |
|---|---|
spring.profiles.active |
SPRING_PROFILES_ACTIVE |
spring.datasource.url |
SPRING_DATASOURCE_URL |
ciclourbana.xarxa.capacitat-minima |
CICLOURBANA_XARXA_CAPACITATMINIMA |
ciclourbana.jwt.secret |
CICLOURBANA_JWT_SECRET |
La regla és: majúscules, els punts i els guions es converteixen en guions baixos. Spring aplica el relaxed binding de 02-05 en sentit invers, així que CICLOURBANA_XARXA_CAPACITATMINIMA i CICLOURBANA_XARXA_CAPACITAT_MINIMA funcionen totes dues.
- Secrets per entorn
De tot allò que canvia entre entorns, els secrets són l'única cosa que no pot viure al repositori. La contrasenya de PostgreSQL, el secret de signatura del JWT de 05-04 i la clau de la passarel·la de pagaments no han d'aparèixer a cap application-prod.yml versionat, ni tan sols «temporalment»: un cop un secret entra a l'historial de Git, hi continua encara que s'esborri al commit següent, i cal rotar-lo.
El patró que aplica CicloUrbana:
# application-prod.yml — versionat, SENSE valors secrets
spring:
datasource:
url: jdbc:postgresql://bd-ribalta:5432/ciclourbana
username: ciclourbana
password: ${POSTGRES_PASSWORD} # sense valor per defecte: obligatòria
ciclourbana:
jwt:
secret: ${JWT_SECRET}
expiracio: 15m
passarela:
url: https://pagaments.ribalta.example/api/v1
api-key: ${PASSARELA_API_KEY}Tres decisions deliberades. ${POSTGRES_PASSWORD} sense valor per defecte fa que l'aplicació falli en arrencar si la variable no hi és: és exactament el comportament que es vol, perquè una arrencada que falla sorollosament és millor que una que arrenca amb una contrasenya de desenvolupament. L'estructura sí que es versiona, perquè es vegi a la revisió de codi què necessita l'entorn. I el fitxer de producció amb valors reals no existeix: els valors els proporciona l'orquestrador des del seu gestor de secrets (Vault, AWS Secrets Manager, els secrets de Kubernetes).
Un .gitignore amb application-local.yml i .env tanca el cercle i dona a cada desenvolupador un fitxer personal per als seus valors, carregat amb spring.config.import: optional:file:./application-local.yml.
- Errors clàssics i com detectar-los aviat
El perfil no activat en producció. L'arrencada diu No active profile set i l'aplicació s'aixeca amb H2 en memòria, Swagger obert i Actuator sencer exposat. Encara pitjor: funciona, i ningú no se n'assabenta fins que les dades desapareixen al reinici següent. La defensa és un ApplicationListener que avorti si no hi ha perfil, o simplement exigir a application.yml una propietat obligatòria que només defineixin els fitxers d'entorn.
Propietats duplicades al fitxer comú i al de perfil. No és un error, és l'eina funcionant; el problema apareix quan algú corregeix el valor a application.yml i no entén per què producció continua igual. Regla: si una propietat és en un fitxer de perfil, treu-la del comú tret que vulguis un valor per defecte explícit.
El valor obligatori que falta en un entorn. És l'error més car, perquè pot trigar hores a aparèixer: l'aplicació arrenca, atén mil peticions i falla la primera vegada que algú intenta pagar, perquè ciclourbana.passarela.api-key estava buida. La solució ja la teníem a 02-05: @ConfigurationProperties validades.
@ConfigurationProperties(prefix = "ciclourbana.passarela")
@Validated
public record PassarelaProperties(
@NotBlank String url,
@NotBlank String apiKey,
@NotNull @DurationMin(millis = 200) Duration tempsEspera) {
}Amb això, un entorn al qual li falti PASSARELA_API_KEY no arrenca, i el missatge diu exactament quina propietat falta:
Binding to target com.ciclourbana.passarela.PassarelaProperties failed:
Property: ciclourbana.passarela.api-key
Reason: no ha d'estar buitUna fallada a l'arrencada, davant de l'orquestrador que encara no ha retirat la versió anterior, és infinitament preferible a una fallada intermitent en hores de servei. És la mateixa filosofia que ddl-auto: validate a 04-08: que l'error aparegui com més aviat millor i tan sorollosament com sigui possible.
Dependències implícites entre perfils. Un bean amb @Profile("prod") que necessita un altre amb @Profile("postgres") funciona mentre algú activi tots dos, i peta el dia que no. Els grups de l'apartat 6 ho fan explícit i comprovable.
Errors Comuns i Consells
Recompilar per entorn. Trenca el principi de la lliçó: el binari de producció no és el que es va provar. Un artefacte, configuració externa.
Confondre perfils de Maven amb perfils d'Spring. Els primers decideixen què s'empaqueta; els segons, com es comporta allò empaquetat.
Copiar l'application.yml sencer a cada fitxer de perfil. Garanteix que un canvi futur s'apliqui a tres dels quatre entorns. Només les diferències.
Esperar que les llistes es fusionin. Les col·leccions se substitueixen senceres. Si prod redefineix estacions-destacades, la del fitxer comú desapareix.
Versionar un application-prod.yml amb credencials. Un cop a l'historial de Git, el secret està compromès encara que s'esborri després: cal rotar-lo.
Abusar de @Profile("prod") per a opcions funcionals. Si la condició no és «on soc» sinó «què està activat», l'eina correcta és @ConditionalOnProperty.
Zero o dues implementacions de la mateixa interfície. Els perfils mal combinats produeixen NoSuchBeanDefinitionException o NoUniqueBeanDefinitionException a l'arrencada. Cobreix-ho amb grups.
Consell: anomena els perfils per capacitat, no per entorn (correu-real, dades-demo, postgres) i compon els entorns amb spring.profiles.group. El codi deixa de saber on viu.
Consell: revisa el log d'arrencada a cada desplegament. La línia The following N profiles are active és la comprovació més barata que existeix i detecta l'error més car.
Consell: valida la configuració amb @ConfigurationProperties i @Validated. Converteix una fallada de configuració en una fallada d'arrencada, que és on ha d'estar.
Exercicis
Exercici 1: separar els entorns de CicloUrbana
Partint d'un únic application.yml que avui té H2, ddl-auto: create-drop, Swagger habilitat, logging.level.com.ciclourbana: DEBUG, management.endpoints.web.exposure.include: "*" i ciclourbana.jwt.expiracio: 8h, escriu application.yml, application-dev.yml i application-prod.yml respectant el mapa de l'apartat 4. Indica què queda a cada fitxer i per què, i com s'arrenca cada entorn.
Exercici 2: perfils per capacitat i grups
CicloUrbana necessita dues implementacions de ServeiNotificacions (una que escriu al log, una altra que envia SMS per una passarel·la real) i dues de ServeiPagaments (un simulador que sempre aprova i el client real). Dissenya els perfils per capacitat, els grups que componen dev, pre i prod, i les anotacions necessàries, garantint que mai no hi hagi zero ni dos candidats. Després explica què passaria si algú arrenqués amb SPRING_PROFILES_ACTIVE=prod,notificacions-falses.
Exercici 3: diagnosticar un desplegament que «funciona però no hauria»
CicloUrbana s'ha desplegat al servidor de l'ajuntament i respon correctament, però l'equip observa que (a) els lloguers desapareixen cada vegada que es reinicia el servei, (b) https://ciclourbana.ribalta.example/swagger-ui.html és accessible des d'Internet, (c) el log creix a un ritme enorme i (d) /actuator/env retorna 200 amb la contrasenya visible. L'ordre d'arrencada és java -jar ciclourbana.jar i al servidor hi ha un application-prod.yml correcte dins del JAR. Diagnostica la causa arrel, explica com s'arriba a cada símptoma i proposa una correcció que impedeixi que torni a passar.
Solucions
Solució 1
application.yml — només allò comú a tots els entorns:
spring:
application:
name: ciclourbana
jpa:
open-in-view: false
properties:
hibernate.jdbc.batch_size: 25
ciclourbana:
xarxa:
ciutat: Ribalta
capacitat-minima: 8
llindar-bateria: 20
durada-maxima-lloguer: 2h
logging:
level:
root: INFOapplication-dev.yml — comoditat, sense dades reals a perdre:
spring:
datasource:
url: jdbc:h2:mem:ciclourbana;MODE=PostgreSQL;DB_CLOSE_DELAY=-1
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
h2:
console:
enabled: true
logging:
level:
com.ciclourbana: DEBUG
org.hibernate.SQL: DEBUG
springdoc:
swagger-ui:
enabled: true
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
ciclourbana:
jwt:
secret: secret-de-desenvolupament-de-32-caracters-minim
expiracio: 8happlication-prod.yml — mínim, tancat i sense secrets:
spring:
datasource:
url: jdbc:postgresql://bd-ribalta:5432/ciclourbana
username: ciclourbana
password: ${POSTGRES_PASSWORD}
jpa:
hibernate:
ddl-auto: validate
show-sql: false
h2:
console:
enabled: false
logging:
level:
com.ciclourbana: INFO
springdoc:
api-docs:
enabled: false
swagger-ui:
enabled: false
management:
server:
port: 8081
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: when-authorized
roles: ADMIN
ciclourbana:
jwt:
secret: ${JWT_SECRET}
expiracio: 15mQuè va on i per què. Al comú hi queda allò que no ha de variar: el nom de l'aplicació, open-in-view: false —una decisió d'arquitectura de 04-02, no d'entorn— i les regles de negoci de la xarxa de Ribalta, que han de ser idèntiques a tot arreu perquè allò provat sigui allò desplegat. Als fitxers de perfil hi queden les quatre famílies que sí que canvien: origen de dades, verbositat, superfície exposada i política de seguretat.
Fixa't que ddl-auto, swagger-ui.enabled i exposure.include apareixen en tots dos fitxers de perfil i en cap el comú. És deliberat: per a propietats perilloses, un valor per defecte a application.yml significa que un perfil que s'oblidi de declarar-les hereta el valor còmode. En no existir-hi per defecte, cada entorn es veu obligat a decidir.
Arrencada de cada entorn:
./mvnw spring-boot:run # dev, si és l'actiu per defecte
java -jar target/ciclourbana.jar --spring.profiles.active=dev
SPRING_PROFILES_ACTIVE=prod POSTGRES_PASSWORD=... JWT_SECRET=... java -jar ciclourbana.jarSolució 2
Perfils per capacitat, quatre en dues parelles mútuament excloents: notificacions-log / notificacions-sms i pagaments-simulats / pagaments-reals.
@Service
@Profile("notificacions-sms")
public class ServeiNotificacionsSms implements ServeiNotificacions { /* ... */ }
@Service
@Profile("!notificacions-sms") // xarxa de seguretat: és el que queda si no n'hi ha cap altre
public class ServeiNotificacionsRegistrades implements ServeiNotificacions { /* ... */ }
@Service
@Profile("pagaments-reals")
public class ClientPassarelaPagaments implements ServeiPagaments { /* ... */ }
@Service
@Profile("!pagaments-reals")
public class SimuladorPagaments implements ServeiPagaments { /* ... */ }spring:
profiles:
group:
dev: notificacions-log, pagaments-simulats, dades-demo
pre: notificacions-sms, pagaments-simulats, cors-estricte
prod: notificacions-sms, pagaments-reals, cors-estricteLa clau del disseny és a la negació. Fer servir @Profile("notificacions-log") a la implementació de reserva permetria que un entorn es quedés sense cap implementació si oblida activar-la; amb @Profile("!notificacions-sms") sempre n'hi ha exactament una: la real quan el perfil està actiu, la de registre en qualsevol altre cas. Els perfils positius (notificacions-log, pagaments-simulats) continuen apareixent als grups perquè documenten la intenció, encara que tècnicament ja no siguin necessaris.
L'alternativa igualment vàlida és deixar els dos perfils positius i marcar un dels beans amb @ConditionalOnMissingBean(ServeiNotificacions.class) (02-06). L'efecte és el mateix; l'avantatge de la negació és que no depèn de l'ordre de registre de les autoconfiguracions.
Què passa amb SPRING_PROFILES_ACTIVE=prod,notificacions-falses. El perfil notificacions-falses no existeix enlloc, així que s'activa i no fa absolutament res —Spring no valida que un perfil correspongui a alguna cosa—. El grup prod continua expandint-se, notificacions-sms queda actiu i se segueixen enviant SMS reals. Aquest és precisament el perill dels perfils: un nom mal escrit no produeix cap error, només un comportament diferent de l'esperat. La defensa és doble: un @PostConstruct que comprovi quina implementació ha quedat registrada i ho escrigui al log (log.info("Notificacions: {}", servei.getClass().getSimpleName())), i una prova d'integració per entorn que verifiqui el tipus del bean amb @ActiveProfiles("prod").
Solució 3
La causa arrel és una de sola: el perfil prod no es va activar mai. L'ordre java -jar ciclourbana.jar no passa --spring.profiles.active, no hi ha SPRING_PROFILES_ACTIVE a l'entorn del servei i application.yml no declara spring.profiles.active. El log d'arrencada en conté la prova: No active profile set, falling back to 1 default profile: "default".
D'aquí surten els quatre símptomes, tots pel mateix mecanisme —application-prod.yml existeix dins del JAR però no es llegeix mai, així que regeix només application.yml, que és el de desenvolupament—:
| Símptoma | Propietat que va quedar vigent | Per què es manifesta així |
|---|---|---|
| (a) Els lloguers desapareixen en reiniciar | spring.datasource.url d'H2 en memòria |
L'aplicació funciona perquè H2 crea l'esquema amb ddl-auto: create-drop; les dades viuen a la RAM i moren amb el procés |
| (b) Swagger accessible des d'Internet | springdoc.swagger-ui.enabled: true |
Exposa l'inventari complet de l'API i els seus models a qualsevol |
| (c) El log creix sense control | logging.level.com.ciclourbana: DEBUG i org.hibernate.SQL: DEBUG |
Cada consulta s'escriu sencera; amb trànsit real, gigabytes al dia |
(d) /actuator/env retorna 200 amb la contrasenya |
exposure.include: "*" i show-values: always |
És l'escenari de l'exercici 3 de la lliçó anterior, en la seva versió accidental |
I hi ha un cinquè símptoma que encara no s'ha manifestat i és el més greu: la base de dades PostgreSQL de l'ajuntament està intacta i buida de trànsit, perquè ningú no hi ha escrit des del desplegament. Els lloguers dels ciutadans d'aquests dies estan perduts.
La correcció immediata és arrencar amb el perfil, preferiblement per variable d'entorn a la unitat de servei o al contenidor:
La correcció estructural, que és el que demana l'enunciat, té tres capes.
Primer, que l'aplicació es negui a arrencar sense perfil, perquè dependre que algú recordi una variable és dependre de la memòria:
@Component
public class ValidadorDePerfil {
public ValidadorDePerfil(Environment entorn) {
if (entorn.getActiveProfiles().length == 0) {
throw new IllegalStateException(
"Cap perfil actiu. Arrenca amb SPRING_PROFILES_ACTIVE=dev|test|pre|prod");
}
}
}Segon, que els valors perillosos no tinguin un valor per defecte còmode. Si spring.datasource.url no és a application.yml, l'aplicació sense perfil no arrenca amb H2: no arrenca en absolut. El mateix amb el secret del JWT declarat com a ${JWT_SECRET} sense valor per defecte, i amb PassarelaProperties validada de l'apartat 13. La idea de fons: treure del mig la possibilitat que un descuit produeixi un sistema que funciona a mitges, perquè aquest és molt pitjor que un que no funciona.
Tercer, que el desplegament es verifiqui. Una comprovació posterior al desplegament que consulti /actuator/info i comprovi la versió, i una altra que consulti /actuator/env esperant 401 o 404, haurien detectat els quatre símptomes al primer minut. A 08-05 això es converteix en un pas automàtic del lliurament continu.
Conclusió
CicloUrbana ja distingeix on viu. Has interioritzat el principi que governa aquesta lliçó —construir una vegada, desplegar en molts llocs— i saps per què recompilar per entorn significa desplegar un binari que ningú no va provar. Coneixes les sis formes d'activar un perfil i la seva precedència, amb SPRING_PROFILES_ACTIVE com la forma estàndard en contenidors, i les dues propietats menys conegudes, spring.profiles.default i spring.profiles.include. Saps com es combinen application.yml i els fitxers de perfil, que la fusió és per clau, que les llistes se substitueixen senceres i que amb diversos perfils actius guanya l'últim. Tens el mapa complet de configuració de CicloUrbana per entorn —base de dades, ddl-auto, Flyway, logs, Swagger, CORS, caducitat del JWT, exposició d'Actuator, dades de demostració— com a plantilla per a qualsevol projecte.
Domines els documents multiperfil amb --- i spring.config.activate.on-profile, i els seus dos límits, que són els que aconsellen preferir fitxers separats. Saps compondre entorns amb spring.profiles.group a partir de perfils que descriuen capacitats i no llocs, i condicionar beans i classes de configuració amb @Profile i les seves expressions: CarregadorDadesDemo idempotent a dades-demo, les dues cares de ServeiCorreu i ConfiguracioCorsProduccio. I, sobretot, saps quan @Profile és l'eina equivocada: quan la condició no és on ets sinó què està activat, la resposta és una propietat i @ConditionalOnProperty, amb la taula de criteris per decidir-ho sense discutir.
A les proves, @ActiveProfiles substitueix en lloc d'afegir i cada combinació obre un context nou, així que estandarditzar-los a ProvaIntegracioBase és el que manté la suite en minuts. Saps comprovar el perfil actiu al log d'arrencada, a /actuator/env i des d'Environment, injectar configuració des de fora de l'artefacte amb la cadena d'ubicacions, spring.config.import i el prefix optional:, i per què en contenidors manen les variables d'entorn. I tens una política de secrets: l'estructura es versiona, els valors mai; ${VARIABLE} sense valor per defecte perquè l'absència sigui una fallada d'arrencada; i @ConfigurationProperties validades que converteixen un descuit de configuració en un error immediat i explícit, en lloc d'una crida fallida a la passarel·la tres hores després.
Amb Actuator i els perfils, CicloUrbana és observable i sap adaptar-se al seu entorn. Però continua sent una aplicació purament reactiva: només fa alguna cosa quan algú li demana alguna cosa. Ningú no tanca de matinada els lloguers que un ciutadà va oblidar de finalitzar, ningú no recalcula l'ocupació de les quatre estacions de Ribalta cada minut per a l'aplicació mòbil, i el correu de confirmació se segueix enviant al mateix fil que atén la petició, obligant el ciutadà a esperar que el servidor SMTP contesti. La lliçó següent, Tasques Programades i Execució Asíncrona, dona a CicloUrbana iniciativa pròpia i la capacitat de fer coses en segon pla, amb tots els paranys que això comporta: el planificador d'un sol fil, la tasca que s'executa N vegades en escalar, la transacció que no viatja a l'altre fil i el context de seguretat que es queda enrere.
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
