La lliçó anterior va acabar amb una pregunta incòmoda: què és exactament mvn test?
L'has escrit cinc vegades. Has posat <scope>test</scope> en una dependència sense saber què significa. Has vist aparèixer spring-boot-starter-parent, maven-surefire-plugin i spring-boot-maven-plugin en un pom.xml i els has donat per bons. Has executat mvn spring-boot:run i mvn dependency:tree. I a 11-01 es va parlar de coordenades groupId:artifactId:version com a preparació d'aquesta lliçó.
Aquesta lliçó explica l'eina que ha estat sostenint les tres anteriors.
Maven és l'eina de construcció estàndard de l'ecosistema Java. La seva feina és convertir el teu codi font i un fitxer de configuració en un artefacte executable, resolent pel camí les dependències, executant les proves i garantint que el procés dóna el mateix resultat a qualsevol màquina.
Aquest últim punt és el que més se subestima. Fins al mòdul 10, BiblioTech es compilava amb javac i un classpath escrit a mà. Amb els quaranta jars que ha portat Spring Boot, això ja no és tediós: és impossible. I hi ha una raó de fons que va més enllà de la comoditat, i que enllaça amb Log4Shell (11-01): la capacitat de respondre a una vulnerabilitat depèn de poder canviar una versió amb una línia i verificar amb una ordre que res no s'ha trencat. La segona meitat la vas aconseguir a 11-04 amb JUnit. Aquí arriba la primera.
En acabar entendràs quin problema resol una eina de construcció; sabràs llegir i escriure un pom.xml complet; dominaràs les dependències, els seus àmbits, la transitivitat i la resolució de conflictes; coneixeràs el cicle de vida i les seves fases; sabràs què és un plugin i què fan els que fas servir; manejaràs perfils i projectes multimòdul; i tindràs criteri per triar entre Maven i Gradle.
I BiblioTech serà un projecte Maven complet i reproduïble.
Contingut
- El problema:
javaca mà - Què resol una eina de construcció
- Convenció sobre configuració
- L'estructura estàndard de directoris
- Instal·lació i comprovació
- El POM: coordenades
SNAPSHOTenfront de versió alliberadaproperties: variables del POM- El
pom.xmlcomplet de BiblioTech - Dependències: declaració i repositoris
- Àmbits de dependència
- Dependències transitives
- Resolució de conflictes: la definició més propera
mvn dependency:treea la pràcticaexclusionsdependencyManagementenfront dedependencies- El BOM i el
spring-boot-starter-parent - El cicle de vida: tres cadenes
- Les fases de la cadena per defecte
- Plugins i goals
- Els plugins que fa servir BiblioTech
surefireenfront defailsafe- Empaquetar:
maven-jar-plugin,shadeispring-boot-maven-plugin - Perfils
- Projectes multimòdul
settings.xmli les credencials- El Maven Wrapper
- Ordres del dia a dia
- Reproductibilitat i fixació de versions
- Maven enfront de Gradle
- BiblioTech com a projecte Maven complet
- Errors comuns i consells
- Exercicis
- El problema:
javac a mà
javac a màTornem al mòdul 1. Així es compilava i s'executava BiblioTech:
javac -d classes src/com/nexussoftware/bibliotech/*.java
java -cp classes com.nexussoftware.bibliotech.MainFunciona amb deu fitxers i zero dependències. Ara mira el que caldria avui:
javac -d target/classes \
-cp "lib/spring-core-6.1.11.jar:lib/spring-context-6.1.11.jar:lib/spring-beans-6.1.11.jar:lib/spring-aop-6.1.11.jar:lib/spring-boot-3.3.2.jar:lib/spring-boot-autoconfigure-3.3.2.jar:lib/hibernate-core-6.5.2.jar:lib/jakarta.persistence-api-3.1.0.jar:lib/h2-2.2.224.jar:lib/jackson-databind-2.17.2.jar:lib/slf4j-api-2.0.13.jar:lib/logback-classic-1.5.6.jar:..." \
$(find src/main/java -name "*.java")I això només compila. Després caldria:
- Copiar els recursos de
src/main/resourcesatarget/classes. - Compilar les proves amb un altre classpath (el de producció més JUnit, AssertJ i Mockito).
- Executar les proves invocant el llançador de JUnit a mà.
- Empaquetar en un jar amb un
MANIFEST.MFcorrecte. - I abans de tot això, descarregar els quaranta jars a mà, amb les seves versions exactes i compatibles entre si.
Aquest punt 5 és l'assassí. Descarregar spring-boot-starter-data-jpa significa esbrinar que necessita Hibernate, que Hibernate necessita jakarta.persistence-api, ByteBuddy, Jandex, Classmate i Antlr, que ByteBuddy necessita... i fer-ho amb versions que no xoquin. A mà, és una feina de dies que cal refer a cada actualització.
S'anomena l'infern de les dependències (dependency hell), i és el problema principal que Maven resol.
- Què resol una eina de construcció
| Problema | Sense eina | Amb Maven |
|---|---|---|
| Compilar | javac amb classpath a mà |
mvn compile |
| Dependències | Descarregar jars a mà | Declarar-les al POM |
| Dependències transitives | Esbrinar-les i descarregar-les una a una | Automàtic |
| Conflictes de versions | Prova i error | Regla determinista |
| Executar proves | Invocar el llançador a mà | mvn test, automàtic |
| Empaquetar | jar cvfm amb manifest a mà |
mvn package |
| Recursos | cp a mà |
Automàtic |
| Reproductibilitat | Depèn de la màquina | Garantida |
| Integració amb IDE | Configurar cadascun | L'IDE llegeix el POM |
| Integració contínua | Script a mida | mvn verify |
I una conseqüència cultural que importa: qualsevol projecte Maven es compila igual. Et donen un repositori desconegut, veus un pom.xml, escrius mvn package i funciona. Sense llegir documentació, sense preguntar a ningú.
- Convenció sobre configuració
Aquest és el principi de disseny de Maven, i explica per què la seva configuració és tan curta.
Si segueixes les convencions, no has de configurar res. Només configures el que se n'aparta.
Maven assumeix que:
- El codi font és a
src/main/java. - Els recursos són a
src/main/resources. - Les proves són a
src/test/java. - Els recursos de prova són a
src/test/resources. - Tot el que es genera va a
target/. - Tot fitxer
*Test.javaés una prova.
Per això a 11-04 vas posar les proves a src/test/java i mvn test les va trobar sense que configuressis res. No era màgia: era la convenció.
L'alternativa —configuració explícita, com Ant— exigeix declarar cada camí, cada tasca i cada dependència entre tasques. El resultat són fitxers de construcció de tres-centes línies, diferents a cada projecte i que cal llegir abans d'entendre res.
El compromís és real: si el teu projecte no encaixa en les convencions, Maven es torna incòmode. És el preu de la uniformitat, i per a la immensa majoria dels projectes Java compensa.
- L'estructura estàndard de directoris
bibliotech/
├── pom.xml <- EL fitxer de configuracio
├── mvnw, mvnw.cmd <- Maven Wrapper (apartat 27)
├── .mvn/wrapper/ <- configuracio del wrapper
├── src/
│ ├── main/
│ │ ├── java/ <- codi de produccio
│ │ │ └── com/nexussoftware/bibliotech/
│ │ └── resources/ <- application.yml, data.sql, logback.xml
│ └── test/
│ ├── java/ <- proves
│ │ └── com/nexussoftware/bibliotech/
│ └── resources/ <- application-test.yml, CSV de prova
└── target/ <- TOT el que es genera (mai a git)
├── classes/ <- .class de produccio + recursos
├── test-classes/ <- .class de proves
├── surefire-reports/ <- informes de proves (11-04)
├── generated-sources/ <- codi generat (Lombok, MapStruct)
└── bibliotech-1.0.0-SNAPSHOT.jargraph TD
A["src/main/java"] -->|compile| B["target/classes"]
R["src/main/resources"] -->|process-resources| B
C["src/test/java"] -->|test-compile| D["target/test-classes"]
TR["src/test/resources"] -->|process-test-resources| D
B --> D
D -->|test: surefire| E["target/surefire-reports"]
B -->|package| F["target/bibliotech-1.0.0-SNAPSHOT.jar"]
F -->|install| G["~/.m2/repository"]
Dues regles d'or:
target/mai no es puja a git. És tot regenerable. El teu.gitignoreha de tenirtarget/.src/mai no es toca a mà des de la construcció. És la font, i és l'única cosa que hi ha al repositori juntament amb el POM.
- Instal·lació i comprovació
Apache Maven 3.9.8
Maven home: /opt/maven
Java version: 17.0.11, vendor: Eclipse Adoptium
Default locale: ca_ES, platform encoding: UTF-8
OS name: "linux", version: "6.12.0", arch: "amd64"Comprova tres coses: la versió de Maven (3.9.x és l'actual), la de Java (17 o superior per a aquest curs) i la codificació de la plataforma, que ha de ser UTF-8 per evitar problemes amb accents.
Si no el tens instal·lat, l'apartat 27 et donarà una raó molt bona per no necessitar-lo.
Crear un projecte des de zero:
mvn archetype:generate \
-DgroupId=com.nexussoftware \
-DartifactId=bibliotech \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.4 \
-DinteractiveMode=falseA la pràctica, per a un projecte Spring Boot es fa servir Spring Initializr (start.spring.io), que genera el POM, el wrapper i la classe principal ja configurats.
- El POM: coordenades
El POM (Project Object Model) és el pom.xml: la descripció completa del projecte.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <!-- sempre 4.0.0 -->
<!-- LES COORDENADES: identifiquen l'artefacte de forma unica -->
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>BiblioTech</name>
<description>Gestio de la biblioteca tecnica interna de Nexus Software</description>
</project>| Coordenada | Què és | Convenció |
|---|---|---|
groupId |
L'organització | Domini invertit: com.nexussoftware |
artifactId |
El mòdul | Minúscules amb guions: bibliotech-core |
version |
La versió | SemVer (11-01): 1.0.0 |
packaging |
Què es produeix | jar (per defecte), war, pom |
Els tres primers formen les coordenades groupId:artifactId:version, que ja vas veure a 11-01. Determinen el camí al repositori:
Valors de packaging:
| Valor | Produeix | Ús |
|---|---|---|
jar |
Un .jar |
Llibreries i aplicacions (el normal) |
war |
Un .war |
Aplicacions web per a servidor extern (poc usat avui) |
pom |
Només el POM | Projecte pare o agregador (apartat 25) |
SNAPSHOT enfront de versió alliberada
SNAPSHOT enfront de versió alliberadaLa versió té dues naturaleses molt diferents:
| Tipus | Exemple | Significat | Comportament |
|---|---|---|---|
| SNAPSHOT | 1.0.0-SNAPSHOT |
En desenvolupament | Mutable: Maven la torna a descarregar periòdicament |
| Alliberada | 1.0.0 |
Publicada | Immutable: es descarrega una vegada i es desa en memòria cau per sempre |
La conseqüència pràctica: si depens de bibliotech-core:1.0.0-SNAPSHOT, Maven comprova diàriament si hi ha una versió més nova. Si depens d'1.0.0, la descarrega una vegada i no torna a mirar.
Regla professional: una versió alliberada mai no se sobreescriu. Si 1.0.0 té una fallada, es publica 1.0.1. Republicar 1.0.0 amb contingut diferent trenca la reproductibilitat de tothom que ja la tenia a la memòria cau, i és una de les pitjors coses que es poden fer en un repositori compartit.
I el seu corol·lari: mai no depenguis d'un SNAPSHOT en producció. Compila avui i demà amb contingut diferent.
Forçar l'actualització de snapshots:
properties: variables del POM
properties: variables del POM<properties>
<java.version>17</java.version>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- versions centralitzades, per no repetir-les -->
<assertj.version>3.25.3</assertj.version>
<mockito.version>5.12.0</mockito.version>
</properties>Es fan servir amb ${nom}:
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
</dependency>Avantatge: quan quinze dependències comparteixen versió (tot l'ecosistema de Jackson, per exemple), es canvia en un sol lloc.
project.build.sourceEncoding és important de veritat: sense ella, Maven fa servir la codificació de la plataforma, i compilar en una màquina amb Windows en català i en un servidor Linux amb UTF-8 pot produir resultats diferents. Maven avisa amb un WARNING si falta.
També existeixen propietats predefinides útils: ${project.version}, ${project.artifactId}, ${project.basedir}, ${maven.build.timestamp}.
- El
pom.xml complet de BiblioTech
pom.xml complet de BiblioTechAquest és el POM complet i comentat, amb tot el que el projecte necessita després de quatre lliçons:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- ================= PARE ================= -->
<!-- Aporta el BOM (versions compatibles de TOT) i configuracio de plugins -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- buit: busca'l al repositori, no al disc -->
</parent>
<!-- ================= IDENTITAT ================= -->
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>BiblioTech</name>
<description>Gestio de la biblioteca tecnica interna de Nexus Software</description>
<!-- ================= PROPIETATS ================= -->
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- ================= DEPENDENCIES ================= -->
<dependencies>
<!-- Nucli de Spring: contenidor IoC, autoconfiguracio, logging (11-02) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Validacio: @NotNull, @Min en @ConfigurationProperties (11-02) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- AOP: @Transactional i aspectes propis (11-02) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- JPA + Hibernate + Spring Data + transaccions (11-03) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2: base de dades en memoria. runtime: no cal en compilar -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Metadades de @ConfigurationProperties: autocompletat a l'IDE -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Proves: JUnit 5, AssertJ, Mockito, spring-test (11-04, 11-06) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- ================= CONSTRUCCIO ================= -->
<build>
<plugins>
<!-- Empaqueta el jar executable i dona el goal spring-boot:run (11-02) -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- Proves UNITARIES: *Test.java -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludedGroups>integracio</excludedGroups> <!-- @Tag d'11-04 -->
</configuration>
</plugin>
<!-- Proves d'INTEGRACIO: *IT.java, a la fase verify -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<!-- ================= PERFILS ================= -->
<profiles>
<profile>
<id>rapid</id> <!-- mvn package -Prapid : sense proves lentes -->
<properties>
<skipITs>true</skipITs>
</properties>
</profile>
</profiles>
</project>Detall important: cap dependència de Spring no porta <version>. Les gestiona el pare. Hi tornarem a l'apartat 17.
- Dependències: declaració i repositoris
Una dependència es declara amb les seves coordenades:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
<scope>compile</scope> <!-- per defecte; es pot ometre -->
</dependency>D'on surt el jar? Maven busca en tres llocs, en aquest ordre:
graph LR
A["mvn compile"] --> B{"Es a<br/>~/.m2/repository?"}
B -->|Si| E["Es fa servir"]
B -->|No| C{"Hi ha repositori<br/>corporatiu (Nexus,<br/>Artifactory)?"}
C -->|Si| D["Es descarrega d'ell"]
C -->|No| F["Es descarrega de<br/>Maven Central"]
D --> G["Es desa a ~/.m2"]
F --> G
G --> E
| Repositori | Què és | Quan |
|---|---|---|
Local (~/.m2/repository) |
Memòria cau al teu disc | Sempre es mira primer |
| Central | El repositori públic (11-01) | Per defecte |
| Corporatiu | Nexus, Artifactory: mirall i artefactes privats | A l'empresa |
Afegir un repositori addicional (evita-ho si pots):
<repositories>
<repository>
<id>nexus-intern</id>
<url>https://nexus.nexussoftware.com/repository/maven-public/</url>
</repository>
</repositories>Consell professional: com menys repositoris, millor. Cadascun afegeix una font d'artefactes en què cal confiar. L'habitual a l'empresa és configurar un sol mirall corporatiu a settings.xml (apartat 26) que al seu torn replica Central: així hi ha un únic punt de control, cacheat i auditable.
- Àmbits de dependència
L'àmbit (scope) determina en quin classpath està disponible una dependència i si s'empaqueta.
| Àmbit | Compilar | Provar | Executar | És transitiu? | Exemple |
|---|---|---|---|---|---|
compile (per defecte) |
Sí | Sí | Sí | Sí | Spring, Jackson |
provided |
Sí | Sí | No | No | API de Servlet en un .war; Lombok |
runtime |
No | Sí | Sí | Sí | Driver d'H2, Logback |
test |
No | Sí | No | No | JUnit, AssertJ, Mockito |
system |
Sí | Sí | No | No | Obsolet, no el facis servir |
import |
— | — | — | — | Només a dependencyManagement: importa un BOM |
Els dos que ja has fet servir sense saber-ho:
runtime per a H2. El teu codi mai no escriu import org.h2.Driver. Només el necessita la JVM en execució, quan Hibernate carrega el driver pel seu nom. Posar-lo a runtime significa que no el pots fer servir per accident al teu codi: si algú escriu una classe que importa alguna cosa d'H2, no compila. Això és una barrera arquitectònica gratis.
test per a JUnit i AssertJ. Estan disponibles en compilar i executar proves, i no s'empaqueten al jar final. És el que evita que la teva aplicació en producció porti dins un framework de proves — amb el seu pes i la seva superfície d'atac.
provided mereix una nota: significa "en temps d'execució això ja hi serà, no ho empaquetis". El seu cas clàssic era l'API de Servlet en un .war, que la proporcionava el servidor d'aplicacions. Avui, amb servidors incrustats, es fa servir sobretot per a Lombok (11-07), que només actua en compilació.
Veure el classpath resultant:
- Dependències transitives
Aquí hi ha la característica que fa que Maven valgui la pena.
Quan declares una dependència, Maven descarrega també les seves, i les d'aquelles, recursivament.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>Aquesta única declaració porta, entre altres:
spring-boot-starter-data-jpa
├── spring-boot-starter-aop
│ ├── spring-aop
│ └── aspectjweaver
├── spring-boot-starter-jdbc
│ ├── HikariCP <- pool de connexions
│ └── spring-jdbc
├── jakarta.persistence-api <- l'especificacio (11-03)
├── jakarta.transaction-api
├── hibernate-core <- la implementacio (11-03)
│ ├── byte-buddy <- genera els proxies de la carrega mandrosa
│ ├── jandex
│ ├── classmate
│ └── antlr4-runtime <- analitza el JPQL
├── spring-data-jpa
│ ├── spring-orm
│ └── spring-tx
└── spring-aspectsGairebé trenta artefactes que tu no has anomenat. Sense Maven, hauries d'esbrinar cadascun, trobar la versió compatible i descarregar-lo a mà.
I aquí es veu una cosa que 11-03 va donar per suposada: ByteBuddy és al teu projecte perquè Hibernate el fa servir per generar les subclasses proxy de la càrrega mandrosa. És el mecanisme del mòdul 10, amb nom i coordenades.
Regla de la transitivitat, important: les dependències test i provided no es propaguen. Si BiblioTech depengués d'una llibreria que fa servir JUnit a les seves proves, tu no heretaries JUnit. És el correcte: les dependències de prova d'un altre no són assumpte teu.
- Resolució de conflictes: la definició més propera
Amb trenta dependències transitives, és inevitable que dues demanin versions diferents del mateix:
el-teu-projecte
├── llibreria-a -> jackson-databind:2.15.0
└── llibreria-b -> jackson-databind:2.17.2Només hi pot haver una versió d'una classe al classpath. Quina guanya?
La regla de la definició més propera (nearest definition wins): guanya la versió que és a menys salts del teu POM. Si empaten, guanya la declarada primer.
el-teu-projecte (profunditat 0)
├── llibreria-a (1)
│ └── jackson-databind:2.15.0 (2)
├── jackson-databind:2.17.2 (1) <- GUANYA: es mes a prop
└── llibreria-b (1)
└── jackson-databind:2.16.0 (2)Conseqüència pràctica molt útil: declarar explícitament una dependència al teu POM et dóna control absolut sobre la seva versió, perquè estarà sempre a profunditat 1 i guanyarà a qualsevol transitiva.
La regla és determinista, però pot sorprendre: no guanya "la més nova", guanya "la més propera". Si una transitiva propera demana una versió antiga, aquesta antiga guanya, i pots acabar amb NoSuchMethodError en execució — el símptoma clàssic d'un conflicte de versions.
Diagnòstic:
[INFO] +- com.exemple:llibreria-a:jar:1.2.0:compile
[INFO] | \- (com.fasterxml.jackson.core:jackson-databind:jar:2.15.0:compile
- omitted for conflict with 2.17.2)Aquest omitted for conflict with et diu exactament quina versió es va descartar i per quina.
mvn dependency:tree a la pràctica
mvn dependency:tree a la pràcticaÉs l'eina de diagnòstic que més vegades et traurà d'un compromís. Quatre usos concrets:
1. Veure tot l'arbre:
2. Buscar qui arrossega un artefacte (la pregunta de seguretat d'11-01):
[INFO] com.nexussoftware:bibliotech:jar:1.0.0-SNAPSHOT
[INFO] \- com.exemple:llibreria-legacy:jar:2.1.0:compile
[INFO] \- org.apache.logging.log4j:log4j-core:jar:2.14.1:compileAquí tens la resposta a "tinc Log4j i qui l'ha ficat?", que el desembre del 2021 va costar setmanes a mitja indústria.
3. Veure conflictes resolts:
4. Detectar dependències declarades i no usades, o usades i no declarades:
[WARNING] Used undeclared dependencies found:
[WARNING] org.slf4j:slf4j-api:jar:2.0.13:compile
[WARNING] Unused declared dependencies found:
[WARNING] com.exemple:utilitats:jar:1.0.0:compileAquest primer avís assenyala un risc real: estàs fent servir SLF4J al teu codi però l'obtens de forma transitiva. Si demà Spring Boot deixa de portar-lo, el teu codi deixa de compilar sense que hagis tocat res. Tot el que facis servir directament, declara-ho directament.
exclusions
exclusionsDe vegades cal treure alguna cosa que arriba transitivament. Casos reals:
<!-- Cas 1: substituir Logback per Log4j2 a Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency><!-- Cas 2: treure Tomcat per fer servir Jetty o Undertow -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency><!-- Cas 3: treure una implementacio de logging duplicada que causa conflicte -->
<dependency>
<groupId>com.exemple</groupId>
<artifactId>llibreria-legacy</artifactId>
<version>2.1.0</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>Aquest tercer cas és important i el veuràs a 11-07: dues implementacions de logging al mateix classpath es barallen i el resultat és que no es registra res, o es registra dues vegades.
Per excloure tot l'arbre d'una dependència de cop:
Fes-ho servir amb cura: és fàcil treure alguna cosa que sí que calia i descobrir-ho en execució.
dependencyManagement enfront de dependencies
dependencyManagement enfront de dependenciesDues seccions que es confonen constantment.
| Secció | Què fa |
|---|---|
<dependencies> |
Afegeix la dependència al projecte |
<dependencyManagement> |
Només declara la versió que es farà servir si algú l'afegeix |
<dependencyManagement>
<dependencies>
<!-- NO afegeix Jackson. Nomes diu: "si es fa servir, sera la 2.17.2" -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- AIXO si que l'afegeix. Sense version: la pren de dependencyManagement -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>Per a què serveix, tres usos:
- Centralitzar versions en un projecte multimòdul: el pare les declara, els mòduls les fan servir sense versió.
- Forçar la versió d'una transitiva sense declarar-la com a dependència directa. Molt útil per apedaçar una vulnerabilitat: si una transitiva té un CVE, aquí imposes la versió corregida.
- Importar un BOM, que és el de l'apartat següent.
El punt 2 mereix un exemple, perquè és l'eina de resposta ràpida davant de vulnerabilitats:
<dependencyManagement>
<dependencies>
<!-- Una transitiva porta la 2.14.1, vulnerable. Forcem la corregida -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.23.1</version>
</dependency>
</dependencies>
</dependencyManagement>dependencyManagement guanya sempre sobre la resolució transitiva, sense importar la profunditat. És la forma correcta d'apedaçar sense esperar que la llibreria intermèdia s'actualitzi.
- El BOM i el
spring-boot-starter-parent
spring-boot-starter-parentUn BOM (Bill of Materials) és un POM de tipus pom que només conté dependencyManagement: un catàleg de versions provades juntes.
Es fa servir de dues formes.
Forma 1: heretar del pare (el que fa BiblioTech):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
</parent>Això et dóna:
| Herència | Què aporta |
|---|---|
dependencyManagement |
Versions compatibles de més de 400 artefactes |
pluginManagement |
Versions i configuració dels plugins habituals |
| Propietats | java.version, codificació, etc. |
| Filtratge de recursos | application.yml amb @propietat@ substituïble |
| Configuració de plugins | spring-boot-maven-plugin ja enganxat a package |
Forma 2: importar el BOM (quan ja tens un altre pare, típic a l'empresa):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.2</version>
<type>pom</type>
<scope>import</scope> <!-- l'ambit import de l'apartat 11 -->
</dependency>
</dependencies>
</dependencyManagement>I ara s'entén del tot l'advertiment que apareixia a 11-02:
No posis
<version>a les dependències que gestiona el BOM.
Si escrius <version>2.15.0</version> a Jackson, estàs anul·lant la versió que l'equip de Spring Boot ha provat amb aquella versió de Spring, d'Hibernate i de la resta de l'ecosistema. Pots acabar amb incompatibilitats subtils: un NoSuchMethodError en execució, sis mesos després, en producció.
Si de veritat necessites una altra versió, la forma correcta és sobreescriure la propietat:
- El cicle de vida: tres cadenes
Maven té tres cicles de vida independents, cadascun amb les seves fases:
| Cicle | Per a què | Fases principals |
|---|---|---|
clean |
Netejar | pre-clean, clean, post-clean |
default |
Construir | validate ... deploy (la important) |
site |
Documentació | pre-site, site, post-site, site-deploy |
mvn clean # executa el cicle clean: esborra target/
mvn package # executa el cicle default fins a la fase package
mvn clean package # tots dos, en aquest ordremvn site genera un lloc web amb informes del projecte. Es fa servir poc avui; la majoria dels equips fan servir altres eines d'informes.
- Les fases de la cadena per defecte
I aquí hi ha la idea clau de Maven:
Executar una fase executa totes les fases anteriors.
Per això mvn test va compilar el teu codi a 11-04 sense que li ho demanessis: test ve després de compile.
graph TD
A["validate<br/>El projecte es correcte"] --> B["compile<br/>src/main/java -> target/classes"]
B --> C["test<br/>Executa *Test amb surefire"]
C --> D["package<br/>Empaqueta en jar/war"]
D --> E["verify<br/>Proves d'integracio (failsafe)<br/>i comprovacions de qualitat"]
E --> F["install<br/>Copia a ~/.m2/repository"]
F --> G["deploy<br/>Puja al repositori remot"]
La llista completa, amb les fases intermèdies que també existeixen:
| # | Fase | Què fa |
|---|---|---|
| 1 | validate |
Comprova que el POM és correcte |
| 2 | generate-sources |
Genera codi font |
| 3 | process-resources |
Copia src/main/resources a target/classes |
| 4 | compile |
Compila src/main/java |
| 5 | process-test-resources |
Copia src/test/resources |
| 6 | test-compile |
Compila src/test/java |
| 7 | test |
Executa les proves unitàries (surefire) |
| 8 | package |
Empaqueta en .jar |
| 9 | pre-integration-test |
Prepara l'entorn d'integració |
| 10 | integration-test |
Executa les proves d'integració (failsafe) |
| 11 | post-integration-test |
Neteja l'entorn |
| 12 | verify |
Comprova els resultats d'integració |
| 13 | install |
Instal·la l'artefacte a ~/.m2/repository |
| 14 | deploy |
Puja al repositori remot |
Quina fer servir a cada situació:
| Situació | Ordre |
|---|---|
| Desenvolupament, cicle ràpid | mvn test |
| Generar el jar | mvn package |
| Integració contínua | mvn verify |
| Publicar per a altres mòduls locals | mvn install |
| Publicar al repositori corporatiu | mvn deploy |
Un matís sobre install: molta gent escriu mvn clean install per costum per a tot. És més lent del necessari i omple ~/.m2 d'artefactes locals. Per verificar, mvn verify n'hi ha prou; install només cal quan un altre projecte local consumirà el teu artefacte.
- Plugins i goals
I aquí hi ha la segona idea clau:
Maven no fa res per si mateix. Tot ho fan els plugins.
Una fase no té codi: és un punt d'enganxament. El que passa a compile és que s'executa el goal compile del plugin maven-compiler-plugin.
| Concepte | Què és |
|---|---|
| Plugin | Un artefacte Maven amb tasques executables |
| Goal | Una tasca concreta: compiler:compile, surefire:test |
| Vinculació | L'associació entre un goal i una fase |
Els enllaços per defecte per a packaging=jar:
| Fase | Plugin:goal |
|---|---|
process-resources |
maven-resources-plugin:resources |
compile |
maven-compiler-plugin:compile |
test-compile |
maven-compiler-plugin:testCompile |
test |
maven-surefire-plugin:test |
package |
maven-jar-plugin:jar |
install |
maven-install-plugin:install |
deploy |
maven-deploy-plugin:deploy |
Un goal també es pot invocar directament, sense cicle de vida, amb la sintaxi plugin:goal:
mvn dependency:tree # plugin dependency, goal tree
mvn spring-boot:run # plugin spring-boot, goal run (11-02)
mvn versions:display-dependency-updates
mvn help:effective-pom # mostra el POM RESULTANT despres de l'herenciaAquest últim és una joia de diagnòstic: mvn help:effective-pom mostra el POM complet després d'aplicar l'herència del pare, amb totes les versions i configuracions resoltes. Quan no entenguis d'on surt una configuració, mira-hi.
- Els plugins que fa servir BiblioTech
maven-compiler-plugin
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>17</release> <!-- millor que source+target -->
<parameters>true</parameters> <!-- conserva noms de parametres -->
<compilerArgs>
<arg>-Xlint:all</arg>
<arg>-Werror</arg> <!-- avisos = errors. Estricte pero sa -->
</compilerArgs>
</configuration>
</plugin>Sobre <release>17</release> enfront de <source>/<target>: release és millor perquè, a més de fixar el nivell del llenguatge, verifica que només fas servir API existent a Java 17. Amb source/target pots compilar amb JDK 21 codi que fa servir un mètode de Java 21 i generar bytecode 17 que fallarà en execució amb NoSuchMethodError. release ho impedeix en compilació.
Sobre <parameters>true</parameters>: conserva els noms dels paràmetres al bytecode. Els necessiten Spring (per a @Value sense nom explícit) i Jackson (11-07) per mapejar constructors.
spring-boot-maven-plugin
Ja el vas fer servir a 11-02. Aporta:
| Goal | Què fa |
|---|---|
spring-boot:run |
Executa l'aplicació sense empaquetar |
spring-boot:repackage |
Converteix el jar normal en jar executable (enganxat a package) |
spring-boot:build-image |
Construeix una imatge de contenidor (12-06) |
maven-surefire-plugin i maven-failsafe-plugin
Tots dos executen proves. La seva diferència és l'apartat següent.
surefire enfront de failsafe
surefire enfront de failsafe| surefire | failsafe | |
|---|---|---|
| Tipus de prova | Unitàries | Integració |
| Fase | test |
integration-test + verify |
| Patró de noms | *Test.java, Test*.java, *Tests.java |
*IT.java, IT*.java, *ITCase.java |
| Si fallen | S'atura immediatament | Registra la fallada, executa post-integration-test i falla a verify |
| Velocitat | Mil·lisegons | Segons |
Aquesta diferència de comportament davant de la fallada no és un caprici. Una prova d'integració pot haver arrencat una base de dades o un contenidor a pre-integration-test. Si failsafe s'aturés en sec, aquests recursos quedarien penjats. Per això failsafe sempre deixa que s'executi la neteja i només falla en arribar a verify.
Conseqüència pràctica en què cau tothom:
mvn test # executa NOMES les unitaries. Les *IT NO s'executen.
mvn verify # executa les unitaries I les d'integracio.Si escrius PrestecRepositoryIT.java i executes mvn test, no s'executa i no hi ha cap avís. Sembla que tot va bé.
Aplicat a BiblioTech, amb les etiquetes d'11-04:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludedGroups>integracio,lenta</excludedGroups>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<groups>integracio</groups>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>Així el cicle ràpid de desenvolupament (mvn test) triga segons, i la integració contínua (mvn verify) ho executa tot.
- Empaquetar:
maven-jar-plugin, shade i spring-boot-maven-plugin
maven-jar-plugin, shade i spring-boot-maven-pluginTres formes de produir un artefacte executable, amb propietats diferents.
maven-jar-plugin: el jar simple
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>com.nexussoftware.bibliotech.BiblioTechApplication</mainClass>
</manifest>
</archive>
</configuration>
</plugin>Produeix un jar amb només les teves classes. Per executar-lo cal aportar el classpath complet:
És el que es fa servir per publicar llibreries, on les dependències les resol el consumidor.
maven-shade-plugin: l'uber-jar
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<transformers>
<transformer implementation=
"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.nexussoftware.bibliotech.BiblioTechApplication</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>Descomprimeix totes les dependències i les fica barrejades en un sol jar. Funciona, però té un problema conegut: si dos jars tenen fitxers amb el mateix nom a META-INF/services —molt habitual—, un trepitja l'altre i alguna cosa deixa de funcionar de forma misteriosa. Es resol amb transformers, però cal saber-ho.
spring-boot-maven-plugin: el jar en capes
És el que fa servir BiblioTech. Ja en vas veure l'estructura a 11-02: manté cada dependència com a jar independent dins de BOOT-INF/lib/ i aporta el seu propi carregador de classes.
| jar simple | shade | Spring Boot | |
|---|---|---|---|
| Dependències | Fora | Descomprimides i barrejades | Jars intactes dins |
| Col·lisió de recursos | — | Sí, risc real | No |
Executable amb java -jar |
Només amb classpath | Sí | Sí |
| Capes per a Docker | No | No | Sí (12-06) |
| Ús típic | Llibreries | Aplicacions sense Spring | Aplicacions Spring Boot |
Aquesta fila de les capes importa per a 12-06: el jar de Spring Boot es pot descompondre en capes (dependències, dependències snapshot, recursos, classes de l'aplicació) de manera que una imatge Docker només hagi de reconstruir la capa que va canviar. Com que les dependències canvien molt menys que el teu codi, el desplegament passa de pujar 50 MB a pujar 200 KB.
exec-maven-plugin
Per executar una classe qualsevol sense Spring Boot:
Útil per a utilitats i scripts de manteniment dins del projecte.
- Perfils
Un perfil activa configuració condicional. No confondre amb els perfils de Spring d'11-02: aquells afecten els beans en execució; aquests afecten la construcció.
<profiles>
<profile>
<id>rapid</id>
<properties>
<skipITs>true</skipITs>
</properties>
</profile>
<profile>
<id>ci</id>
<activation>
<property><name>env.CI</name></property> <!-- si existeix la variable CI -->
</activation>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId> <!-- cobertura, 12-05 -->
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>produccio</id>
<properties>
<spring.profiles.active>prod</spring.profiles.active>
</properties>
</profile>
</profiles>Activació:
| Forma | Sintaxi |
|---|---|
| Explícita | mvn package -Prapid |
| Desactivar | mvn package -P!rapid |
| Per propietat | <activation><property>... |
| Per variable d'entorn | env.CI |
| Per sistema operatiu | <activation><os><family>windows |
| Per versió de JDK | <activation><jdk>17</jdk> |
| Per defecte | <activeByDefault>true</activeByDefault> |
Veure quins perfils estan actius:
Avís: no fiquis lògica de negoci en perfils de Maven. Si l'artefacte que construeixes és diferent segons el perfil, ja no estàs provant el que desplegues. La pràctica moderna és construir un sol artefacte i configurar-lo en execució amb variables d'entorn (11-02, 12-06).
- Projectes multimòdul
Quan un projecte creix, es divideix en mòduls. És el que necessitarà BiblioTech a 12-01.
bibliotech/
├── pom.xml <- pare agregador, packaging: pom
├── bibliotech-domini/
│ ├── pom.xml
│ └── src/main/java/... <- entitats i regles. SENSE dependencies de framework
├── bibliotech-persistencia/
│ ├── pom.xml
│ └── src/main/java/... <- repositoris JPA. Depen de domini
├── bibliotech-servei/
│ ├── pom.xml
│ └── src/main/java/... <- logica. Depen de domini i persistencia
└── bibliotech-app/
├── pom.xml
└── src/main/java/... <- @SpringBootApplication. Depen de totsEl POM pare:
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/>
</parent>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging> <!-- NO produeix jar: agrega -->
<modules>
<module>bibliotech-domini</module>
<module>bibliotech-persistencia</module>
<module>bibliotech-servei</module>
<module>bibliotech-app</module>
</modules>
<dependencyManagement>
<dependencies>
<!-- Versions dels moduls interns, centralitzades -->
<dependency>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-domini</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-persistencia</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>Un mòdul fill:
<project>
<parent>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>bibliotech-persistencia</artifactId> <!-- groupId i version heretats -->
<dependencies>
<dependency>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-domini</artifactId> <!-- sense version: la dona el pare -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
</project>Des de l'arrel:
mvn clean install # construeix TOTS els moduls, en ordre de dependencies
mvn -pl bibliotech-servei install # nomes aquest modul
mvn -pl bibliotech-servei -am install # aquest modul I aquells dels quals depenEl benefici real no és organitzatiu, és arquitectònic: bibliotech-domini no pot fer servir Spring ni JPA, perquè no els té al seu classpath. Maven imposa la separació de capes que en un projecte d'un sol mòdul depèn de la disciplina de l'equip. És un límit verificat per la construcció, i és molt potent.
Es desenvolupa a 12-01.
settings.xml i les credencials
settings.xml i les credencialsMentre que el pom.xml descriu el projecte i va a git, settings.xml descriu la teva màquina i mai no va a git.
Ubicacions: ~/.m2/settings.xml (usuari) i ${maven.home}/conf/settings.xml (global).
<settings>
<!-- Mirall corporatiu: tot hi passa -->
<mirrors>
<mirror>
<id>nexus-corporatiu</id>
<mirrorOf>*</mirrorOf>
<url>https://nexus.nexussoftware.com/repository/maven-public/</url>
</mirror>
</mirrors>
<!-- CREDENCIALS: per aixo aquest fitxer NO va al repositori -->
<servers>
<server>
<id>nexus-corporatiu</id>
<username>${env.NEXUS_USUARI}</username>
<password>${env.NEXUS_PASSWORD}</password>
</server>
</servers>
<proxies>
<proxy>
<id>proxy-empresa</id>
<active>true</active>
<protocol>http</protocol>
<host>proxy.nexussoftware.com</host>
<port>8080</port>
</proxy>
</proxies>
</settings>Advertiment de seguretat. Les credencials mai no es posen al
pom.xml, perquè el POM va al repositori de codi i queda al seu historial per sempre. Van asettings.xml, i preferiblement llegides de variables d'entorn com a l'exemple. Maven ofereixmvn --encrypt-passwordper xifrar-les amb una clau mestra, encara que no substitueix un gestor de secrets.I una regla que sí que és absoluta: un secret que ha estat a git es considera compromès per sempre. Esborrar-lo en un commit posterior no l'elimina de l'historial. Si passa, cal rotar la credencial. La gestió de secrets es tracta a 12-07.
- El Maven Wrapper
Problema real: tu fas servir Maven 3.9.8, un company té 3.6.3, el servidor d'integració contínua té 3.8.1. Alguna cosa falla només en una de les tres. Diagnosticar-ho costa un dia.
El Maven Wrapper ho resol: un script al repositori que descarrega i fa servir la versió exacta de Maven que el projecte declara.
Genera:
mvnw <- script Unix
mvnw.cmd <- script Windows
.mvn/wrapper/maven-wrapper.properties <- la versio declaradaI a partir d'aquí:
Avantatges:
- Tothom fa servir la mateixa versió, sense excepció.
- No cal instal·lar Maven per compilar el projecte. Només Java.
- La versió és a git, així que canviar-la és un commit revisable.
- La integració contínua no necessita configurar res.
Aquests fitxers sí que van a git (a diferència de settings.xml). Spring Initializr els genera per defecte, i és una pràctica estàndard avui: un repositori Java modern es compila amb ./mvnw verify sense instal·lar res més que un JDK.
- Ordres del dia a dia
| Ordre | Què fa |
|---|---|
mvn clean |
Esborra target/ |
mvn compile |
Compila el codi de producció |
mvn test |
Compila i executa les proves unitàries |
mvn package |
Genera el jar |
mvn verify |
Tot, incloses les proves d'integració |
mvn install |
Instal·la a ~/.m2 |
mvn clean install |
Reconstrueix des de zero i instal·la |
mvn -DskipTests package |
Empaqueta sense executar les proves (les compila) |
mvn -Dmaven.test.skip=true package |
Ni les compila |
mvn -o package |
Sense connexió: només el repositori local |
mvn -U package |
Força l'actualització de snapshots |
mvn -X package |
Sortida de depuració completa |
mvn -q package |
Silenciós: només errors |
mvn -T 1C package |
Paral·lel: 1 fil per nucli |
mvn -pl modul -am install |
Un mòdul i les seves dependències |
mvn dependency:tree |
L'arbre de dependències |
mvn dependency:analyze |
Declarades i no usades / usades i no declarades |
mvn versions:display-dependency-updates |
Què està desactualitzat |
mvn versions:display-plugin-updates |
Plugins desactualitzats |
mvn help:effective-pom |
El POM resultant després de l'herència |
mvn help:active-profiles |
Perfils actius |
Tres notes pràctiques:
-DskipTestsenfront de-Dmaven.test.skip=true: el primer compila les proves però no les executa (detecta errors de compilació en elles); el segon ni les compila. Prefereix el primer.-o(offline) és útil en un tren o amb la xarxa caiguda, si ja tens tot a~/.m2.-T 1Cpot reduir a la meitat el temps d'un projecte multimòdul gran. És segur si els plugins que fas servir són compatibles amb l'execució paral·lela, i els habituals ho són.
I per a l'actualització periòdica de dependències:
[INFO] The following dependencies have newer versions:
[INFO] com.h2database:h2 ................................. 2.2.224 -> 2.3.230
[INFO] org.springframework.boot:spring-boot-starter ....... 3.3.2 -> 3.3.3Executar-lo cada poques setmanes i aplicar els pedaços és una de les pràctiques d'higiene més rendibles que existeixen, i la resposta directa a la lliçó de Log4Shell.
- Reproductibilitat i fixació de versions
La mateixa etiqueta de git ha de produir el mateix artefacte, avui i d'aquí a tres anys. Això és reproductibilitat, i és el que permet reconstruir una versió antiga per depurar un incident.
Coses que la trenquen:
| Pràctica | Per què trenca | Alternativa |
|---|---|---|
<version>LATEST</version> |
Canvia amb el temps. Eliminat a Maven 3 | Versió concreta |
<version>RELEASE</version> |
Igual. Eliminat | Versió concreta |
<version>[1.0,2.0)</version> (rang) |
Resol diferent segons el dia | Versió concreta |
Dependències -SNAPSHOT |
Mutables per definició | Versió alliberada |
| No fixar versions de plugins | Maven tria i pot canviar | Fixar-les (o heretar del BOM) |
| Dependre de la codificació de la plataforma | Diferent per màquina | project.build.sourceEncoding |
| Dependre de la versió de JDK instal·lada | Diferent per màquina | <release>17</release> |
Aquest penúltim punt és real i s'oblida sempre: si no fixes la versió d'un plugin, Maven fa servir l'última disponible i la teva compilació pot canviar de comportament sense que hagis tocat res. El spring-boot-starter-parent fixa les dels plugins habituals; per als altres, posa'ls versió.
La reproductibilitat no és teòrica. El dia que hi hagi un incident a la versió 1.4.2 que es va desplegar fa vuit mesos, hauràs de reconstruir-la exactament per depurar-la.
- Maven enfront de Gradle
Les dues eines dominants. Comparació honesta:
| Aspecte | Maven | Gradle |
|---|---|---|
| Configuració | XML declaratiu | DSL en Groovy o Kotlin (imperatiu) |
| Verbositat | Alta (XML) | Baixa |
| Corba d'aprenentatge | Baixa: convencions fixes | Mitjana-alta: cal aprendre el DSL |
| Predictibilitat | Molt alta: cicle de vida fix | Menor: qualsevol pot escriure lògica |
| Compilació incremental | No | Sí |
| Memòria cau de construcció | No | Sí, local i remota |
| Velocitat en projectes grans | Menor | Bastant més gran |
| Dimoni en segon pla | No | Sí |
| Flexibilitat | Limitada (plugins) | Total (és codi) |
| Llegibilitat per tercers | Alta: tots els POM s'assemblen | Variable: pot haver-hi lògica arbitrària |
| Adopció a l'empresa | Majoritària | Creixent |
| Android | No | Estàndard |
| Eines i documentació | Enorme | Àmplia |
Un exemple del mateix projecte en tots dos:
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>Gradle és evidentment més concís. I també és on hi ha el seu risc: com que el fitxer de construcció és codi, un equip pot acabar amb lògica condicional, tasques pròpies i comportament que cal llegir i entendre abans de tocar res. Un pom.xml de tres-centes línies és avorrit però s'entén d'un cop d'ull; un build.gradle.kts de cent línies pot fer qualsevol cosa.
Per què aquest curs fa servir Maven:
- És el majoritari en back-end empresarial Java: el que et trobaràs.
- És més fàcil d'aprendre: no cal aprendre un llenguatge a més de l'eina.
- És predictible: el cicle de vida és sempre el mateix, i això és exactament el que vols quan estàs aprenent la resta.
- La documentació de Spring Boot fa servir Maven als seus exemples principals.
I una recomanació professional: aprèn Maven primer, i Gradle quan el necessitis. Els conceptes —coordenades, àmbits, transitivitat, cicle de vida, plugins— són els mateixos en tots dos. Canvia la sintaxi.
- BiblioTech com a projecte Maven complet
El resultat. Estructura final:
bibliotech/
├── .gitignore <- target/, *.iml, .idea/
├── .mvn/wrapper/
│ └── maven-wrapper.properties
├── mvnw, mvnw.cmd
├── pom.xml
└── src/
├── main/
│ ├── java/com/nexussoftware/bibliotech/
│ │ ├── BiblioTechApplication.java
│ │ ├── config/ PropietatsBiblioTech, ConfiguracioBiblioTech
│ │ ├── domini/ Material, Llibre, Revista, Dvd, Empleat, Prestec, Reserva
│ │ ├── servei/ GestorPrestecs, CalculadoraMultes, ServeiAvisos
│ │ ├── persistencia/ MaterialRepository, EmpleatRepository, PrestecRepository
│ │ ├── xarxa/ ClientMetadades, ServidorCataleg
│ │ ├── infraestructura/ AspecteCronometre
│ │ └── presentacio/ ArrencadaBiblioTech
│ └── resources/
│ ├── application.yml
│ ├── application-dev.yml
│ ├── application-prod.yml
│ └── data.sql
└── test/
├── java/com/nexussoftware/bibliotech/
│ ├── domini/ PrestecTest, MaterialTest, IsbnTest
│ ├── servei/ CalculadoraMultesTest, GestorPrestecsTest
│ ├── persistencia/ EscripturaAtomicaTest, PrestecRepositoryIT
│ └── BiblioTechApplicationTests
└── resources/
└── application-test.ymlEl .gitignore mínim:
I el cicle complet:
# Compilacio neta amb totes les proves
./mvnw clean verify
# Desenvolupament
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
# Empaquetar i executar
./mvnw clean package
java -jar target/bibliotech-1.0.0-SNAPSHOT.jar --spring.profiles.active=prod
# Diagnostic
./mvnw dependency:tree
./mvnw versions:display-dependency-updatesSortida de ./mvnw clean verify:
[INFO] --- clean:3.2.0:clean (default-clean) @ bibliotech ---
[INFO] Deleting /home/marta/bibliotech/target
[INFO] --- resources:3.3.1:resources (default-resources) @ bibliotech ---
[INFO] Copying 4 resources from src/main/resources
[INFO] --- compiler:3.13.0:compile (default-compile) @ bibliotech ---
[INFO] Compiling 31 source files with javac [debug release 17]
[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ bibliotech ---
[INFO] Compiling 9 source files with javac [debug release 17]
[INFO] --- surefire:3.2.5:test (default-test) @ bibliotech ---
[INFO] Tests run: 41, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- jar:3.4.1:jar (default-jar) @ bibliotech ---
[INFO] Building jar: /home/marta/bibliotech/target/bibliotech-1.0.0-SNAPSHOT.jar
[INFO] --- spring-boot:3.3.2:repackage (repackage) @ bibliotech ---
[INFO] Replacing main artifact with repackaged archive
[INFO] --- failsafe:3.2.5:integration-test (default) @ bibliotech ---
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- failsafe:3.2.5:verify (default) @ bibliotech ---
[INFO] BUILD SUCCESS
[INFO] Total time: 24.183 sEn aquesta sortida hi ha tota la lliçó: les fases en ordre, els plugins que executa cadascuna, surefire amb les 41 proves unitàries, el jar, el repackage de Spring Boot i failsafe amb les 3 d'integració.
Comparació amb el punt de partida:
| Aspecte | javac a mà (mòduls 1-10) |
Maven |
|---|---|---|
| Compilar | Ordre de 15 línies | ./mvnw compile |
| Dependències | Descàrrega manual de 40 jars | 6 declaracions al POM |
| Transitives | A mà, una a una | Automàtic |
| Conflictes | Prova i error | Regla determinista |
| Proves | No hi havia forma còmoda | ./mvnw test |
| Empaquetar | jar amb manifest a mà |
./mvnw package |
| Una altra màquina | "Funciona a la meva" | ./mvnw verify, i ja |
| Canviar una versió | Descarregar, substituir, resar | Una línia, verify |
| Auditoria de seguretat | Impossible | dependency:tree |
Aquesta penúltima fila és la resposta completa a Log4Shell: una línia per canviar la versió, una ordre per verificar que res no s'ha trencat.
- Errors comuns i consells
Error: posar <version> a dependències que gestiona el BOM. Anul·les versions provades i provoques incompatibilitats subtils que apareixen en execució. Si necessites una altra, sobreescriu la propietat.
Error: fer servir rangs de versions o LATEST. Trenquen la reproductibilitat. Versions concretes sempre.
Error: creure que mvn test executa les proves d'integració. Les *IT les executa failsafe, a mvn verify. I no hi ha cap avís que no s'hagin executat.
Error: no fixar la versió dels plugins. Maven fa servir l'última disponible i la teva compilació canvia sense que toquis res.
Error: pujar target/ a git. És tot regenerable, ocupa molt i provoca conflictes constants.
Error: credencials al pom.xml. El POM va a git i queda a l'historial per sempre. Van a settings.xml, i un secret que ha estat a git cal rotar-lo.
Error: mvn clean install per a tot. És més lent del necessari i omple ~/.m2. Per verificar, mvn verify.
Error: -DskipTests a la integració contínua. Allà és exactament on s'han d'executar.
Error: no declarar una dependència que fas servir directament. Si t'arriba de forma transitiva i demà l'intermediari deixa de portar-la, el teu codi deixa de compilar. mvn dependency:analyze ho detecta.
Error: source/target en comptes de release. Permet compilar codi que fa servir API inexistent a la versió destí, i la fallada apareix en execució.
Error: oblidar project.build.sourceEncoding. Els accents es corrompen de forma diferent segons la màquina.
Consell: fes servir sempre el Maven Wrapper. Elimina tota una classe de problemes de "a la meva màquina funciona" i no exigeix instal·lar res.
Consell: aprèn a llegir dependency:tree. És el que més vegades et traurà d'un compromís, i és l'eina de resposta davant de vulnerabilitats.
Consell: mvn help:effective-pom quan no entenguis d'on surt alguna cosa. Mostra el POM després de l'herència, amb tot resolt.
Consell: mvn versions:display-dependency-updates cada poques setmanes. Actualitzar pedaços amb regularitat és molt més barat que un salt gran d'emergència.
Consell: separa *Test de *IT. El cicle ràpid triga segons; la integració contínua ho executa tot.
Consell: mvn -T 1C en projectes grans. Pot reduir el temps a la meitat.
Consell: quan alguna cosa falli de forma inexplicable, mvn -X. La sortida de depuració diu d'on surt cada configuració i cada artefacte.
- Exercicis
Exercici 1: llegir i arreglar un POM
Un company de Nexus Software ha escrit aquest pom.xml per a un microservei nou. Compila, però té set problemes. Troba'ls, explica la conseqüència de cadascun i escriu la versió corregida.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
</parent>
<groupId>com.nexussoftware</groupId>
<artifactId>servei-cataleg</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>LATEST</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
</dependency>
<dependency>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-comu</artifactId>
<version>2.1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
</project>Exercici 2: resoldre un conflicte de dependències
BiblioTech llança aquesta excepció en execució, no en compilar:
java.lang.NoSuchMethodError: 'com.fasterxml.jackson.databind.ObjectMapper
com.fasterxml.jackson.databind.json.JsonMapper$Builder.build()'L'arbre de dependències, retallat:
com.nexussoftware:bibliotech:jar:1.0.0-SNAPSHOT
+- org.springframework.boot:spring-boot-starter-web:jar:3.3.2:compile
| \- org.springframework.boot:spring-boot-starter-json:jar:3.3.2:compile
| \- com.fasterxml.jackson.core:jackson-databind:jar:2.17.2:compile
+- com.exemple:client-informes:jar:4.2.0:compile
| \- com.fasterxml.jackson.core:jackson-databind:jar:2.9.8:compile
\- com.exemple:utilitats-nexus:jar:1.8.0:compile
\- com.fasterxml.jackson.core:jackson-databind:jar:2.13.0:compileEs demana:
- Explica per què l'error apareix en execució i no en compilar.
- Aplica la regla de resolució: quina versió guanya i per què?
- Dóna tres solucions diferents amb el seu XML, indicant avantatges i inconvenients.
- Tria'n una i justifica-la.
- Escriu l'ordre que confirma que el conflicte s'ha resolt.
Exercici 3: separar les proves i afegir cobertura
BiblioTech té 41 proves unitàries (11-04) i afegirà proves d'integració contra H2. Actualment mvn test triga 6 segons; amb les d'integració passaria a 45.
Configura el pom.xml perquè:
mvn testexecuti només les unitàries (ràpid, per a desenvolupament).mvn verifyexecuti unitàries i integració.- Les d'integració es diguin
*IT.javai estiguin etiquetades amb@Tag("integracio"). - Existeixi un perfil
cique a més generi l'informe de cobertura amb JaCoCo i falli si la cobertura d'instruccions baixa del 70 %. - Existeixi un perfil
rapidque salti les proves d'integració fins i tot amvn verify. - Escriu les ordres de cada escenari: desenvolupament, abans de pujar canvis, integració contínua i empaquetatge urgent.
Solucions
Solució 1
Els set problemes:
| # | Problema | Conseqüència |
|---|---|---|
| 1 | <version>3.2.0</version> a spring-boot-starter-data-jpa |
Anul·la el BOM del pare (3.3.2). Barreja Spring Data 3.2 amb Spring Framework 6.1.11: incompatibilitats subtils en execució |
| 2 | <version>LATEST</version> |
Eliminat a Maven 3; i encara que funcionés, trenca la reproductibilitat per complet |
| 3 | H2 sense <scope>runtime</scope> |
S'empaqueta al classpath de compilació: algú es pot acoblar a H2 per accident, i la BD de proves viatja a producció |
| 4 | junit-jupiter sense <scope>test</scope> |
JUnit acaba dins del jar de producció: pes i superfície d'atac innecessaris |
| 5 | JUnit amb versió explícita | El BOM ja la gestiona. I a més s'hauria de fer servir spring-boot-starter-test, que porta AssertJ i Mockito |
| 6 | Dependència -SNAPSHOT en un projecte amb versió alliberada 1.0.0 |
La versió 1.0.0 no és reproduïble: depèn d'un artefacte mutable. Mai no s'allibera depenent d'un SNAPSHOT |
| 7 | <source>/<target> en comptes de <release> |
Permet compilar amb JDK 21 codi que fa servir API de Java 21 i generar bytecode 17: NoSuchMethodError en execució. A més, el pare ja ho configura amb java.version |
Un vuitè, menor: falta <relativePath/> al <parent>, cosa que fa que Maven busqui primer un POM a ../pom.xml. No falla, però emet un avís i pot sorprendre.
Versió corregida:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- (8) busca'l al repositori -->
</parent>
<groupId>com.nexussoftware</groupId>
<artifactId>servei-cataleg</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version> <!-- (7) el pare l'aplica al compilador -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- (1) sense version: la gestiona el BOM del pare -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- (2) sense version: Jackson tambe el gestiona el BOM -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- (3) runtime: no es fa servir en compilar -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- (4)(5) l'starter de proves: JUnit + AssertJ + Mockito, ambit test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- (6) versio ALLIBERADA de la llibreria interna -->
<dependency>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-comu</artifactId>
<version>2.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Nota sobre el punt 6: si bibliotech-comu:2.1.0 encara no està publicada, la solució correcta és publicar-la abans d'alliberar el servei. Mentrestant, el servei ha de ser 1.0.0-SNAPSHOT. És la regla que una versió alliberada només pot dependre de versions alliberades.
Solució 2
1. Per què en execució i no en compilar.
En compilació, Maven fa servir la versió resolta —2.9.8, com es veu al punt 2— i el compilador només verifica les signatures que el teu codi fa servir. client-informes ja està compilat: el seu bytecode conté una crida a JsonMapper$Builder.build() que existia a 2.17 però no a 2.9.8.
En execució, la JVM intenta enllaçar aquesta crida contra la classe que hi ha al classpath, no la troba i llança NoSuchMethodError. Els errors d'enllaç són d'execució, no de compilació: és el mateix mecanisme del mòdul 10 en parlar de la càrrega de classes.
2. Quina versió guanya.
Les tres són a profunditat 2 o 3:
| Ruta | Profunditat | Versió |
|---|---|---|
starter-web → starter-json → jackson-databind |
3 | 2.17.2 |
client-informes → jackson-databind |
2 | 2.9.8 |
utilitats-nexus → jackson-databind |
2 | 2.13.0 |
Empaten client-informes (2.9.8) i utilitats-nexus (2.13.0) a profunditat 2. En cas d'empat guanya la declarada primer al POM, i client-informes apareix abans. Guanya 2.9.8, la més antiga de les tres.
Aquest és exactament el cas que sorprèn: no guanya la més nova, guanya la més propera, i una transitiva antiga pot tombar el projecte.
3. Tres solucions.
A. Declarar la dependència directament (profunditat 1: guanya sempre):
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<!-- sense version: la del BOM de Spring Boot (2.17.2) -->
</dependency>
...
</dependencies>A favor: simple, explícita, i dependency:analyze deixa de queixar-se. En contra: cal recordar que hi és per un conflicte; convé un comentari.
B. dependencyManagement (guanya sobre qualsevol profunditat):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>
</dependencyManagement>A favor: és l'eina pensada per a això, deixa clara la intenció i és la forma estàndard d'apedaçar una transitiva vulnerable. En contra: fixa la versió a mà en comptes de deixar-la al BOM (millor: sobreescriure la propietat <jackson.version>).
C. exclusions a les dependències problemàtiques:
<dependency>
<groupId>com.exemple</groupId>
<artifactId>client-informes</artifactId>
<version>4.2.0</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- i el mateix a utilitats-nexus -->A favor: explícit sobre qui causava el problema. En contra: verbós, cal repetir-ho a cada dependència culpable, i si demà n'apareix una tercera cal recordar-se'n.
4. Quina triar.
La B, amb un matís: en lloc de fixar 2.17.2 a mà, sobreescriure la propietat del BOM.
<properties>
<!-- Forcem la versio de Jackson: client-informes:4.2.0 arrossega la 2.9.8,
que no te JsonMapper.Builder.build() (NoSuchMethodError, BIB-207) -->
<jackson.version>2.17.2</jackson.version>
</properties>Raons: dependencyManagement (que és el que hi ha darrere d'aquesta propietat) és el mecanisme dissenyat per a això, guanya a qualsevol profunditat, funciona encara que demà aparegui una quarta dependència que porti una altra versió, i el comentari documenta el perquè per a qui el llegeixi d'aquí a dos anys.
Precaució obligatòria: pujar client-informes de Jackson 2.9.8 a 2.17.2 és un salt de vuit versions menors. Podria fer servir API que va canviar. Per això aquest canvi ha d'anar acompanyat de les proves d'11-04 executant-se en verd — que és exactament l'argument amb què es va tancar aquella lliçó.
5. Comprovació:
[INFO] +- com.exemple:client-informes:jar:4.2.0:compile
[INFO] | \- (com.fasterxml.jackson.core:jackson-databind:jar:2.9.8:compile
- omitted for conflict with 2.17.2)I després, mvn verify per confirmar que les 41 proves continuen en verd.
Solució 3
<build>
<plugins>
<!-- (1) SUREFIRE: nomes proves UNITARIES -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- Doble seguretat: per nom i per etiqueta -->
<excludes>
<exclude>**/*IT.java</exclude>
</excludes>
<excludedGroups>integracio</excludedGroups>
</configuration>
</plugin>
<!-- (2)(3) FAILSAFE: proves d'INTEGRACIO a verify -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<includes>
<include>**/*IT.java</include>
</includes>
<groups>integracio</groups>
<skipITs>${skipITs}</skipITs> <!-- (5) controlat per perfil -->
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<skipITs>false</skipITs> <!-- (5) per defecte SI que s'executen -->
<jacoco.version>0.8.12</jacoco.version>
</properties>
<profiles>
<!-- (4) PERFIL ci: cobertura amb llindar -->
<profile>
<id>ci</id>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<!-- Instrumenta abans de les proves -->
<execution>
<id>preparar-agent</id>
<goals><goal>prepare-agent</goal></goals>
</execution>
<!-- Genera l'informe HTML despres de les proves -->
<execution>
<id>informe</id>
<phase>test</phase>
<goals><goal>report</goal></goals>
</execution>
<!-- Comprova el llindar i FALLA si no es compleix -->
<execution>
<id>llindar-cobertura</id>
<phase>verify</phase>
<goals><goal>check</goal></goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<!-- (5) PERFIL rapid: sense proves d'integracio -->
<profile>
<id>rapid</id>
<properties>
<skipITs>true</skipITs>
</properties>
</profile>
</profiles>I la prova d'integració corresponent:
package com.nexussoftware.bibliotech.persistencia;
import org.junit.jupiter.api.Tag;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
@DataJpaTest // (11-04) nomes la capa de persistencia
@Tag("integracio") // (3) etiqueta
class PrestecRepositoryIT { // (3) sufix IT: la recull failsafe, no surefire
@Autowired private PrestecRepository repositori;
// ...
}(6) Ordres per escenari:
| Escenari | Ordre | Què executa | Temps |
|---|---|---|---|
| Desenvolupament, després de cada canvi | ./mvnw test |
Només les 41 unitàries | ~6 s |
| Abans de pujar canvis | ./mvnw verify |
Unitàries + integració | ~45 s |
| Integració contínua | ./mvnw clean verify -Pci |
Tot + cobertura amb llindar | ~60 s |
| Empaquetatge urgent | ./mvnw package -Prapid |
Només unitàries, genera el jar | ~10 s |
| Emergència real | ./mvnw package -DskipTests |
Cap prova. Només per depurar en local | ~5 s |
Detalls de la solució que convé subratllar:
- Doble filtre a surefire (per nom
*ITi per etiqueta). És redundant a propòsit: si algú oblida el sufixITperò posa l'etiqueta —o al revés—, la prova continua quedant fora del cicle ràpid. Els filtres redundants en configuració de construcció són barats i eviten sorpreses. ${skipITs}com a propietat en comptes de duplicar la configuració del plugin al perfil. El perfil només canvia un valor.- JaCoCo només al perfil
ci. Instrumentar el bytecode alenteix les proves; en desenvolupament no aporta res. checka la faseverify. Així la compilació falla per cobertura insuficient després d'haver executat totes les proves, no abans.- El llindar del 70 % és una decisió d'equip, no una veritat universal. I recorda l'avís d'11-04: la cobertura no és qualitat, i un llindar massa alt genera proves dolentes. Es discuteix a fons a 12-05.
Conclusió
BiblioTech és ara un projecte Maven complet, reproduïble i executable a qualsevol màquina del món que tingui Java 17.
Entens quin problema resol una eina de construcció: no només compilar, sinó resoldre l'infern de les dependències —els trenta artefactes transitius que porta un sol starter, amb versions que han de ser compatibles entre si—, executar proves, empaquetar i fer-ho tot de forma reproduïble. I saps per què el javac amb classpath a mà del mòdul 1 va deixar de ser viable tan bon punt va entrar Spring Boot.
Coneixes el principi de convenció sobre configuració, que explica per què a 11-04 vas posar les proves a src/test/java i mvn test les va trobar sense que configuressis res, i per què qualsevol projecte Maven del món es compila igual sense llegir documentació. Amb el seu compromís reconegut: si el teu projecte no encaixa en les convencions, Maven es torna incòmode.
Saps llegir i escriure un POM: les coordenades groupId:artifactId:version que ja vas veure a 11-01 i la seva correspondència amb el camí a ~/.m2, el packaging, la diferència entre SNAPSHOT mutable i versió alliberada immutable —amb la seva regla professional: una versió alliberada mai no se sobreescriu i mai no depèn d'un SNAPSHOT—, i les properties per centralitzar versions i fixar la codificació.
Domines les dependències: els tres repositoris on es busquen, els àmbits —inclosos els dos que ja feies servir sense saber-ho, runtime per a H2 i test per a JUnit, que és el que evita que un framework de proves acabi dins del jar de producció—, la transitivitat que porta trenta jars per una declaració, i la regla de la definició més propera amb la seva conseqüència que sorprèn tothom: no guanya la més nova, guanya la més propera, i una transitiva antiga pot provocar un NoSuchMethodError en execució. Saps diagnosticar-ho amb dependency:tree —que a més respon a la pregunta de seguretat d'11-01: "tinc aquesta llibreria i qui l'ha ficat?"— i amb dependency:analyze, que detecta el risc real de fer servir directament alguna cosa que només tens de forma transitiva.
Distingeixes dependencyManagement de dependencies i coneixes els seus tres usos, inclòs el que importa per a la seguretat: forçar la versió corregida d'una transitiva vulnerable sense esperar que l'intermediari s'actualitzi. I entens per fi del tot el spring-boot-starter-parent com a BOM que gestiona més de 400 artefactes, amb l'advertiment que arrossegaves des d'11-02 ara justificat: posar <version> al que gestiona el BOM és anul·lar versions provades juntes.
Coneixes el cicle de vida amb les seves tres cadenes i la idea clau que executar una fase executa totes les anteriors —que és la resposta a per què mvn test compilava el teu codi sense demanar-l'hi—, i la segona idea clau: Maven no fa res per si mateix, tot ho fan els plugins enganxats a fases, invocables també directament com a plugin:goal. Saps què fan els de BiblioTech, per què <release>17</release> és millor que source/target, i —fonamental— la diferència entre surefire i failsafe, amb el parany en què cau tothom: mvn test no executa les *IT, i no n'avisa.
Saps empaquetar de tres formes i per què el jar en capes de Spring Boot és superior a l'uber-jar de shade, amb la seva conseqüència per a 12-06: reconstruir només la capa que va canviar converteix un desplegament de 50 MB en un de 200 KB. Manejes perfils de construcció —diferents dels de Spring— amb l'avís de no construir artefactes diferents per entorn. I coneixes els projectes multimòdul, el benefici real dels quals no és organitzatiu sinó arquitectònic: bibliotech-domini no pot fer servir Spring perquè no el té al seu classpath, i això converteix la separació de capes en un límit verificat per la construcció.
Saps que les credencials van a settings.xml, mai al POM, i que un secret que ha estat a git es considera compromès per sempre. Fas servir el Maven Wrapper, que elimina tota una classe de problemes de "a la meva màquina funciona" i permet compilar el projecte sense instal·lar Maven. Tens les ordres del dia a dia i les pràctiques de reproductibilitat: mai rangs, mai LATEST, mai SNAPSHOT en producció, versions de plugins fixades i codificació explícita.
I tens criteri per a la comparació amb Gradle: més concís, amb memòria cau i incremental, bastant més ràpid en projectes grans; però menys predictible, perquè el seu fitxer de construcció és codi que pot fer qualsevol cosa. Amb la recomanació pràctica: els conceptes es transfereixen; aprèn Maven primer.
BiblioTech es compila amb ./mvnw clean verify en vint-i-quatre segons: quaranta-una proves unitàries, tres d'integració, i un jar executable de cinquanta megues que arrenca a qualsevol màquina amb Java 17. I canviar la versió d'una dependència vulnerable és ara una línia i una ordre. La lliçó de Log4Shell està saldada.
Però torna a mirar aquestes quaranta-una proves.
Proven CalculadoraMultes, que només necessita un Clock. Proven les regles de Prestec, que són Java pur. Proven EscripturaAtomica amb un directori temporal. I tan bon punt va aparèixer un col·laborador de veritat —un repositori, un servei d'avisos—, vas haver d'escriure a mà una implementació en memòria i una altra que registrava crides: vint línies de classes internes per prova, amb dos col·laboradors senzills. GestorPrestecs en té cinc. PrestecRepository és una interfície de Spring Data amb vint mètodes heretats: implementar-la a mà és inviable. I el ClientMetadades de 09-06 necessita una API HTTP real a l'altre costat.
Hi ha a més preguntes que les assercions sobre el resultat no responen. Es va enviar l'avís a l'empleat correcte, amb el text correcte? Es va cridar el repositori una sola vegada o quaranta? Què passa quan la base de dades llança una excepció —un escenari dificilíssim de provocar amb una implementació real i que en producció passarà un dimarts qualsevol?
A la propera lliçó apareixen els dobles de prova amb nom i cognoms —dummy, stub, spy, mock i fake— i Mockito 5, que els genera per tu amb una anotació, fent servir exactament la generació de bytecode del mòdul 10. Definiràs comportament amb when(...).thenReturn(...), provocaràs fallades amb thenThrow, verificaràs interaccions amb verify —i aprendràs el criteri de quan verificar i quan no, perquè l'excés de verificació produeix just aquelles proves fràgils que es trenquen en refactoritzar—, capturaràs amb ArgumentCaptor el que realment es va passar al servei d'avisos, i provaràs el ClientMetadades sense tocar la xarxa.
I veuràs una cosa més important que qualsevol API: que de vegades la millor resposta no és un simulat, sinó un disseny que fa innecessari el simulat. Just com va fer el Clock fix d'11-04.
Curs de Programació en Java
Mòdul 1: Introducció a Java
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
