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

  1. El problema: javac a mà
  2. Què resol una eina de construcció
  3. Convenció sobre configuració
  4. L'estructura estàndard de directoris
  5. Instal·lació i comprovació
  6. El POM: coordenades
  7. SNAPSHOT enfront de versió alliberada
  8. properties: variables del POM
  9. El pom.xml complet de BiblioTech
  10. Dependències: declaració i repositoris
  11. Àmbits de dependència
  12. Dependències transitives
  13. Resolució de conflictes: la definició més propera
  14. mvn dependency:tree a la pràctica
  15. exclusions
  16. dependencyManagement enfront de dependencies
  17. El BOM i el spring-boot-starter-parent
  18. El cicle de vida: tres cadenes
  19. Les fases de la cadena per defecte
  20. Plugins i goals
  21. Els plugins que fa servir BiblioTech
  22. surefire enfront de failsafe
  23. Empaquetar: maven-jar-plugin, shade i spring-boot-maven-plugin
  24. Perfils
  25. Projectes multimòdul
  26. settings.xml i les credencials
  27. El Maven Wrapper
  28. Ordres del dia a dia
  29. Reproductibilitat i fixació de versions
  30. Maven enfront de Gradle
  31. BiblioTech com a projecte Maven complet
  32. Errors comuns i consells
  33. Exercicis

  1. El problema: 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.Main

Funciona 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:

  1. Copiar els recursos de src/main/resources a target/classes.
  2. Compilar les proves amb un altre classpath (el de producció més JUnit, AssertJ i Mockito).
  3. Executar les proves invocant el llançador de JUnit a mà.
  4. Empaquetar en un jar amb un MANIFEST.MF correcte.
  5. 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.

  1. 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ú.

  1. 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.

  1. 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.jar
graph 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:

  1. target/ mai no es puja a git. És tot regenerable. El teu .gitignore ha de tenir target/.
  2. 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.

  1. Instal·lació i comprovació

mvn -version
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=false

A 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.

  1. 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:

~/.m2/repository/com/nexussoftware/bibliotech/1.0.0-SNAPSHOT/bibliotech-1.0.0-SNAPSHOT.jar

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)

  1. SNAPSHOT enfront de versió alliberada

La 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:

mvn -U clean install     # -U = update-snapshots

  1. 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}.

  1. El pom.xml complet de BiblioTech

Aquest é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.

  1. 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.

  1. À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:

mvn dependency:build-classpath -Dmdep.outputFile=classpath.txt

  1. 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-aspects

Gairebé 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.

  1. 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.2

Nomé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:

mvn dependency:tree -Dverbose
[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.

  1. 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:

mvn dependency:tree

2. Buscar qui arrossega un artefacte (la pregunta de seguretat d'11-01):

mvn dependency:tree -Dincludes=org.apache.logging.log4j:log4j-core
[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:compile

Aquí 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:

mvn dependency:tree -Dverbose

4. Detectar dependències declarades i no usades, o usades i no declarades:

mvn dependency:analyze
[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:compile

Aquest 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.

  1. exclusions

De 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:

<exclusion>
    <groupId>*</groupId>
    <artifactId>*</artifactId>
</exclusion>

Fes-ho servir amb cura: és fàcil treure alguna cosa que sí que calia i descobrir-ho en execució.

  1. dependencyManagement enfront de dependencies

Dues 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:

  1. Centralitzar versions en un projecte multimòdul: el pare les declara, els mòduls les fan servir sense versió.
  2. 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.
  3. 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.

  1. El BOM i el spring-boot-starter-parent

Un 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:

<properties>
    <jackson.version>2.17.2</jackson.version>   <!-- nom definit pel BOM -->
</properties>

  1. 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 ordre

mvn site genera un lloc web amb informes del projecte. Es fa servir poc avui; la majoria dels equips fan servir altres eines d'informes.

  1. 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.

  1. 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'herencia

Aquest ú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.

  1. 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.

  1. 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.

  1. Empaquetar: maven-jar-plugin, shade i spring-boot-maven-plugin

Tres 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:

java -cp "target/bibliotech.jar:lib/*" com.nexussoftware.bibliotech.BiblioTechApplication

É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:

mvn exec:java -Dexec.mainClass="com.nexussoftware.bibliotech.EinaImportacio"

Útil per a utilitats i scripts de manteniment dins del projecte.

  1. 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:

mvn help:active-profiles

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).

  1. 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 tots

El 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 depen

El 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.

  1. settings.xml i les credencials

Mentre 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 a settings.xml, i preferiblement llegides de variables d'entorn com a l'exemple. Maven ofereix mvn --encrypt-password per 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.

  1. 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.

mvn wrapper:wrapper -Dmaven=3.9.8

Genera:

mvnw                                     <- script Unix
mvnw.cmd                                 <- script Windows
.mvn/wrapper/maven-wrapper.properties    <- la versio declarada

I a partir d'aquí:

./mvnw clean verify        # en comptes de mvn clean verify
./mvnw spring-boot:run

Avantatges:

  1. Tothom fa servir la mateixa versió, sense excepció.
  2. No cal instal·lar Maven per compilar el projecte. Només Java.
  3. La versió és a git, així que canviar-la és un commit revisable.
  4. 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.

  1. 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:

  • -DskipTests enfront 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 1C pot 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:

mvn versions:display-dependency-updates
[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.3

Executar-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.

  1. 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ó.

mvn versions:display-plugin-updates   # detecta plugins sense versio fixada

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.

  1. 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 (Kotlin DSL)
implementation("org.springframework.boot:spring-boot-starter-data-jpa")

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:

  1. És el majoritari en back-end empresarial Java: el que et trobaràs.
  2. És més fàcil d'aprendre: no cal aprendre un llenguatge a més de l'eina.
  3. És predictible: el cicle de vida és sempre el mateix, i això és exactament el que vols quan estàs aprenent la resta.
  4. 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.

  1. 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.yml

El .gitignore mínim:

target/
!.mvn/wrapper/maven-wrapper.jar
*.iml
.idea/
.classpath
.project
.settings/

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-updates

Sortida 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 s

En 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.

  1. 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.

  1. 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:compile

Es demana:

  1. Explica per què l'error apareix en execució i no en compilar.
  2. Aplica la regla de resolució: quina versió guanya i per què?
  3. Dóna tres solucions diferents amb el seu XML, indicant avantatges i inconvenients.
  4. Tria'n una i justifica-la.
  5. 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è:

  1. mvn test executi només les unitàries (ràpid, per a desenvolupament).
  2. mvn verify executi unitàries i integració.
  3. Les d'integració es diguin *IT.java i estiguin etiquetades amb @Tag("integracio").
  4. Existeixi un perfil ci que a més generi l'informe de cobertura amb JaCoCo i falli si la cobertura d'instruccions baixa del 70 %.
  5. Existeixi un perfil rapid que salti les proves d'integració fins i tot a mvn verify.
  6. 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ó:

mvn dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind -Dverbose
[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 *IT i per etiqueta). És redundant a propòsit: si algú oblida el sufix IT però 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.
  • check a la fase verify. 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

Mòdul 2: Flux de control

Mòdul 3: Programació orientada a objectes

Mòdul 4: Programació orientada a objectes avançada

Mòdul 5: Estructures de dades i col·leccions

Mòdul 6: Gestió d'excepcions

Mòdul 7: Entrada/sortida de fitxers

Mòdul 8: Multifil i concurrència

Mòdul 9: Xarxes

Mòdul 10: Temes avançats

Mòdul 11: Frameworks i llibreries de Java

Mòdul 12: Construcció d'aplicacions del món real

© Copyright 2026. Tots els drets reservats