La lliçó anterior va recollir les pràctiques de CicloUrbana escrites en positiu: el que convé fer i per què. Aquesta recorre el revers, que és com gairebé tots aprenem de debò. Cada pràctica de 10-01 és la cicatriu d'un error concret que algú va cometre abans, i conèixer l'error —el seu símptoma exacte, el seu missatge al log, la forma que té— val més que conèixer la regla, perquè el dia que aparegui el reconeixeràs en lloc d'investigar-lo durant tres hores.
El catàleg està ordenat per on apareixen, no per gravetat, perquè així és com es troben: primer els que impedeixen arrencar, després els que arrenquen però no fan res, i per últim els que funcionen fins que arriba el volum. Tots segueixen el mateix esquema —símptoma → causa → diagnòstic → solució— i tots porten el codi que falla al costat del corregit. I hi ha una secció destacada per al parany que hem trepitjat tres vegades al llarg del curs i que mereix per fi un tractament únic: la del proxy.
Contingut
- Com llegir aquest catàleg
- Arrencada i context
- El parany del proxy
- Configuració
- JPA i persistència
- REST i el contracte
- Seguretat
- Concurrència
- Rendiment
- Proves
- Taula de diagnòstic ràpid
- Errors Comuns i Consells
- Exercicis
- Com llegir aquest catàleg
Els errors d'aquest catàleg es divideixen en tres famílies que exigeixen actituds diferents:
| Família | Com es manifesta | Cost real |
|---|---|---|
| Sorollosos | L'aplicació no arrenca o llança una excepció clara | Baix: molesten una tarda i s'arreglen |
| Silenciosos | Tot funciona, però no fa el que et penses | Alt: es descobreixen en producció, setmanes després |
| Diferits | Funcionen avui i fallen amb el volum o la concurrència | Molt alt: apareixen en el pitjor moment |
La llista està esbiaixada expressament cap a les dues últimes. Un NoSuchBeanDefinitionException costa vint minuts; un @Transactional que s'ignora en silenci costa una reconciliació de dades.
- Arrencada i context
2.1. La classe principal fora del paquet arrel
Símptoma. NoSuchBeanDefinitionException sobre beans que existeixen i estan correctament anotats. O pitjor: l'aplicació arrenca, però cap endpoint no respon i no hi ha cap error.
Causa. @SpringBootApplication inclou @ComponentScan sense arguments, cosa que escaneja el paquet de la classe anotada i tots els seus subpaquets. Si CicloUrbanaApplication viu a com.ciclourbana.arrencada, els paquets com.ciclourbana.estacions i com.ciclourbana.lloguers queden fora de l'escaneig.
Diagnòstic. Compara el paquet de la classe principal amb el dels beans que no apareixen, i confirma-ho amb /actuator/beans, que no els llista.
Solució. Moure CicloUrbanaApplication a com/ciclourbana/, arrel de tots els paquets de negoci. Afegir @ComponentScan("com.ciclourbana") funciona, però és un pedaç que emmascara una estructura mal posada.
2.2. NoSuchBeanDefinitionException amb la classe al davant
Símptoma. Parameter 0 of constructor in com.ciclourbana.lloguers.LloguerService required a bean of type 'CalculadoraTarifa' that could not be found.
Causes possibles, en ordre de freqüència. La classe no té estereotip (@Component, @Service, @Repository); és fora del paquet escanejat (2.1); està condicionada per un @Profile o @ConditionalOnProperty que no es compleix; o es defineix amb @Bean en una classe que no és @Configuration.
Diagnòstic. Arrencar amb --debug imprimeix l'informe d'autoconfiguració, que llista les condicions avaluades positives i negatives amb el seu motiu. És l'eina més infrautilitzada de Spring Boot.
2.3. Dependències circulars
Símptoma. El requadre ┌─────┐ ... └─────┘ amb la llista de beans del cicle, i APPLICATION FAILED TO START.
Causa. A necessita B i B necessita A. Amb injecció per constructor és lògicament impossible.
Diagnòstic. El mateix missatge diu quins beans formen el cicle i en quin fitxer estan definits.
Solució. Les tres d'Injecció de Dependències: extreure la responsabilitat compartida a un tercer component, invertir la direcció amb un esdeveniment, o replantejar qui ha de cridar qui.
El que no és solució. spring.main.allow-circular-references=true ni @Lazy en un dels dos punts: fan desaparèixer el missatge sense arreglar res, i el cicle continua produint un ordre d'inicialització imprevisible.
2.4. Dos candidats sense @Primary
Símptoma. required a single bean, but 2 were found: tarifaEstandard, tarifaEstudiant.
Solució. @Primary sobre la implementació dominant quan n'hi ha una, @Qualifier al punt d'injecció quan cal una de concreta, o —el més robust en projectes grans— un qualificador propi com @Universitaria, que el compilador verifica.
L'error associat, més perillós. Confiar en la coincidència de noms: anomenar el paràmetre tarifaEstudiant perquè Spring triï aquell bean. Funciona, i es trenca en silenci el dia que un IDE reanomena el paràmetre.
- El parany del proxy
Aquesta secció unifica el que fins ara hem vist tres vegades per separat, a Transaccions, a Seguretat a Nivell de Mètode i a Tasques Programades i Asincronia. És l'error més freqüent de tot el curs i el més silenciós.
3.1. Per què passa
Cap d'aquestes anotacions no modifica el teu codi. Spring crea un proxy —una subclasse generada per CGLIB— que embolcalla el teu bean, i el que s'injecta a la resta de l'aplicació no és el teu objecte: és el proxy.
flowchart LR
C["LloguerController"] -->|"crida externa:<br/>SÍ que travessa el proxy"| P["Proxy de LloguerService"]
P -->|"obre transacció,<br/>comprova permisos,<br/>consulta la memòria cau"| S["LloguerService (objecte real)"]
S -->|"this.metode():<br/>NO travessa el proxy"| S
D'aquí la regla única que explica els quatre casos: només les crides que entren des de fora passen pel proxy. Una crida d'un mètode de la classe a un altre de la mateixa classe va per this i l'anotació s'ignora completament i sense cap avís.
3.2. Els quatre casos, amb el seu símptoma característic
| Anotació | Què deixa de passar | Símptoma real |
|---|---|---|
@Transactional |
No hi ha transacció | Cada operació s'autoconfirma; una fallada a mitges deixa dades inconsistents sense error |
@Async |
No hi ha asincronia | El mètode s'executa síncron; la latència no baixa per molts fils que hi afegeixis |
@Cacheable |
No hi ha memòria cau | La taxa d'encerts és 0 % i ningú no entén per què |
@PreAuthorize |
No hi ha comprovació de permisos | Cap. I això és exactament el problema |
L'últim és el més greu del curs: una regla de seguretat que no s'executa i no avisa.
// ❌ Les tres anotacions internes s'ignoren
@Service
public class LloguerService {
@Transactional
public void finalitzarLot(List<Long> ids) {
for (Long id : ids) {
finalitzar(id, peticio); // this.finalitzar(...): sense proxy
}
}
@PreAuthorize("@seguretatLloguers.esPropietari(#idLloguer, principal)")
@Transactional
public LloguerResponse finalitzar(Long idLloguer, FinalitzarLloguerRequest p) { ... }
}3.3. Els altres dos disparadors: visibilitat i final
Amb proxies CGLIB, Spring genera una subclasse que sobreescriu els mètodes. D'aquí:
| Situació | Resultat |
|---|---|
Mètode private |
No es pot sobreescriure: l'anotació s'ignora en silenci |
Mètode protected o de paquet |
No s'intercepta de manera fiable |
Mètode final |
No es pot sobreescriure: s'ignora |
Classe final |
No es pot crear el proxy: fallada d'arrencada o bean sense interceptar |
Objecte creat amb new |
No és un bean: no hi ha proxy ni cap anotació funciona |
Crida des del constructor o @PostConstruct |
El proxy encara no està muntat |
Des de Spring Framework 6.0 l'arrencada registra un avís en detectar @Transactional en un mètode no públic. És una ajuda, no una garantia.
3.4. Les tres formes de resoldre-ho
A. Extreure a un altre bean (la recomanada). A més de funcionar, gairebé sempre millora el disseny: orquestrar un lot i executar l'operació individual són responsabilitats diferents.
// ✅ La crida surt d'un bean i entra en un altre: travessa el proxy
@Service
public class ProcessadorDevolucions {
private final LloguerService lloguerService; // el PROXY, no l'objecte real
public void processarLot(List<Long> ids) {
ids.forEach(id -> lloguerService.finalitzar(id, peticio));
}
}B. Autoinjecció del proxy d'un mateix. Funciona i és lletja: un camp @Lazy private final LloguerService self i cridar self.finalitzar(...); el @Lazy trenca el cicle. Es fa servir quan extreure no compensa, i convé comentar per què. C. Gestió programàtica. Per a @Transactional, TransactionTemplate no depèn de proxies i dona control exacte de l'abast; és la sortida quan cal una transacció per iteració d'un bucle.
Com confirmar que el proxy actua. Per a transaccions, logging.level.org.springframework.transaction: DEBUG ha d'imprimir Creating new transaction on ho esperes. Per a memòria cau, /actuator/metrics/cache.gets. Per a tasques, /actuator/scheduledtasks. Si no apareix res, és aquest parany.
- Configuració
4.1. El perfil que no es va activar mai
Símptoma. En producció: les dades desapareixen en reiniciar, Swagger és accessible des d'Internet, el log creix sense control i /actuator/env retorna 200 amb la contrasenya visible.
Causa. Una sola errada: java -jar ciclourbana.jar sense --spring.profiles.active i sense SPRING_PROFILES_ACTIVE a l'entorn. application-prod.yml és dins del JAR però no es llegeix mai.
Diagnòstic. El log d'arrencada conté la prova literal:
Solució. Fixar el perfil a l'entorn del contenidor o del servei, i —això és el que impedeix que torni a passar— una comprovació a l'arrencada que falli si el perfil actiu és buit en un entorn que no sigui local.
4.2. La propietat mal escrita que s'ignora en silenci
Símptoma. Es canvia un valor al YAML i no passa res.
Causa. Spring Boot no valida que les propietats d'un fitxer es corresponguin amb res. ciclourbana.tarifes.preu-minu: 0.15 simplement no s'enllaça a res, i el valor per defecte continua vigent.
Diagnòstic. /actuator/configprops mostra els valors efectius de cada @ConfigurationProperties; /actuator/env mostra de quina font surt cada propietat. Si el valor efectiu no és el que vas escriure, la clau està malament.
Solució. Dues, complementàries. El processador de metadades (spring-boot-configuration-processor), que fa que l'IDE autocompleti i subratlli en vermell el que no existeix. I @ConfigurationProperties en lloc de @Value, perquè l'enllaç d'un record amb @Validated falla en arrencar si un camp obligatori queda a nul.
4.3. YAML mal indentat i la precedència inesperada
Símptoma A. Una branca sencera de configuració s'ignora. Causa: un nivell d'indentació de més o de menys converteix hikari en fill de la clau equivocada. YAML és sensible als espais i no admet tabuladors.
Símptoma B. El valor del fitxer no guanya. Causa: la precedència, de major a menor: arguments de línia d'ordres → variables d'entorn → fitxers externs → fitxers dins del JAR. Una SPRING_DATASOURCE_URL heretada de l'entorn guanya sempre al fitxer, i és una font clàssica de desconcert en contenidors.
Símptoma C. Una llista té menys elements dels esperats. Causa: en combinar perfils, les llistes i els mapes se substitueixen sencers, no es fusionen.
4.4. Secrets versionats
Símptoma. Cap. Aquest és el problema.
Causa. Una contrasenya «temporal» en un application-prod.yml, un .env sense .gitignore, o un COPY . . al Dockerfile sense .dockerignore que fica el directori .git complet dins de la imatge.
Diagnòstic i solució. Una eina de detecció de secrets sobre l'historial complet (--log-opts="--all"), no sobre la còpia de treball. I després, rotar el secret: esborrar-lo del codi no el desactiva, perquè continua a l'historial de Git, a cada clon i a cada imatge construïda des d'aleshores.
- JPA i persistència
5.1. LazyInitializationException
Símptoma. failed to lazily initialize a collection of role: ... could not initialize proxy - no Session.
Causa. S'accedeix a una relació mandrosa fora de la transacció, típicament durant la serialització JSON, perquè open-in-view: false tanca el context de persistència en acabar el mètode del servei.
Solució. No és carregar més coses: és mapejar a DTO dins de la transacció. L'entitat no ha de sortir del servei.
// ❌ L'entitat surt amb relacions sense carregar
@Transactional(readOnly = true)
public Estacio obtenir(Long id) { return estacioRepositori.findById(id).orElseThrow(); }
// ✅ El mapatge passa amb la transacció oberta
@Transactional(readOnly = true)
public EstacioDetallResponse obtenirDetall(Long id) {
Estacio estacio = estacioRepositori.cercarAmbBicicletes(id)
.orElseThrow(() -> new RecursNoTrobatException("Estació", id));
return mapper.aDetall(estacio);
}El que no és solució. open-in-view: true ni marcar la relació com a EAGER. La primera amaga el problema darrere de la serialització; la segona el converteix en el 5.2.
5.2. L'N+1 silenciós i l'EAGER per defecte
Símptoma. L'endpoint funciona i la latència creix proporcionalment al nombre de resultats. Amb 20 estacions triga 200 ms; amb 200, dos segons.
Causa. Una consulta per a la llista i una més per cada element. I una causa estructural que molta gent desconeix: @ManyToOne i @OneToOne són EAGER per defecte a JPA, així que una relació que ningú no va declarar mandrosa dispara una consulta per fila sense que aparegui al codi.
Diagnòstic.
201 sentències per a 200 estacions no necessita més anàlisi. I a les traces de 09-06 es veu directament: vint spans select idèntics i consecutius.
Solució. Declarar totes les associacions FetchType.LAZY, i carregar el que calgui amb @EntityGraph, JOIN FETCH o —el millor quan només s'ha de mostrar— una projecció que faci comptar la base de dades.
Com evitar que torni. Una prova amb @DataJpaTest que afirmi el nombre de consultes: assertThat(comptador.getTotal()).isLessThanOrEqualTo(2). Sense aquesta prova, l'N+1 torna d'aquí a tres mesos.
5.3. equals i hashCode mal implementats en entitats
Símptoma. Una entitat afegida a un HashSet no es troba després de desar-la, o un Set amb duplicats aparents.
Causa. L'equals generat per l'IDE inclou l'id, que és null abans de persistir i deixa de ser-ho després: el hashCode canvia mentre l'objecte és dins del HashSet, i l'element queda inaccessible en un cubell equivocat.
Solució. equals basat en l'id, tolerant a proxies, i un hashCode constant per classe:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Estacio altra)) return false;
return id != null && id.equals(altra.getId()); // sense id, només igual a si mateixa
}
@Override
public int hashCode() { return getClass().hashCode(); } // estable sempreI la regla pràctica que evita el problema sencer: no fiquis entitats sense persistir en un HashSet.
5.4. save() innecessari i les cascades que esborren de més
Símptoma A. Res visible; només soroll. Causa: cridar save() sobre una entitat ja gestionada. El dirty checking genera l'UPDATE en el commit sense que ningú ho demani; el save() és redundant i fa creure al lector que sense ell no es desaria.
Símptoma B. En esborrar una estació desapareixen les seves bicicletes, i amb elles l'històric. Causa: cascade = CascadeType.ALL amb orphanRemoval = true posat «perquè funcioni el desament», sense adonar-se que ALL inclou REMOVE.
// ❌ Esborrar l'estació esborra la seva flota
@OneToMany(mappedBy = "estacio", cascade = CascadeType.ALL, orphanRemoval = true)
// ✅ Només el que té sentit: una bicicleta sobreviu a la seva estació
@OneToMany(mappedBy = "estacio", cascade = {CascadeType.PERSIST, CascadeType.MERGE})El criteri: la cascada d'esborrat només és correcta quan el fill no té sentit sense el pare —les línies d'una factura—, i una bicicleta de Ribalta existeix amb independència d'on estigui ancorada.
5.5. La transacció que no reverteix
Símptoma. El lloguer queda creat encara que el pagament fallés. O bé, UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only.
Causa. Es va capturar l'excepció i no es va rellançar. Sense excepció que arribi al proxy, el proxy fa commit. I si l'excepció es va llançar en un mètode intern també transaccional, aquest ja va marcar la transacció per a rollback, de manera que el commit final peta.
// ❌ El lloguer es registra sense cobrar
try { passarelaPagament.cobrar(importTotal); }
catch (PagamentRebutjatException e) { log.error("Pagament rebutjat", e); }
// ✅ Rellançar com a excepció del domini
catch (PagamentRebutjatException e) {
log.error("Pagament rebutjat per al lloguer {}", lloguer.getId(), e);
throw new ReglaNegociException("PAGAMENT_REBUTJAT", "El pagament ha estat rebutjat");
}La variant germana: esperar rollback amb una excepció comprovada. Per defecte Spring només reverteix davant de RuntimeException i Error. A CicloUrbana no dona problemes perquè tota la jerarquia de CicloUrbanaException hereta de RuntimeException; on no sigui així, cal rollbackFor = Exception.class. I la més subtil: @Transactional sobre un mètode void el cos del qual captura i registra totes les fallades: no llança mai, la transacció sempre confirma i qui el crida no té manera de saber que no s'ha fet res.
5.6. L'OFFSET gran
Símptoma. La pàgina 1 vola; la pàgina 10.000 triga segons. Causa: LIMIT 20 OFFSET 200000 obliga PostgreSQL a produir i descartar 200.000 files abans de retornar-ne 20.
Solució. Paginació per keyset: el client envia l'última fila vista i la consulta es posiciona a l'índex en lloc de comptar. I per a llistats on el total no es necessita, Slice en lloc de Page, perquè el count(*) de cada Page sol ser més car que la consulta de dades.
- REST i el contracte
6.1. Exposar entitats i les seves tres conseqüències
Símptoma A: fuita de dades. GET /api/v1/usuaris/1 retorna contrasenyaHash i el DNI d'un ciutadà. Ningú no ho va decidir: el mecanisme per defecte és publicar.
Símptoma B: referències circulars. StackOverflowError o una resposta de diversos megabytes en serialitzar Estacio → bicicletes → Bicicleta → estacio → ....
Símptoma C: l'app mòbil deixa de mostrar un camp. Algú va reanomenar una propietat del domini i el JSON va canviar amb ella.
Solució. DTOs, sense l'excepció «només en aquest endpoint». I per al cas C, una prova de contracte que afirmi exactament quines claus retorna la resposta pública: si algú afegeix un camp intern, falla abans del desplegament.
6.2. DTOs sense validar i el mass assignment
Símptoma. Arriba un null on no hauria d'arribar, o un usuari es registra amb {"rols":["ADMIN"]}.
Causa. Falta @Valid al @RequestBody —les restriccions del DTO hi són, però ningú no les avalua—, o el DTO d'entrada té camps que el client no hauria de poder fixar.
La regla que ho resol sencer: el que no és al DTO de petició no es pot modificar. ActualitzarUsuariRequest conté nom i tipusTarifa; ni rols, ni contrasenya, ni actiu, ni id.
6.3. Retornar 200 per a tot i empassar-se les excepcions
Símptoma. El client rep 200 amb un cos buit o {"error": "alguna cosa ha anat malament"}, i les mètriques de 09-03 mostren zero errors mentre els ciutadans es queixen.
Causa. Un try { ... } catch (Exception e) { return ResponseEntity.ok().build(); } al controlador, o un @ExceptionHandler que retorna sempre el mateix codi.
Per què importa més del que sembla. El codi d'estat governa els reintents dels clients, el comportament de memòries cau i proxies, i totes les alertes de disponibilitat. Un error retornat com a 200 és invisible per a la monitorització.
Solució. Un @RestControllerAdvice únic que tradueixi cada excepció del domini al seu codi: 404 per a RecursNoTrobatException, 409 per a ConflicteRecursException, 422 per a ReglaNegociException, 400 per a la validació.
6.4. Serialitzar Page directament
Símptoma. Un avís a l'arrencada —Serializing PageImpl instances as-is is not supported— i un JSON amb pageable, sort.sorted i altres camps interns de Spring Data convertits, sense que ningú ho decideixi, en part del contracte públic.
Solució. Un embolcall propi, que és el que fa PaginaResponse<T>: contingut, pàgina, mida, total d'elements i total de pàgines, i res més. El contracte deixa de dependre de l'estructura interna d'una llibreria.
- Seguretat
7.1. L'ordre de les regles deixa un endpoint obert
Símptoma. Un endpoint que hauria de requerir rol respon 200 sense autenticació.
Causa. authorizeHttpRequests avalua en ordre i guanya la primera coincidència. Una regla general col·locada abans que una d'específica fa que l'específica no s'avaluï mai.
Diagnòstic. Una prova amb spring-security-test que recorri una llista de rutes sense token i afirmi 401/403 en totes. És l'única manera fiable: llegir l'ordre a ull funciona malament tan bon punt hi ha més de sis regles.
I la regla estructural: acabar amb anyRequest().denyAll(). Amb denegació per defecte, oblidar una regla produeix un 403 visible; amb permís per defecte, produeix un forat.
7.2. CSRF desactivat sense entendre per què
Símptoma. Cap d'immediat. El risc apareix si canvia el mecanisme d'autenticació.
El raonament correcte. CSRF protegeix contra el fet que el navegador adjunti automàticament una credencial a una petició originada en un altre lloc, i això passa amb galetes de sessió. Amb un token Bearer que el client adjunta explícitament, l'atac no aplica i desactivar-lo és correcte.
On és el parany: el dia que algú decideixi guardar el JWT en una galeta —una decisió raonable per altres motius—, CSRF torna a ser necessari i ningú no ho recordarà. Per això la línia que el desactiva ha de portar escrita la seva condició: // sense CSRF: la credencial viatja a Authorization, no en una galeta.
7.3. Contrasenyes sense codificar i JWT mal construïts
| Error | Símptoma | Solució |
|---|---|---|
| Guardar la contrasenya en clar o amb MD5/SHA-1 | Cap, fins a la filtració | BCrypt amb cost revisat, o Argon2 |
Comparar contrasenyes amb equals |
Vulnerable a atacs de temps | Sempre passwordEncoder.matches(...) |
| JWT sense caducitat | Un token robat serveix per sempre | exp curt (≤15 min) i refresc rotatori |
| Dades sensibles al payload | El JWT és llegible, només va signat | Només identificador i rols |
| Secret de signatura curt o al repositori | Es pot falsificar qualsevol token | ≥256 bits, aleatori, per variable d'entorn i rotable |
El segon punt de la taula mereix èmfasi perquè és contraintuïtiu: un JWT no està xifrat. Qualsevol pot descodificar-ne la càrrega útil en un navegador. La signatura garanteix que no s'ha manipulat, no que ningú no el pugui llegir.
7.4. Filtrar el missatge d'error intern
Símptoma. La resposta conté ERROR: relation "usuaris" does not exist, una traça de pila o una ruta del sistema de fitxers. Causa: retornar e.getMessage() d'una excepció d'origen desconegut, o deixar server.error.include-stacktrace al seu valor per defecte.
Solució. include-stacktrace: never, include-message: never, i un ProblemDetail amb missatge controlat més l'identificador de rastreig. El detall real viu al log del servidor, on el ciutadà no arriba i el suport sí.
- Concurrència
8.1. Estat mutable en un singleton
Símptoma. Un comptador que perd increments, una dada que apareix a la resposta d'un altre usuari, una fallada irreproduïble en local que només passa sota càrrega.
Causa. Un camp mutable en un bean singleton, compartit per tots els fils: private Usuari usuariActual és literalment l'usuari d'una altra petició, i private int lloguersProcessats perd increments.
Solució. L'estat per petició viatja als arguments o al MDC; l'estat compartit viu a la base de dades, a la memòria cau o en un Counter de Micrometer, que sí que és segur entre fils.
8.2. La tasca programada que s'executa N vegades en escalar
Símptoma. Amb tres rèpliques, tres correus de resum a cada ciutadà i tres cobraments de recàrrec.
Causa. Cada instància té el seu propi planificador. @Scheduled no coordina res entre processos.
Solució. ShedLock sobre la PostgreSQL que ja existeix, amb @SchedulerLock(name = ..., lockAtLeastFor = ..., lockAtMostFor = ...) i usingDbTime() perquè la referència temporal sigui el rellotge de la base de dades i no el de cada JVM. I la defensa de fons, que serveix fins i tot si el bloqueig falla: fer les tasques idempotents. Marcar una bicicleta com a MANTENIMENT dues vegades no canvia res; sumar un import a un acumulador dues vegades, sí.
8.3. El context que no viatja al fil asíncron
Símptoma. Un @PreAuthorize que falla dins d'un mètode asíncron per falta d'autenticació, i línies de log de la feina en segon pla sense rastreId, impossibles de relacionar amb la petició que les va originar.
Causa. El SecurityContextHolder i el MDC viuen en ThreadLocal i no creuen sols al fil de l'executor.
Solució. Embolcallar l'executor en un DelegatingSecurityContextAsyncTaskExecutor i registrar un TaskDecorator que copiï el MDC, amb la restauració en un finally —igual d'important que el MDC.remove() del FiltreRastreig, perquè el fil del pool es reutilitza i l'identificador quedaria enganxat contaminant les tasques següents—.
L'error conceptual associat: posar @Async i @Transactional al mateix mètode. La transacció tampoc no viatja: el fil de l'executor n'obre una de nova i independent, no veu les escriptures sense confirmar de qui el crida, i una entitat gestionada passada com a argument pertany a un EntityManager d'un altre fil. La regla és passar identificadors, mai entitats, i disparar la feina a AFTER_COMMIT.
- Rendiment
9.1. Optimitzar sense mesurar
Símptoma. Una tarda de feina i cap millora perceptible, o una millora que ningú no pot demostrar.
Causa. La intuïció sobre on se'n va el temps és notòriament dolenta: gairebé tothom aposta pel seu propi codi Java quan el repartiment real està dominat per la base de dades. I la llei d'Amdahl ho remata: fer deu vegades més ràpida la serialització JSON —que és el 5 % del temps— millora el conjunt un 5 %.
Solució. Línia base amb k6 desada al repositori, un canvi cada vegada, i tornar a mesurar. I l'SLO en p95 i p99, mai en la mitjana: amb 1.000 peticions, una mitjana de 120 ms pot amagar un p99 de 3 segons que afecta 20 ciutadans de cada mil.
9.2. La memòria cau que amaga una consulta mal escrita
Símptoma. El p95 millora, però la càrrega de la base de dades continua alta i qualsevol fallada de memòria cau produeix un pic enorme.
Causa. Es va afegir @Cacheable sobre un mètode que feia 43 consultes. La memòria cau no arregla la consulta: la oculta el 95 % del temps i la deixa intacta per al 5 % restant, que ara és el pitjor.
La regla d'or: primer s'elimina la feina innecessària, després es cacheja el que queda. Un endpoint que després de corregir l'N+1 i afegir un índex passa de 2.410 ms a 91 ms pot no necessitar memòria cau en absolut.
9.3. El pool desproporcionat i el DEBUG en producció
Símptoma A. Es puja maximum-pool-size de 10 a 100 i el rendiment baixa. Causa: una connexió activa és un nucli de CPU i un disc de la base de dades treballant; amb més connexions que capacitat real, el servidor gasta el temps canviant de context i competint per bloqueigs.
Diagnòstic. hikaricp.connections.pending distingeix dues situacions que es confonen sempre: si hi ha espera i la base de dades va sobrada, la concurrència és real i el pool es queda curt; si hi ha espera i la CPU de la base de dades està tranquil·la, alguna cosa reté connexions —una crida HTTP dins d'una transacció és el sospitós número u— i pujar el pool només allarga l'agonia.
Símptoma B. El disc s'omple i el cost de l'agregació de logs es dispara. Causa: logging.level.com.ciclourbana: DEBUG o org.hibernate.SQL: DEBUG heretats del perfil de desenvolupament. Solució: INFO com a nivell base i, quan calgui detall, pujar-lo en calent amb /actuator/loggers i baixar-lo en acabar. Amb un advertiment de seguretat: alguns clients HTTP registren les capçaleres completes en DEBUG, inclosa Authorization.
- Proves
10.1. @SpringBootTest per a tot
Símptoma. La suite triga vint-i-cinc minuts i ningú no l'executa en local.
Causa. El con de gelat: aixecar el context sencer per provar una fórmula de tarifa. I una causa secundària que sorprèn: Spring cacheja els contextos entre classes de prova, però cada combinació diferent de propietats, perfils o @MockitoBean en crea un de nou, així que una suite amb quinze variants aixeca quinze contextos.
Solució. Unificar la configuració de les proves d'integració en una classe base comuna —ProvaIntegracioBase— i baixar a la piràmide tot el que no necessiti el context.
10.2. Proves intermitents
| Causa | Símptoma | Solució |
|---|---|---|
LocalDateTime.now() al codi provat |
Falla a mitjanit o en el canvi d'hora | Clock injectat i Clock.fixed a la prova |
| Dependència de l'ordre d'execució | Falla en afegir una prova nova | Cada prova prepara el seu i no deixa rastre |
| Dades compartides entre proves | Falla en executar en paral·lel | @Transactional a la prova, o neteja explícita |
Thread.sleep esperant alguna cosa asíncrona |
Falla en una màquina més lenta | Awaitility amb condició i temps màxim |
| Dependència d'un servei extern real | Falla quan el servei està caigut | Testcontainers o un doble |
I la pitjor conseqüència, que és cultural: una sola prova intermitent entrena l'equip a reintentar en lloc d'investigar, i aquest costum acaba ignorant també les fallades reals.
10.3. La cobertura com a objectiu
Símptoma. 85 % de cobertura i errors en producció al codi cobert.
Causa. La cobertura mesura quin codi es va executar, no quin comportament es va verificar. Una prova sense ni un assert dona cobertura del 100 %.
// Cobertura total, zero verificació
@Test
void noVerificaAbsolutamentRes() {
new TarifaEstandard().calcular(Duration.ofMinutes(30));
}Solució. Llegir la cobertura com un mapa del vermell —un if de negoci sencer sense cobrir és una pregunta legítima— i no com una puntuació. I una regla mecànica de revisió: tota prova té almenys un assertThat o un assertThatThrownBy.
- Taula de diagnòstic ràpid
| Símptoma | On mirar primer | Lliçó |
|---|---|---|
NoSuchBeanDefinitionException |
Paquet de la classe principal, estereotip, @Profile; arrencar amb --debug |
02-01 |
| Requadre de dependències a l'arrencada | El mateix missatge: diu quins beans formen el cicle | 02-02 |
| «Aquesta anotació no fa res» | Parany del proxy: autoinvocació, mètode private/final, new |
Apartat 3 |
| La transacció no reverteix | Excepció capturada sense rellançar, o comprovada | 04-07 |
UnexpectedRollbackException |
Un mètode intern va marcar rollback i l'extern va capturar | 04-07 |
LazyInitializationException |
Entitat fora del servei; mapejar a DTO dins de la transacció | 04-07 |
| Latència proporcional al nombre de resultats | N+1: generate_statistics, comptador de consultes, la cascada de spans |
09-01 |
| Mediana bona i p99 pèssim | Espera de recurs: hikaricp.connections.pending, bloqueigs, GC |
09-01 |
| El valor del YAML s'ignora | /actuator/configprops i /actuator/env; indentació i precedència |
02-04 |
| En producció es comporta com en desenvolupament | El perfil no es va activar: buscar No active profile set al log |
07-02 |
| Un endpoint respon sense autenticació | Ordre de les regles; falta denyAll() al final |
05-02 |
| Un usuari accedeix a dades d'un altre | Falta @PreAuthorize per dada, o és en un mètode no interceptat |
05-05 |
| La resposta filtra SQL o traces | include-stacktrace, include-message, e.getMessage() aliè |
03-06 |
| Una tasca «va deixar d'executar-se» | Excepció que va escapar al planificador; /actuator/scheduledtasks |
07-03 |
| Feina duplicada en escalar | Sense ShedLock; i tasques no idempotents | 07-03 |
Logs asíncrons sense rastreId |
Falta el TaskDecorator que copia el MDC |
07-03 |
| Taxa d'encerts de memòria cau a 0 % | Autoinvocació, o clau que canvia a cada crida | 09-02 |
| Mètriques que fan caure Prometheus | Cardinalitat: una etiqueta amb molts valors diferents | 09-03 |
OOMKilled (codi 137) sense excepció |
MaxRAMPercentage massa alt o fuita; -Xlog:gc |
09-01 |
| La suite triga vint minuts | @SpringBootTest per tot arreu i contextos no reutilitzats |
06-04 |
Errors Comuns i Consells
Buscar la causa on és el símptoma. El planificador d'un sol fil fa que una tasca lenta bloquegi una altra, i el símptoma apareix a la tasca equivocada. Un pool retingut per crides HTTP produeix un p99 alt en endpoints que no hi tenen res a veure. Abans de mirar el codi del símptoma, pregunta't quin recurs comparteix amb la resta.
Arreglar el símptoma en lloc de la causa. @Lazy per a un cicle, open-in-view: true per a una LazyInitializationException, pujar el pool per a una espera de connexions, flyway:repair per a un checksum que no quadra. Els quatre fan desaparèixer el missatge i deixen el problema intacte.
Canviar diverses coses alhora en diagnosticar. Si toques l'índex, el pool i la memòria cau al mateix desplegament i millora, no saps quin va servir ni quin està empitjorant una altra cosa.
Ignorar els avisos de l'arrencada. Spring Boot avisa de @Transactional en mètodes no públics, de la serialització de PageImpl i d'open-in-view actiu. Una arrencada plena d'avisos que «sempre hi han estat» és un lloc on l'avís nou passa desapercebut.
Consell: quan alguna cosa «no fa res», sospita del proxy abans que de res. Quatre anotacions diferents, un sol mecanisme, un sol diagnòstic: la crida ve de fora del bean? el mètode és public i no final? l'objecte el va crear Spring?
Consell: converteix cada error de producció en una prova. Abans d'arreglar-lo, escriu la prova que el reprodueix i comprova-la en vermell. Així saps que vas arreglar el que et pensaves i garanteixes que no torna.
Consell: aprèn a llegir els tres informes que Spring Boot ja et dona. El d'autoconfiguració amb --debug, /actuator/env amb les fonts de cada propietat i /actuator/configprops amb els valors efectius. Entre els tres expliquen la majoria dels «però si jo ho vaig posar».
Exercicis
Exercici 1: sis fallades en un servei d'incidències
Troba els sis errors d'aquesta classe, indica per a cadascun el símptoma que produirà i en quin moment apareixerà, i reescriu-la.
@Service
public class IncidenciaService {
@Autowired private IncidenciaRepositori repositori;
private final Map<Long, Incidencia> ultimesVistes = new HashMap<>();
@Transactional
public void processarPendents() {
for (Incidencia i : repositori.findAll()) {
tancarSiEscau(i);
}
}
@Async
@Transactional
private void tancarSiEscau(Incidencia incidencia) {
try {
incidencia.setEstat(EstatIncidencia.TANCADA);
ultimesVistes.put(incidencia.getId(), incidencia);
notificador.avisarTaller(incidencia.getBicicleta().getMatricula());
} catch (Exception e) {
log.error("Error", e);
}
}
}Exercici 2: del símptoma a la causa
Per a cadascun d'aquests cinc informes de producció, formula la hipòtesi més probable, digues què comprovaries per confirmar-la i què no faries.
- «Des del desplegament de dimarts, el p99 de
POST /api/v1/lloguersva passar de 300 ms a 4 s. La mediana continua a 45 ms. CPU de l'aplicació al 20 %, CPU de la base de dades al 25 %. No hi haFull GC.» - «L'operari Bru diu que va marcar una bicicleta com a avariada i continua apareixent disponible. No hi ha cap error al log.»
- «Els ciutadans reben tres correus de resum mensual des que l'ajuntament va demanar més capacitat.»
- «
/api/v1/estacionstrigava 200 ms amb 20 estacions. Ara que n'hi ha 200, triga 2 s.» - «Vam canviar
ciclourbana.tarifes.preu-minuta 0,15 aapplication-prod.yml, vam desplegar, i les factures continuen calculant-se a 0,12.»
Exercici 3: la migració perillosa
L'equip desplegarà la versió 2.5.0 de CicloUrbana amb desplegament progressiu —les instàncies de la versió anterior conviuen uns minuts amb les noves— i inclou aquesta migració:
-- V10__ajustos_lloguers.sql
ALTER TABLE lloguers DROP COLUMN estacio_origen_id;
ALTER TABLE lloguers ADD COLUMN estacio_inici_id BIGINT NOT NULL;
ALTER TABLE lloguers ADD CONSTRAINT fk_lloguer_estacio_inici
FOREIGN KEY (estacio_inici_id) REFERENCES estacions(id);
CREATE INDEX idx_lloguers_estacio_inici ON lloguers (estacio_inici_id);Enumera tots els problemes que provocarà i reescriu el pla de migració complet.
Solucions
Solució 1
Error 1 — @Autowired en camp. No permet final, obliga a reflexió per provar la classe i amaga el creixement de dependències. Apareix: en escriure la primera prova unitària.
Error 2 — HashMap mutable en un singleton. Estat compartit entre fils sense sincronització: HashMap es pot corrompre en escriptures concurrents —arribant a bucles infinits a l'estructura interna— i a més creix sense límit, així que és també una fuita de memòria. Apareix: sota càrrega, de manera irreproduïble.
Error 3 — autoinvocació. tancarSiEscau(i) es crida amb this: ni @Async ni @Transactional tenen efecte. Tot s'executa síncron dins de la transacció externa. Apareix: mai com a error; simplement l'asincronia no existeix.
Error 4 — anotacions sobre un mètode private. Encara que es corregís l'autoinvocació, un mètode privat no és interceptable. Apareix: mai; s'ignora en silenci.
Error 5 — excepció capturada i no rellançada. Si alguna cosa falla, es registra i el bucle continua; la transacció externa confirma i es donen per tancades incidències que no ho estan. I si la fallada va marcar la transacció per a rollback, el commit final dona UnexpectedRollbackException. Apareix: el dia de la primera fallada parcial.
Error 6 — crida externa dins de la transacció. notificador.avisarTaller(...) reté una connexió del pool durant tota la crida de xarxa. Amb findAll() sobre milers d'incidències, la transacció pot durar minuts amb una connexió bloquejada. Apareix: quan creix el volum, com a espera de connexions i p99 alt.
I un setè problema de disseny que l'enunciat no numera: findAll() sense filtre ni paginació per descartar la majoria en memòria.
@Service
public class IncidenciaService { // constructor omès: repositori i tancador, final
/** Sense @Transactional: només orquestra. Cada incidència va en la seva transacció. */
public int processarPendents() {
List<Long> ids = repositori.cercarIdsPendentsDeTancament(); // filtra a la BD
int tancades = 0;
for (Long id : ids) {
try {
tancador.tancar(id); // altre bean: la crida travessa el proxy
tancades++;
} catch (DataAccessException e) {
log.error("No s'ha pogut tancar la incidència {}", id, e);
}
}
return tancades;
}
}
@Service
public class TancadorIncidencies {
@Transactional(timeout = 5) // públic, en un altre bean: sí que s'intercepta
public void tancar(Long idIncidencia) {
Incidencia incidencia = repositori.findById(idIncidencia)
.orElseThrow(() -> new RecursNoTrobatException("Incidència", idIncidencia));
incidencia.setEstat(EstatIncidencia.TANCADA); // dirty checking, sense save()
esdeveniments.publishEvent(new IncidenciaTancada(idIncidencia,
incidencia.getBicicleta().getMatricula()));
}
}
@Component
public class NotificadorIncidencies {
/** Fora de la transacció i fora del fil que orquestra. */
@Async("executorCorreu")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void alTancarse(IncidenciaTancada esdeveniment) {
notificador.avisarTaller(esdeveniment.matricula());
}
}Cal notar que la correcció de l'error 3 arregla també el 6: en sortir la notificació a l'AFTER_COMMIT, la connexió ja està retornada al pool quan comença la crida de xarxa. I l'estat mutable de l'error 2 simplement desapareix: no calia.
Solució 2
1. Espera de connexió del pool. Els senyals encaixen un a un: mediana intacta (el camí ràpid no va canviar), p99 disparat amb CPU baixa a banda i banda (no és saturació de càlcul), sense Full GC (no són pauses de memòria). Comprovaria: hikaricp.connections.pending i hikaricp.connections.usage; /actuator/threaddump durant un pic buscant fils que ja tenen connexió i estan bloquejats en un socket; i leak-detection-threshold: 20000, la traça del qual assenyala el mètode culpable. La causa més probable és una crida HTTP dins d'una transacció introduïda dimarts. El que no faria: pujar maximum-pool-size ni els fils de Tomcat; amb la base de dades al 25 % no falta capacitat, i més connexions retingudes per crides remotes només allarguen el problema.
2. El parany del proxy sobre @Transactional. «Sense cap error al log» és la signatura. Comprovaria: si marcarAvariada s'invoca des d'un altre mètode de la mateixa classe, si és public, i si logging.level.org.springframework.transaction: DEBUG imprimeix Creating new transaction en aquesta crida. Segona hipòtesi, si la transacció sí que existeix: falta el save() i l'entitat no està gestionada perquè es va construir fora del context. El que no faria: afegir save() a cegues; si el problema és el proxy, el save() tampoc no s'executarà en transacció.
3. Tasca programada multiplicada en escalar. «Des que l'ajuntament va demanar més capacitat» vol dir més rèpliques, i cadascuna té el seu propi planificador. Comprovaria: el nombre de rèpliques i si la tasca porta @SchedulerLock. El que no faria: moure la tasca a un perfil actiu en una sola instància com a solució definitiva —crea un punt únic de fallada—; ShedLock és la resposta, i a més convé revisar si la tasca és idempotent.
4. N+1. La latència creix proporcionalment al nombre de resultats: és la definició. Comprovaria: generate_statistics en preproducció per veure el recompte de sentències, o directament la cascada de spans buscant select repetits. El que no faria: afegir una memòria cau. Amagaria el problema el 95 % del temps i deixaria el 5 % restant pitjor que abans.
5. Propietat que no s'enllaça. Tres hipòtesis per ordre: la clau està mal escrita; una variable d'entorn CICLOURBANA_TARIFES_PREU_MINUT guanya al fitxer per precedència; o el perfil prod no es va activar i el fitxer no es llegeix. Comprovaria: /actuator/configprops per al valor efectiu de TarifesProperties i /actuator/env per saber de quina font surt. Tots dos responen en trenta segons. El que no faria: recompilar «per si de cas» ni canviar el valor a application.yml sense saber quina de les tres causes és.
Solució 3
Els problemes, per ordre de gravetat.
1. Es perden les dades i són irrecuperables. DROP COLUMN estacio_origen_id esborra l'estació d'origen de tots els lloguers històrics. No hi ha cap UPDATE que els ompli després: es perd la dada d'on va començar cada lloguer de la xarxa.
2. ADD COLUMN ... NOT NULL sense valor per defecte falla. Si la taula té files, PostgreSQL no pot omplir la columna nova i la migració avorta a mitges. I encara que no fallés, no hi hauria d'on treure el valor: ja es va esborrar a la línia anterior.
3. Trenca les instàncies de la versió anterior. Durant el desplegament progressiu, les instàncies 2.4.0 continuen executant SELECT ... estacio_origen_id .... Tan bon punt s'aplica la migració, totes elles comencen a fallar amb column does not exist, i com que la migració s'executa abans d'arrencar les noves, el servei queda caigut per a tots els ciutadans de Ribalta.
4. CREATE INDEX sense CONCURRENTLY bloqueja les escriptures de la taula lloguers durant tota la construcció de l'índex. Amb milions de files, són minuts sense poder iniciar ni finalitzar lloguers.
5. És un canvi de nom disfressat. El canvi real és estacio_origen_id → estacio_inici_id, i reanomenar una columna mai no és compatible cap enrere. A més, el benefici és cosmètic: convé preguntar-se si mereix el risc.
El pla correcte: expand/contract en tres desplegaments.
-- V10__expand_estacio_inici.sql (desplegament 1, amb la versió 2.5.0)
ALTER TABLE lloguers ADD COLUMN estacio_inici_id BIGINT; -- admet nuls
UPDATE lloguers SET estacio_inici_id = estacio_origen_id; -- per lots si és gran
ALTER TABLE lloguers ADD CONSTRAINT fk_lloguer_estacio_inici
FOREIGN KEY (estacio_inici_id) REFERENCES estacions(id);
CREATE INDEX CONCURRENTLY idx_lloguers_estacio_inici ON lloguers (estacio_inici_id);
-- V11__endurir_estacio_inici.sql (desplegament 2, amb la versió 2.6.0)
UPDATE lloguers SET estacio_inici_id = estacio_origen_id WHERE estacio_inici_id IS NULL;
ALTER TABLE lloguers ALTER COLUMN estacio_inici_id SET NOT NULL;
-- V12__contract_eliminar_estacio_origen.sql (desplegament 3, amb la versió 2.7.0)
DROP INDEX IF EXISTS idx_lloguers_estacio_origen;
ALTER TABLE lloguers DROP COLUMN estacio_origen_id;A 2.5.0 l'entitat mapeja la columna nova i escriu a totes dues, perquè les instàncies 2.4.0 que encara viuen continuïn veient dades correctes; la columna nova admet nuls perquè durant uns minuts hi ha instàncies antigues inserint files sense omplir-la. A 2.6.0 l'entitat deixa d'escriure a la vella, i només llavors —a 2.7.0— s'esborra.
Les quatre regles que resumeix l'exercici. Una migració ha de funcionar amb la versió que corre ara i amb la que es desplegarà. Afegir és compatible; reanomenar i esborrar no ho són mai. Un NOT NULL s'assoleix en tres passos, mai en un. I CONCURRENTLY és obligatori en qualsevol índex sobre una taula en producció.
I una comprovació pràctica que costa poc i detecta gairebé tot això: executar la migració contra una còpia recent de producció amb la versió anterior de l'aplicació corrent a sobre. Si l'aplicació antiga continua funcionant després de migrar, el desplegament progressiu és segur.
Conclusió
Tens ara el revers del catàleg anterior: els errors que les pràctiques de 10-01 prevenen, cadascun amb el seu símptoma real, la seva causa, com es diagnostica i el codi corregit. I amb una classificació que orienta l'atenció: els sorollosos costen una tarda, els silenciosos costen una reconciliació de dades i els diferits apareixen justament quan pitjor va.
Saps reconèixer les fallades d'arrencada —la classe principal fora del paquet arrel, el bean que no existeix, el cicle amb el seu requadre, els dos candidats sense desempat— i fer servir les tres eines que Spring Boot ja et dona per diagnosticar-les: l'informe de --debug, /actuator/env i /actuator/configprops. Tens per fi el parany del proxy tractat en un sol lloc, amb el seu mecanisme —només les crides que entren des de fora travessen el proxy—, les seves quatre cares (@Transactional, @Async, @Cacheable i la més perillosa, @PreAuthorize), els seus disparadors de visibilitat i final, i les seves tres solucions, amb la d'extreure a un altre bean com la que a més millora el disseny.
Reconeixes el perfil que no es va activar mai i la seva prova literal al log, la propietat mal escrita que s'ignora sense protestar, les llistes que se substitueixen en lloc de fusionar-se i el secret versionat que no s'arregla esborrant-lo sinó rotant-lo. A JPA tens la LazyInitializationException amb la seva única solució correcta, l'N+1 i el seu EAGER amagat a @ManyToOne, l'equals que trenca els HashSet, les cascades que esborren de més, la transacció que confirma perquè algú va capturar l'excepció i l'OFFSET gran. A REST, les tres conseqüències d'exposar entitats, el mass assignment, el 200 que fa invisibles els errors per a la monitorització i el Page que converteix l'estructura interna d'una llibreria en contracte públic. A seguretat, l'ordre de les regles, el CSRF desactivat sense raó escrita, el JWT que no està xifrat i l'error que regala informació interna. A concurrència, l'estat mutable compartit, la tasca que es multiplica en escalar i el context que no viatja al fil asíncron. A rendiment, optimitzar sense mesurar, la memòria cau que amaga una consulta mal escrita i el pool desproporcionat. I a proves, el con de gelat, les cinc causes d'intermitència i la cobertura que puja sense verificar res. Tanca el catàleg la taula de diagnòstic ràpid: vint-i-un símptomes amb el lloc per on començar i la lliçó on és la resposta completa.
Queda una tercera dimensió de la pregunta «està ben feta?». Hem vist què convé fer i què convé evitar, però cap de les dues coses no diu res sobre com es llegeix el codi. Un servei pot complir les trenta-vuit comprovacions i no tenir ni un sol error del catàleg, i tot i així ser un mètode de dues-centes línies amb quatre banderes booleanes, noms que no diuen res, comentaris que repeteixen el codi i un model de domini que és una bossa de setters. Això no trenca res avui: trenca la capacitat de l'equip de canviar-ho demà. La lliçó següent, Consells per Escriure Codi Net, s'ocupa d'aquesta dimensió: noms que revelen intenció, funcions amb un sol nivell d'abstracció, SOLID amb exemples reals de Ribalta, comentaris que expliquen el perquè, immutabilitat i Optional ben fets servir, el model de domini anèmic enfront de Lloguer.finalitzar(...), les regles de dependència verificades automàticament amb ArchUnit, l'estil que no es discuteix perquè l'aplica una eina, i com es reconeix, es registra i es paga el deute tècnic.
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
