Introduzione

Con questo articolo si prosegue nella descrizione dei principali componenti di Maven avviata nell'articolo precedente. In particolare, l'attenzione è focalizzata su uno degli elementi fondamentali di Maven: la descrizione dichiarativa del progetto contenuta nel file pom.xml. POM è l'acronimo di Modello a Oggetti del Progetto (Project Object Model) e contiene i meta-dati del progetto stesso memorizzati utilizzando il formato XML. A differenza dei file di build Ant, contenenti la descrizione in termini procedurali dei vari processi (build, deployment, etc.), la filosofia del file POM è drasticamente differente e prevede la sola descrizione del progetto: i meta-dati. Questa filosofia di base, tuttavia, può essere alterata attraverso l'inclusione di comandi Ant. La struttura del file POM è molto ricca e non sempre immediata o intuitiva (tanto che anche la relativa descrizione, per motivi di spazio, è stata sviluppata nell'arco di due articoli, vale a dire il presente e la prossima parte IV). Tuttavia ciò non deve spaventare, sia perché non è necessario per ogni progetto definire tutte le sezioni, sia perché Maven fornisce una serie di comportamenti di default sufficienti per la maggior parte dei progetti. Inoltre, una volta preparati i vari file POM per il primo progetto, questi si prestano a essere riutilizzati per i successivi, con minime variazioni. La ricchezza del file POM rappresenta un punto di forza di Maven in quanto offre tantissimi punti in cui è possibile alterarne il comportamento di default. Questa elevata versatilità, se da un lato può presentare problemi di comprensione, dall'altro dovrebbe conferire anche una certa sicurezza di utilizzo: è difficile trovarsi di fronte a scenari di build non previsti, o di fronte ad integrazioni/problematiche non gestibili dalla struttura del POM. Ciò evita di di dover ricorrere a script complicati e difficili da mantenere, come spesso avveniva in passato. Ricordarsi sempre che i vari elementi permettono di dichiarare il comportamento desiderato da Maven e che le diverse sezioni, come per esempio le coordinate, sono ripetute in diversi elementi con significati differenti dipendenti dal contesto.

Struttura

Il diagramma di figura 1 illustra la struttura concettuale del file pom.xml la cui implementazione è riportata nel listato che segue.

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>...</groupId>
  <artifactId>...</artifactId>
  <version>...</version>
  <packaging>...</packaging>
  <parent>...</parent>
  <dependencyManagement>...</dependencyManagement>
  <dependencies>...</dependencies>
  <modules>...</modules>
  <name>...</name>
  <description>...</description>
  <url>...</url>
  <inceptionYear>...</inceptionYear>
  <licenses>...</licenses>
  <developers>...</developers>
  <contributors>...</contributors>
  <organization>...</organization>
  <properties>...</properties>
  <build>...</build>
  <reporting>...</reporting>
  <!-- Environment Information -->
  <issueManagement>...</issueManagement>
  <ciManagement>...</ciManagement>
  <mailingLists>...</mailingLists>
  <scm>...</scm>
  <prerequisites>...</prerequisites>
  <repositories>...</repositories>
  <pluginRepositories>...</pluginRepositories>
  <distributionManagement>...</distributionManagement>
  <profiles>...</profiles>
</project>
Figura 1 - Struttura logica del file POM.xml. Gli elementi sono stati raggruppati secondo un ordine logico che non sempre rispetta quello di apparizione all'interno del file POM.
Figura 1 - Struttura logica del file POM.xml. Gli elementi sono stati raggruppati secondo un ordine logico che non sempre rispetta quello di apparizione all'interno del file POM.

Dall'analisi del diagramma è possibile evidenziare una delle maggiori differenze con i file build.xml ant: il file pom.xml contiene la dichiarazione del progetto e non le azioni da eseguire durante per il processo di build (ossia le procedure). Sebbene, sia possibile includere nei file pom.xml una serie di maven-antrun-plugin che permettono di eseguire task Ant. I file pom.xml definiscono "chi", "cosa" e "dove", mentre i file di build si limitano al "quando" e al "come"

Come si può notare il file POM, in prima analisi, può essere suddiviso in cinque macro componenti, come segue. POM relationships. Questa parte permette di organizzare i progetti attraverso una serie di file POM opportunamente relazionati. In particolare, le relazioni disponibili sono: dipendenza, ereditarietà e aggregazione. build settings. In questa sezione sono definite le varie informazioni richieste dal processo di build, come, per esempio, una serie di proprietà, il goal, le varie directory, i plug-in da utilizzare incluse le relative relazioni, le risorse da utilizzare. project information. Questa sezione è dedicata a eventuali informazioni supplementari che è possibile definire per il progetto. In particolare, in questa sezione trovano posto informazioni come il nome del progetto, la descrizione, l'URL, le persone che vi hanno preso parte, etc. build environment. Questo elemento contiene le varie informazioni relative all'ambiente di build, come le impostazioni del software utilizzato per gestire i problemi, di quello per la integrazione continua, per la gestione della configurazione, etc.; Maven environment. Questa sezione è demandata alla configurazione dell'ambiente Maven, pertanto è possibile impostare i vari repository, la gestione della distribuzione, i profili utilizzati per modificare il comportamento di Maven in situazioni ben definite, e così via.

Le relazioni tra file POM (POM relationships)

Figura 2 - Rappresentazione grafica della sezione delle dipendenze.
Figura 2 - Rappresentazione grafica della sezione delle dipendenze.

Coordinates (le coordinate)

I campi assolutamente obbligatori di un POM sono: groupId (identificatore del gruppo), artifactId (identificatore del manufatto) e version (versione), necessari per identificare univocamente un file di distribuzione. Di questi, i primi due possono essere ereditati da un POM genitore. Il groupId identifica univocamente un insieme di progetti appartamenti alla stessa organizzazione, team, etc. Per esempio il core Maven dichiara la seguente stringa per il groupId: org.apache.Maven. La notazione con il punto, quantunque sia un'ottima convenzione, non è né obbligatoria né tanto meno deve corrispondere alla struttura in package del progetto. Tuttavia, una volta memorizzato il file di distribuzione nel repository, il groupId si comporta come i package Java: rappresenta il percorso relativo all'interno del repository a tal fine il punto viene sostituito dallo barretta obliqua dipendente dal sistema operativo sottostante ("/" in UNIX). Il campo artifactId indica il nome del progetto. La combinazione con il campo precedente genera il nome univoco del progetto tra l'insieme di tutti i progetti (in maniera del tutto analogo alle classi Java) a netto della versione. Il campo version è l'ultima parte necessaria per completare il nome. In particolare, indica una specifica versione del progetto. Questo campo dovrebbe essere mantenuto sincronizzato con le variazioni che avvengono nel sistema di versionamento che gestisce i sorgenti del progetto. Inoltre, è utilizzato nel repository dei manufatti (tipcamente file jar) per mantenerli separati (diversi sistemi, per esempio, potrebbero utilizzare diverse versioni di una stessa libreria).

Dependencies (dipendenze)

Le coordinate di un progetto sono un elemento molto importante poiché costituiscono il criterio utilizzato da Maven per referenziare i vari progetti. Queste, per esempio sono necessarie per gestire il grafo delle dipendenze. Area che tradizionalmente ha creato non pochi problemi nell'elaborazione degli script di build. In effetti, la quasi totalità dei progetti, per poter essere costruiti (build) e eseguiti correttamente, necessita di includere opportune librerie sia appartenenti al progetto stesso (sotto-progetti), sia fornite da terze parti. La sincronizzazione delle varie librerie, il prelievo della versione corretta , la relativa distribuzione sono aspetti che da sempre hanno complicato il processo di build dei vari progetti. Maven è in grado di gestire queste problematiche attraverso le liste di dipendenze, eseguendo il download automatico dei manufatti richiesti ed effettuando i necessari collegamenti durante le varie fasi. Maven, inoltre, risolve automaticamente eventuali dipendenze transitive, ossia esegue la stessa procedura anche per eventuali dipendenze della libreria presente nella lista delle dipendenze del progetto in esame. Il listato seguente mostra un esempio ricorrente di dipendenze: l'inclusione del librarie di test junit e di log log4j.

<dependencies>
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.0</version>
    <type>jar</type>
    <scope>test</scope>
    <optional>true</optional>
  </dependency>
  <dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.6</version>
    <type>jar</type>
  </dependency>
</dependencies>

Il campo scope (opzionale) prevede i seguenti cinque valori

  • compile: valore di default, prevede che il file incluso sia disponibile per tutti i percorsi.
  • provided: è molto simile al precedente, tuttavia asserisce che il file è fornito da un'altra entità, come il JDK o un container. È disponibile solo per il classpath di compilazione e non è transitivo.
  • runtime: indica che la dipendenza non è richiesta a tempo di compilazione, ma solo in esecuzione. Pertanto è disponibile solo per i percorsi di runtime e di test.
  • test: specifica che la dipendenza non è richiesta per un uso tipico, ma solo per il test del progetto.
  • system: è molto simile a provided con l'eccezione che è necessario fornire esplicitamente il jar che contiene la dipendenza. Pertanto, il relativo file è sempre disponibile e non è necessario eseguire la ricerca nel repository.

Il campo optional dichiara opzionale (quindi non necessaria) una dipendenza quando il progetto è a sua volta dichiarato nella lista delle dipendenze di un altro (tipicamente, le dipendenze transitive sono dichiarate opzionali). Nel listatino 2, il JUnit è dichiarato opzionale in quanto, qualora il relativo progetto dovesse apparire nella lista delle dipendenze di un altro, non è necessario includerlo. Un altro modo di ottenere un risultato simile consiste nel dichiarare le esclusioni all'interno della sezione delle dipendenze. Ciò fa sì che i progetti dichiarati, contenuti all'interno della dipendenza, non siano inclusi. Pertanto si ottiene l'effetto di escludere specifiche dipendenze transitive.

Inheritance (ereditarietà)

Un'altra caratteristica molto potente e unica di Maven è la possibilità di relazionare tra loro i progetti con legami di ereditarietà, un po' come avviene con le classi Java. Sebbene framework come Ant permettano di simulare, in qualche misura, la relazione di ereditarietà, i file POM di Maven prevedono nativamente la possibilità di dichiarare tale relazione. I progetti genitori e quelli esclusivamente aggregabili devono specificare il valore POM all'interno del campo packaging. Questo campo serve per indicare la natura del manufatto (file di distribuzione) prodotto. Per default, Maven suppone che si voglia generare un file JAR. Altre alternative sono: POM (questo valore è riservato per progetti "genitore" e per quelli aggregabili), war, ear, Maven-plugin, ejb, ear, rar e par. Gli elementi ereditabili dai discendenti che si possono specificare in un POM genitore sono:

  • dependencies (dipendenze)
  • developers and contributors (lista degli sviluppatori e contributori)
  • plugin lists (elenchi dei plugin)
  • reports lists (elenchi dei reports)
  • plugin executions with matching ids (esecuzioni dei plugin con identificatori corrispondenti)
  • plugin configuration (configurazione dei plugin)

POM genitori sono identificati da un'apposita sezione da includere nel progetto, denominata parent. Questa, oltre a includere le usuali informazioni (groupId, artifactId, version), dispone di un ulteriore campo, non obbligatorio, denominato relativePath. Questo indica il path in cui cercare il progetto genitore specificato, prima di accedere al repository locale e a quello remoto. Qualora in un file POM.xml si intenda ereditare da un altro file, le relative coordinate vanno specificate all'interno della sezione <parent> ... </parent>. A tal proposito, vediamo questo esempio di relazione di eredità. Si noti che il file in questione non riporta i campi groupId e version in quanto ereditati dal POM genitore.

<project>
  <modelVersion>4.0.2</modelVersion>
  <parent>
    <groupId>com.mokabyte.finance</groupId>
    <artifactId>frontoffice</artifactId>
    <version>2</version>
  </parent>
  <artifactId>PriceBlotter</artifactId>
</project>
<modules>
  <module>client-system-endpoint</module>
  <module>admin-dto</module>
  <module>admin-gui</module>
  <module>client-api</module>
  <module>server</module>
</modules>

Proprietà del POM

Le proprietà possono essere referenziate in vari modi:

  • env.<proprietà>: il prefisso env fa riferimento alle variabili di ambiente, come per esempio il percorso (${env.PATH})
  • project.<proprietà>: in questo caso il suffisso project permette di accedere agli elementi del file POM. Come lecito attendersi, la notazione da utilizzare è quella con il punto. Per esempio, la proprietà ${project.version} si riferisce all'elemento <project><version>
  • settings.<proprietà>: consistentemente con il caso precedente, anche questa notazione permette di accedere agli elementi all'interno di un file di configurazione di Maven. In questo contesto, tuttavia, il file acceduto è settings.xml. Per esempio, la proprietà ${settings.offline} si riferisce all'elemento <settings><offline>
  • java.<proprietà>: permette di accedere alle proprietà di sistema messe a disposizione dalla API java.lang.System.getProperties. Per esempio, ${java.home}
  • <proprietà>: fa riferimento a valori specificati nella struttura <properties> ... </properties>

Build settings

<build>...</build>
<profiles>
  <profile>
    <!-- "Profile Build" contains a subset of "Project Build"s elements -->
    <build>...</build>
  </profile>
</profiles>
<build>
  <defaultGoal>install</defaultGoal>
  <directory>${basedir}/target</directory>
  <finalName>${artifactId}-${version}</finalName>
  <filters>
    <filter>filters/filter1.properties</filter>
  </filters>
  ...
</build>
<build>
  ...
  <resources>
    <resource>
      <targetPath>META-INF/mokabyte</targetPath>
      <filtering>false</filtering>
      <directory>${basedir}/src/main/mokabyte</directory>
      <includes>
        <include>configuration.xml</include>
      </includes>
      <excludes>
        <exclude>**/*.properties</exclude>
      </excludes>
    </resource>
  </resources>
  <testResources>
    ...
  </testResources>
  ...
</build>
<plugins>
  <plugin>
    <groupId>org.apache.Maven.plugins</groupId>
    <artifactId>Maven-compiler-plugin</artifactId>
    <configuration>
      <source>1.5</source>
      <target>1.5</target>
    </configuration>
  </plugin>
</plugins>
<build>
  <plugins>
    <plugin>
      <artifactId>Maven-antrun-plugin</artifactId>
      <executions>
        <execution>
          <id>echodir</id>
          <goals>
            <goal>run</goal>
          </goals>
          <phase>verify</phase>
          <inherited>false</inherited>
          <configuration>
            <tasks>
              <echo>Build Dir: ${project.build.directory}</echo>
            </tasks>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>
<build>
  ...
  <pluginManagement>
    <plugins>
      <plugin>
        <groupId>org.apache.Maven.plugins</groupId>
        <artifactId>Maven-jar-plugin</artifactId>
        <version>2.0</version>
        <executions>
          <execution>
            <id>pre-process-classes</id>
            <phase>compile</phase>
            <goals>
              <goal>jar</goal>
            </goals>
            <configuration>
              <classifier>pre-process</classifier>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </pluginManagement>
  ...
</build>
<plugins>
  <plugin>
    <groupId>org.apache.Maven.plugins</groupId>
    <artifactId>Maven-jar-plugin</artifactId>
  </plugin>
</plugins>

Gli elementi presi in considerazione da Maven possono essere suddivisi in due grandi categorie: file sorgenti e compilati. Per quanto riguarda la prima categoria, è possibile indicare le directory dei file sorgente Java, scripts, e test. Per quanto riguarda i file compilati, invece, è possibile dichiarare le directory delle classi (.class) dei sorgenti e dei test. In termini del POM questi elementi sono contenuti prossimo listato. I percorsi possono essere specificati sia in termini assoluti (non raccomandabile), sia in modo relativo a partire dalla directory di base (proprietà ${basedir}). Come descritto in precedenza, questa intera sezione può essere omessa qualora si accetti la configurazione di default di Maven.

<project>
  <build>
    <sourceDirectory>${basedir}/src/main/java</sourceDirectory>
    <scriptSourceDirectory>${basedir}/src/main/scripts</scriptSourceDirectory>
    <testSourceDirectory>${basedir}/src/test/java</testSourceDirectory>
    <outputDirectory>${basedir}/target/classes</outputDirectory>
    <testOutputDirectory>${basedir}/target/test-classes</testOutputDirectory>
    ...
  </build>
</project>
<extensions>
  <extension>
    <groupId>org.apache.Maven.wagon</groupId>
    <artifactId>wagon-ftp</artifactId>
    <version>1.0-alpha-3</version>
  </extension>
</extensions>
<project>
  ...
  <reporting>
    <plugins>
      <plugin>
        <outputDirectory>${basedir}/target/site</outputDirectory>
        <artifactId>Maven-project-info-reports-plugin</artifactId>
        <reportSets>
          <reportSet></reportSet>
        </reportSets>
      </plugin>
    </plugins>
  </reporting>
  ...
</project>