Aller au contenu

Systèmes de Construction (Les bases)

Dans cette unité, nous aborderons les bases des systèmes de construction et illustrerons leur fonctionnement à l'exemple de Maven. Nous commencerons par une brève récapitulation de la compilation de langages, notamment dans le contexte du langage de programmation Java, puis nous examinerons les défis associés à l'assemblage des binaires. Enfin, nous examinerons les caractéristiques principales de Maven, notamment la gestion des dépendances, et nous ferons nos premiers essais pour personnaliser le comportement du système de construction à l'aide d'un fichier de configuration.

Résumé de la lecture

Les systèmes de construction ont un seul but : s'assurer que votre code source peut être traduit de manière fiable en un produit utilisable. Bien que cela puisse sembler simple, ce n'est pas du tout une tâche facile. Les systèmes de construction sont un moyen puissant et hautement configurable d'apporter de l'ordre et de la fiabilité dans le chemin allant du code source au produit.

Rappel: Packages, imports et le classpath

Pour les débutants, l’instruction import est souvent perçue comme « quelque chose qu’il faut écrire au début de la classe pour que le message d’erreur disparaisse ».
Pour répondre à la question « pourquoi avons-nous besoin des imports ? » (ne serait-il pas plus simple si tout était importé automatiquement ?), nous allons examiner les principes de base suivants :

  • Comment le contexte du code est structuré en packages Java, et pourquoi c’est important.
  • Pourquoi vous devez importer certaines classes mais pas d’autres, et les exceptions à cette règle.
  • Pourquoi il n’est pas conseillé d’importer plus que le strict nécessaire.

Rappel sur les packages

  • Java organise le code en packages
    • Les packages fournissent un contexte pour un ensemble de classes connexes.
    • Exemple : Mouse.class peut être une classe imitant le comportement d’une souris biologique, ou bien une classe gérant un périphérique d’entrée humain.
    • Les packages environnants animals et devices permettent d’éliminer l’ambiguïté.
  • Par défaut, vous ne pouvez accéder qu’aux classes définies dans le même package (ou dans java.lang).
    • Exemple : Lorsqu’on travaille dans le package animals, l’instruction new Mouse() n’est pas ambiguë — elle créera toujours la classe imitant le comportement d’une souris biologique.
---
title: Example of ambigous context
---
classDiagram
    namespace animal {
        class Mouse {
            +String species
            +void squeak()
        }
    }

    namespace devices {
        class Ꮇouse {
            +String brand
            +void click()
            +void scroll()
        }
    }

Importations

  • Par défaut, il n’est pas possible d’instancier (ou même d’appeler) des classes situées à l’extérieur de votre package actuel.
    • Cela vous protège contre les ambiguïtés accidentelles.
    • Exemple : Lorsque vous travaillez à l’extérieur des packages animals et devices, que devrait-il se passer si vous créez un new Mouse() ? Il n’est pas clair de quelle classe il s’agit.
  • Les importations vous permettent d’étendre le contexte de votre package courant en y ajoutant des classes supplémentaires :
    • import animals.Mouse vous permettra d’utiliser le modèle de la souris biologique (new Mouse()), mais pas celui du périphérique d’entrée humain.
classDiagram
    namespace animals {
        class Mouse {
            +String species
            +void squeak()
        }
    }

    namespace devices {
        class Ꮇouse {
            +String brand
            +void click()
            +void scroll()
        }
    }

    class Main

    Mouse <.. Main: import

Illustration de Main utilisant une instruction import animals.Mouse.

Pourquoi les packages et les imports sont-ils des antagonistes sémantiques ?

Les packages définissent des frontières de contexte, tandis que les imports étendent ces frontières existantes.

Qu’en est-il de String

Il existe certaines classes qui n’ont pas besoin d’être importées, c’est-à-dire que vous pouvez les utiliser dans votre code sans ajouter explicitement une instruction import, même si elles ne font pas partie du package courant.

Un exemple est String. Lorsque vous avez écrit votre premier programme « HelloWorld », vous avez défini une chaîne de caractères et appelé la classe System, mais vous n’avez eu besoin d’aucune importation :

// No import for String required here...

public class HelloWorld
{
  public static void main(String[] args)
  {
    System.out.println("Hello, world!");
  }
}

Mais nous venons tout juste d’apprendre que l’appel à une classe externe nécessite une importation. Que se passe-t-il ici ?

Il existe une exception à la règle

java.lang.* est importé automatiquement. Les classes de ce package sont tellement courantes que toutes sont accessibles par défaut : String, System, Math, etc.

L’astérisque

Les importations ne sont pas nécessairement limitées à une seule classe : vous pouvez aussi utiliser le caractère générique * pour importer tout un package.

  • ca.quam.mgl7010.animals.* importera tous les éléments présents dans le package.
  • Cependant, il est généralement déconseillé d’utiliser les caractères génériques.
Que pourrait-il arriver en utilisant un caractère générique (*) ?

Les caractères génériques importent tout le contenu d’un package. Que se passe-t-il si le contenu du package change (par exemple, si une nouvelle classe y est ajoutée) et qu’un conflit survient avec quelque chose que vous importiez déjà ? Il vaut mieux importer uniquement ce dont vous avez réellement besoin — évitez les importations inutiles.

Limitations du classpath

  • L’instruction import permet d’accéder à des classes connues de la JVM, qui seraient autrement hors contexte.
    • « Connue de la JVM » signifie : une classe présente sur le classpath (une variable qui indique tous les répertoires que la JVM parcourt pour trouver des classes).

Classpath et importations

Vous ne pouvez importer que ce qui se trouve sur le classpath. Si vous trouvez une bibliothèque utile sur Internet, vous ne pouvez pas simplement l’importer sans l’avoir d’abord téléchargée et ajoutée à votre classpath. La JVM ne peut pas explorer Internet pour vous !

Livraisons logicielles

Dans la plupart des cas, votre client s’intéresse peu à votre code source, mais plutôt à un fichier exécutable unique. Pour lui, peu importe comment votre programme fonctionne ; l’important est qu’il puisse facilement l’exécuter.

  • Peu pratique :
    • Devoir installer le Java Development Kit.
    • Devoir installer un IDE.
    • Devoir compiler lui-même les sources.
    • Devoir se souvenir d’une commande pour lancer votre code.
  • Pratique :
    • Un seul fichier sur lequel le client peut simplement double-cliquer, et votre magnifique programme démarre.

Java propose un format de fichier dédié exactement à cet usage : les fichiers JAR, ou fichiers « Java ARchive ».

Fichiers JAR

  • Les JAR sont des fichiers zip.
  • Ils peuvent contenir tout ce que vous y mettez. Pour créer une version livrable, vous devriez ajouter :
    • Tout le bytecode : (*class fichiers, CAFEBABE…)
    • Un manifeste indiquant (entre autres) quelle classe sert de lanceur.
  • Si vous ajoutez ces éléments, une JVM peut directement consommer (interpréter et exécuter) le fichier JAR.
    • Votre client n’a besoin que d’une JVM, pas d’un environnement complet de développement Java.
    • Comme le JAR contient du bytecode, il fonctionnera sur n’importe quelle plateforme.

Créer une version JAR

  • Commençons avec un programme simple :

    public class HelloWorld {
      public static void main(String[] args) {
        System.out.println("Bonjour, INF2050!");
      }
    }
    

  • Créer un fichier JAR à partir des sources est relativement simple :

    # Compiler tous les fichiers java en fichiers *.class, et les placer dans un nouveau répertoire build
    javac -d ./build *java
    
    # Entrer dans le répertoire build
    cd build
    
    # Créer une archive Java (JAR) à partir de tous les fichiers *.class.
    # Ajouter un MANIFEST.MF pointant vers HelloWorld comme classe de lancement.
    jar cfe MyDeliverable.jar HelloWorld *class
    

  • Cela produit un fichier JAR : MyDeliverable.jar

    • Contenu :

      MyDeliverable.jar
       ├── HelloWorld.class
       └── META-INF
           └── MANIFEST.MF
      

    • Avec le contenu du fichier MANIFEST.MF :

      Manifest-Version: 1.0
      Created-By: 22.0.2 (Oracle Corporation)
      Main-Class: HelloWorld
      

  • Le fichier JAR peut être exécuté directement à l’aide de la JVM : java -jar MyDeliverable.jar

JAR pour d’autres usages

  • Les fichiers JAR ne sont pas nécessairement des applications autonomes.
  • Comme le format JAR n’est qu’un conteneur zip, vous pouvez aussi y regrouper du code sous forme de bibliothèque. Cela signifie :
    • Le JAR n’est pas un programme, mais un ensemble de fonctionnalités utiles ou d’interfaces.
    • Le JAR n’a pas besoin de spécifier une classe de lancement dans son manifeste, puisqu’il n’est pas destiné à être exécuté.
    • Le JAR peut aussi contenir, de façon optionnelle, de la documentation et les fichiers source, afin que les utilisateurs puissent comprendre son fonctionnement interne.

De façon générale :

  • Lorsque vous construisez une version livrable pour un client, seul le comportement compte. Vous ajoutez uniquement le bytecode et un manifeste précisant la classe de lancement.
  • Lorsque vous construisez une bibliothèque pour d’autres développeurs, le comportement et le fonctionnement interne comptent. Vous ajoutez aussi le code source et la documentation.

Récapitulatif

Les fichiers JAR ne sont que des conteneurs. Ce qu’on y inclut dépend du public cible.

Publications logicielles avec Maven

On pourrait soutenir que sélectionner manuellement des fichiers et les regrouper dans un JAR est quelque peu peu pratique.

Idéalement :

  • Créer une nouvelle version est rapide et pratique
  • Créer une nouvelle version est fiable

C’est l’une des motivations clés des systèmes de build : passer rapidement, facilement et de manière fiable des sources du projet à quelque chose qui peut être livré au client.

Dans ce qui suit, nous allons examiner comment le processus de build est réalisé à l’aide de l’outil de build « Maven ».

Structure d’un projet Maven

Avant de commencer, nous devons respecter quelques contraintes imposées par Maven :

  • Les projets Maven imposent une structure interne spécifique, légèrement différente de celle des projets Java standards.
  • Heureusement, nous n’avons pas besoin de créer manuellement la structure initiale du projet, mais pouvons utiliser Maven pour initialiser nos projets :
mvn archetype:generate \
-DgroupId=ca.uqam.info \
-DartifactId=MavenHelloWorld \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false

Note: Certains systèmes (Windows) ne peuvent pas gérer les commandes sur plusieurs lignes. Supprimez les et placez tout sur une seule ligne.

Décomposons la commande ci-dessus :

  • archetype signifie « nous voulons utiliser un modèle de projet »
    • Il existe différents archetypes pour différents usages. Par exemple, pour une application web ou un backend serveur, nous aurions utilisé un archetypeArtifactId différent.
  • Comme pour toute dépendance dont vous pourriez avoir besoin, votre propre logiciel doit avoir un identifiant unique. D’autres développeurs pourraient en effet utiliser votre logiciel comme bibliothèque !
    • groupId représente une chaîne spécifique à une organisation, généralement le nom de domaine inversé de l’entreprise pour laquelle vous travaillez. Comme nous sommes tous au département d’informatique de l’UQAM nous utilisons ca.uqam.info
    • artifactId désigne le logiciel que vous construisez. Il doit être un nom descriptif, indiquant ce que fait votre logiciel.

Une fois la commande exécutée, la structure de dossiers et de fichiers suivante sera créée :

MavenHelloWorld/
├── pom.xml
└── src
    ├── main
    │   └── java
    │        └── ca
    │            └── uqam
    │                └── info
    │                    └── App.java
    └── test
        └── java
            └── ca
                └── uqam
                    └── info
                        └── AppTest.java

12 répertoires, 3 fichiers

Pour l’instant, nous ne nous intéressons qu’au pom.xml et au fichier de classe initial App.java. Nous traiterons des tests dans une conférence ultérieure.

Classe App initiale

Le fichier pom initial n’est qu’une classe HelloWorld de base :

package ca.uqam.info;

/**
 * Hello world!
 *
 */
public class App {
  public static void main(String[] args) {
    System.out.println("Hello World!");
  }
}

Package structures

Notice how the initial groupId argument has affected to project's package naming and internal folder structure ?

Initial pom file

The initial pom file looks, as created by the as follows:

<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 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>ca.uqam.info</groupId>
    <artifactId>MavenHelloWorld</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>MavenHelloWorld</name>
    <url>http://maven.apache.org</url>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Nous voyons déjà une première entrée de dépendance, à savoir junit.

  • Dans l’esprit d’un bon développement logiciel, Maven suppose que nous allons tester notre logiciel.
  • Cependant, junit ne fait pas partie de Java standard. Nous avons donc besoin d’un bloc de dépendances.

Quelque chose de particulier dans le bloc de dépendances ?

Le bloc de dépendances junit comporte en réalité une entrée supplémentaire <scope>test</scope>. Cela s’explique par le fait que Maven fait la distinction entre les dépendances nécessaires pour compiler un logiciel et celles nécessaires pour exécuter un logiciel. Junit n’est pas nécessaire à l’exécution, donc Maven a ajouté un tag de scope test supplémentaire.

Compilation avec Maven

Utilisons Maven pour compiler le projet, c’est-à-dire créer le bytecode Java. La commande correspondante est mvn package.

  • La première fois que vous exécutez mvn package, nous verrons comment Maven télécharge junit.

    • Il y aura quelques messages de journalisation : text ... Downloading from central: https://repo.maven.apache.org/maven2/org/apache /maven/surefire/common-java5/3.2.5/common-java5-3.2.5.pom Downloaded from central: https://repo.maven.apache.org/maven2/org/apache /maven/surefire/common-java5/3.2.5/common-java5-3.2.5.pom (2.8 kB at 156 kB/s) ...
    • Une fois la commande terminée, nous trouverons un nouveau répertoire target, avec le contenu suivant : ```text target/ ├── MavenHelloWorld-1.0-SNAPSHOT.jar ├── classes │ └── ca │ └── uqam │ └── info │ └── App.class ...

    21 répertoires, 10 fichiers ```

  • Parmi d’autres, c’est exactement le même résultat que nous aurions pu créer manuellement, en utilisant le compilateur Java :

    • Un fichier jar
    • Les fichiers de classes de notre code source

package produit un build

Bien que la configuration initiale ait été un peu fastidieuse, un projet n’a besoin d’être configuré qu’une seule fois. À partir de là, nous pouvons produire facilement de nouveaux builds (JAR) avec la commande Maven correspondante mvn clean package.

Exécution des artefacts Maven

L’exécution des artefacts générés est presque identique à celle des binaires créés manuellement.

Fichiers de classes

Nous pouvons exécuter sans problème les fichiers de classes générés. Notez toutefois que nous devons nous trouver à la racine de la structure des packages pour appeler notre programme :

  • Exécution du programme App.class depuis un mauvais emplacement :
    $ cd target/classes/ca/uqam/info; java App
    Error: Could not find or load main class App
    Caused by: java.lang.NoClassDefFoundError: App
    (wrong name: ca/uqam/info/App)
    
  • Exécution du programme App.class depuis la racine de la structure des packages :
    $ cd target/classes/
    $ tree
    .
    └── ca
        └── uqam
            └── info
                └── App.class
    $ java ca/uqam/info/App
    Hello World!
    

Fichiers Jar

L’exécution du fichier jar n’est pas possible sans spécifier la classe principale, car par défaut le manifest ne contient pas de référence à la classe de lancement.

  • Tentative d’exécution du fichier jar sans arguments :
$ cd target; java -jar MavenHelloWorld-1.0-SNAPSHOT.jar
no main manifest attribute, in MavenHelloWorld-1.0-SNAPSHOT.jar
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.4.1
Build-Jdk-Spec: 22
  • Exécution du fichier jar en spécifiant la classe principale via l’argument classpath :
$ java -cp MavenHelloWorld-1.0-SNAPSHOT.jar ca.uqam.info.App
Hello World!

Note : Maven offre bien sûr un moyen d’intégrer un MANIFEST fonctionnel dans le fichier jar produit. Nous y reviendrons plus tard.

Un build propre

Le répertoire target accumule tous les artefacts jamais construits. Si vous modifiez votre code ou le pom.xml et que vous reconstruisez, de nouveaux fichiers peuvent être ajoutés et il peut être difficile de distinguer les anciens des nouveaux fichiers. Une bonne astuce est d’utiliser toujours l’argument clean avant de compiler, ce qui efface entièrement le répertoire target : Construisez votre projet systématiquement avec **mvn clean package**

Dépendances

La plupart du temps, vous ne voulez pas tout programmer depuis le début ( voir leçon précédente sur le développement orienté vers la réutilisation)

Exemple JSON

  • Nous allons maintenant examiner comment la compilation et l'exécution changent lorsque des bibliothèques supplémentaires sont impliquées.
  • Imaginez que nous voulons sérialiser (créer une représentation en chaîne de caractères lisible par machine) d'un objet Java :

    class Student {
      private final int age;
      private final String firstName;
      private final String lastName;
    
      public Student(int age, String firstName, String lastName) {
        this.age = age;
        this.firstName = firstName;
        this.lastName = lastName;
      }
    
      //... et les getters
    }
    

  • Un objet étudiant, créé avec new Student(34, "Maximilian", "Schiedermeier"), devrait être sérialisé en :

    {
      "age": 34,
      "firstName": "Maximilian",
      "lastName": "Schiedermeier"
    }
    

Création manuelle de chaîne

  • Bien sûr, je pourrais construire manuellement une chaîne JSON :

    // Créer un étudiant
    Student myStudent = new Student(34, "Maximilian", "Schiedermeier");
    
    // Exporter l'étudiant
    String jsonString =
        "{\n"
            + "\t\"age\": " + myStudent.getAge()
            + ",\n\t\"firstName\": \"" + myStudent.getFirstName()
            + "\", \n\t\"lastName\": \"" + myStudent.getLastName()
            + "\"\n}";
    System.out.println(jsonString);
    

  • Mais que faire si je dois exporter un autre objet ? Que se passe-t-il si la structure de l'objet change ?

Utiliser une bibliothèque

  • Il serait beaucoup plus facile de réutiliser la bibliothèque Google GSON existante :

    import com.google.gson.Gson;
    
    class MainWithGson {
    
      public static void main(String[] args) {
    
        // Créer un étudiant
        Student myStudent = new Student(34, "Maximilian", "Schiedermeier");
    
        // Exporter l'étudiant
        String jsonString = new Gson().toJson(myStudent);
        System.out.println(jsonString);
      }
    }
    

  • Cependant, nous utilisons maintenant du code qui n'est pas le nôtre, et le compilateur, ainsi que le JDK, doivent connaître cette dépendance.

    • Télécharger le fichier JAR de la bibliothèque Gson :
    • Cette fois, nous compilons avec l'argument -cp (classpath), indiquant au compilateur qu'il y a des classes supplémentaires à prendre en compte. javac -cp gson-2.11.0.jar *java
    • De même, lors de l'exécution du bytecode compilé, la JVM doit connaître la bibliothèque GSON : java -cp gson-2.11.0.jar:. MainWithGson
Que pourrait-il mal se passer ?

En réutilisant la bibliothèque Google GSON, nous avons créé une "dépendance". Sans cette bibliothèque à portée de main, notre code ne peut ni être compilé, ni être exécuté.

Gestion des dépendances

La gestion des dépendances vise à éliminer tous les problèmes mentionnés précédemment en spécifiant plutôt quelles dépendances existent (et où les obtenir), plutôt que de gérer manuellement les fichiers JAR.

Essentiellement, les ingrédients pour tout outil de gestion des dépendances sont :

  • Un dépôt en ligne, archivant systématiquement toutes les versions de toutes les bibliothèques
  • Un fichier de configuration local, décrivant pour chaque dépendance :
    • Un identifiant unique, par exemple "Bibliothèque Google GSON"
    • La version spécifique, par exemple "2.11.0"

Avantages :

  • Les fichiers de configuration sont textuels et légers. Ils peuvent être stockés dans le projet lui-même.
  • Les fichiers de configuration sont écrits dans une syntaxe interprétable par machine. Un outil peut collecter toutes les dépendances pour vous et même modifier le classpath si nécessaire.
  • Vous avez une trace claire de toutes les versions exactes des dépendances. Vous pouvez facilement analyser votre projet pour détecter les vulnérabilités de sécurité.
  • Aucun dommage n'est causé si vous perdez un JAR de bibliothèque, vous pouvez facilement le récupérer à partir du dépôt.

Maven

Maven est un système de construction pour Java qui offre exactement ces deux composants :

  • Un dépôt central, contenant presque toutes les bibliothèques Java jamais créées : mavencentral.org
  • Un fichier de configuration de projet qui (entre autres) liste toutes les dépendances du projet : pom.xml
    • POM signifie "Project Object Model"
    • XML est un format de fichier lisible par machine
    • Une dépendance est déclarée comme suit :
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.11.0</version>
</dependency>

Au lieu de télécharger nous-mêmes des fichiers JAR et de les placer dans le classpath, nous demandons à Maven de s'assurer que toutes les dépendances listées sont en place.

Jamais, au grand jamais

Ne jamais, au grand jamais, interférer manuellement avec la gestion des dépendances dans un projet prêt pour Maven. Si vous avez besoin d'une bibliothèque supplémentaire, modifiez le pom.xml, mais ne faites jamais glisser et déposer un fichier JAR dans votre projet, ni modifier le classpath.

Dépôts

Le dépôt local :

  • Maven maintient également un dépôt local sur votre ordinateur, dans le répertoire ~/.m2. Chaque bibliothèque que vous avez utilisée est mise en cache dans ce répertoire.
  • Le dépôt local a deux objectifs :
    • Performance : Il est plus rapide de réutiliser un fichier JAR mis en cache que de le télécharger sur Internet à chaque fois.
    • Mode hors ligne : Vous n'êtes peut-être pas toujours en ligne. Avec les dépendances mises en cache, vous pouvez développer sans connexion Internet.

Dépôts tiers :

  • Vous pourriez rencontrer des situations où vous avez besoin d'une bibliothèque qui n'est pas dans le dépôt central officiel de Maven.
  • Exemples :
    • Bibliothèques qui ne sont pas libres d'utilisation et donc pas accessibles publiquement.
    • Vos propres bibliothèques que vous ne souhaitez pas télécharger.
  • Quiconque peut créer son propre dépôt.
    • Un dépôt en ligne se compose simplement de quelques fichiers accessibles via un serveur web HTTP.
    • Cependant, par défaut, Maven ne connaît pas les dépôts tiers. Si vous souhaitez que Maven recherche dans votre propre dépôt, vous devez modifier le fichier pom.xml et indiquer l'emplacement de votre dépôt tiers.

L'algorithme de résolution des dépendances de Maven

Pour construire un projet, Maven essaie de satisfaire toutes les dépendances avec les artefacts correspondants (les fichiers JAR et certaines métadonnées). Pour satisfaire une dépendance, Maven :

  1. Vérifie d'abord le dépôt local .m2 pour un fichier mis en cache.
  2. Si le fichier n'est pas mis en cache, il vérifie si des dépôts tiers sont définis. (Généralement, aucun n'est défini.)
  3. Contacte les serveurs du dépôt officiel de Maven pour récupérer l'artefact nécessaire.
flowchart LR
  resolve[\Resolve depdendency/]
  resolve --> localcheck{Artifact in local repo ?}
  localcheck -.  yes .-> done([Success])
  localcheck ==>|no| remotecheck{3rd party repo defined ?}
  remotecheck -.  yes .-> 3rdpartycheck{Artifact in 3rd party ?}
  3rdpartycheck -.  yes .-> done
  3rdpartycheck -.  no .-> centralcheck{Artifact in central ?}
  remotecheck ==>|no| centralcheck
  centralcheck ==>|yes| done
  centralcheck -.  no .-> fail([Fail])
Que se passe-t-il lorsqu'un projet est construit pour la deuxième fois ?

Maven aura déjà toutes les dépendances mises en cache. Il prendra le chemin le plus élevé.

Compilation vs exécution

Par défaut, Maven inclut les dépendances uniquement au moment de la compilation, c’est-à-dire que nous ne pouvons pas exécuter le JAR produit sans fournir manuellement toutes les dépendances via les arguments de classpath.

Dans une conférence ultérieure, nous apprendrons comment configurer Maven pour produire un JAR autonome, qui peut être utilisé tel quel.

Le problème avec les JARs

Les JARs sont un moyen simple de partager des fonctionnalités, mais à mesure que les projets se développent, plusieurs problèmes tendent à persister :

  • Plus vous avez de dépendances, plus vous transportez de JARs avec vous.
    • Où stocker les JARs ? Dans le dépôt ? Que faire si vous avez besoin du même JAR dans plusieurs projets, devez-vous les stocker deux fois ?
    • Chaque fois qu'un nouveau développeur rejoint le projet, vous devez lui transmettre tous les JARs et lui faire étendre manuellement son classpath.
    • Compiler votre projet devient quelque peu fastidieux, car vous devez toujours vérifier qu'une longue liste de dépendances est correctement installée.
    • Le client se plaint que votre logiciel ne fonctionne pas. Il est probable qu'il ait négligé d'installer un JAR, ou a installé la mauvaise version. Comment savoir lequel c'est ?
  • Un JAR est un instantané, c'est une version fixe.
    • Que faire si une vulnérabilité de sécurité a été trouvée dans un JAR que vous avez téléchargé. Comment le sauriez-vous ?
    • Vous avez perdu un JAR dont vous avez besoin pour construire votre projet, où le retrouver ? Quelle version était-elle déjà compatible avec votre projet ?

Une véritable histoire d'horreur

Dans un précédent laboratoire de recherche, nous avions un logiciel particulièrement difficile à utiliser. Avant qu'un développeur puisse même écrire une seule ligne de code, il devait passer au moins 30 minutes à 1 heure à configurer manuellement le projet. Le projet avait même des JARs dont personne ne savait exactement d'où ils venaient, s'ils étaient encore nécessaires, ou ce qu'ils apportaient exactement. Il y avait des rumeurs selon lesquelles un stagiaire, qui avait travaillé il y a environ 3 ans, avait créé les JARs. Mais le stagiaire était parti depuis longtemps et personne n'avait ses coordonnées. En même temps, ce étaient des artefacts logiciels volumineux qui alourdaient notre exécutable.
D'innombrables heures de développement ont été gaspillées à cause d'une mauvaise gestion des dépendances.

Plugins Maven

En plus de télécharger et de mettre en cache les dépendances pour une utilisation dans le classpath local, Maven a également un second objectif : Modifier le pipeline de construction.

  • Par défaut, tout ce qui se passe lors de mvn clean package est la compilation standard des fichiers sources (en utilisant toutes les bibliothèques spécifiées pour le processus).
  • Mais la plupart du temps, vous souhaitez faire plus, par exemple produire une documentation lisible par l'homme, exécuter des tests ou créer un artefact de construction avec toutes les dépendances incluses.
  • Le comportement de Maven concernant le pipeline de construction peut être modifié avec des plugins.

Un plugin est un court extrait (ou parfois pas si court) dans une section dédiée plugins du pom.xml. Vous pouvez avoir autant de plugins que vous le souhaitez dans le pom.xml :

<project>
    <build>
        <plugins>
            <!-- First plugin details -->
            <plugin>
                ...
            </plugin>
            <!-- Second plugin details -->
            <plugin>
                ...
            </plugin>
            ...
        </plugins>
    </build>
</project>
  • Chaque plugin a une emplacement par défaut dans le pipeline de construction, car la plupart des tâches n'ont de sens qu'à un moment donné du processus.
  • Exemple : construire un JAR avec toutes les dépendances à l'intérieur devrait se faire à la fin, après que toutes les classes aient été compilées, que tous les tests aient réussi, etc.

Nous examinerons comment les plugins fonctionnent plus en détail, ainsi que la compréhension par Maven des points de variation des plugins dans le processus de construction dans une future leçon. Pour l'instant, nous allons voir quelques exemples de plugins utiles.

Exec

Le plugin exec vous permet de spécifier une classe principale pour votre code, qui devrait être appelée par défaut lorsque le code est exécuté.

  • C'est ce qui se rapproche le plus du fameux triangle vert ("")
  • Tout ce que vous devez faire est d'indiquer la classe principale à appeler lors de l'exécution :
<!-- Specify main class for exec goal -->
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>1.6.0</version>
    <executions>
        <execution>
            <goals>
                <goal>java</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <mainClass>full.package.name.YourMainClassLauncher</mainClass>
    </configuration>
</plugin>

Une fois le plugin défini, vous pouvez facilement exécuter votre programme avec : mvn clean compile exec:java

Ajouter une configuration d'exécution Maven dans l'IDE

Une fois le plugin exec défini dans votre pom.xml, modifiez la "Configuration d'exécution" de l'IDE (aussi appelée lorsque le triangle vert est cliqué) pour simplement appeler le plugin exec de Maven !

Plugin Maven Jar

Le plugin JAR de Maven vous permet d'ajouter des informations supplémentaires lorsque votre programme est empaqueté dans un JAR.

  • Précédemment, nous avons vu qu'un JAR produit par Maven ne peut pas être lancé, sans indiquer explicitement la classe principale.
  • Le maven-jar-plugin vous permet de fournir une information par défaut, sur laquelle classe principale doit être listée dans le manifest du JAR.
<!-- specify main class for JAR manifest-->
<plugin>
    <artifactId>maven-assembly-plugin</artifactId>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <archive>
            <manifest>
                <addClasspath>true</addClasspath>
                <mainClass>ca.uqam.info.MainWithGson</mainClass>
            </manifest>
        </archive>
        <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
        <finalName>MainWithGson</finalName>
        <appendAssemblyId>true</appendAssemblyId>
    </configuration>
</plugin>

JavaDoc

Lors de la deuxième séance de laboratoire, vous avez appris une commande pour extraire manuellement toutes les informations JavaDoc de votre code afin de générer un site Web lisible par l'homme. Le plugin JavaDoc vous permet d'automatiser cette étape, en tant que composant standard du processus de construction.

  • Activer le plugin JavaDoc est également une bonne pratique, car vous pouvez directement voir s'il y a des problèmes dans votre style de code, chaque fois que vous compilez votre code.
  • Idéalement, le plugin est configuré pour échouer en cas d'avertissements, afin qu'aucun développeur ne soit tenté de travailler avec ou de produire du code non documenté.
    • "Je documenterai cela plus tard", se transforme facilement en "Je ne documenterai jamais cela."
<!-- Plugin to ensure all functions are commented and generate javadoc -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-javadoc-plugin</artifactId>
    <version>3.4.1</version>
    <configuration>
        <javadocExecutable>${java.home}/bin/javadoc</javadocExecutable>
        <reportOutputDirectory>${project.reporting.outputDirectory}/docs
        </reportOutputDirectory>
        <failOnWarnings>true</failOnWarnings>
        <quiet>true</quiet>
    </configuration>
    <executions>
        <execution>
            <id>attach-javadocs</id>
            <goals>
                <goal>jar</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Utilisez une bibliothèque de snippets

La plupart des développeurs ne créent pas manuellement leur pom.xml ligne par ligne, mais l'assemblent à partir de blocs préparés. Utilisez une bibliothèque de snippets, par exemple https://m5c.github.io/MavenSnippetLibrary/ pour créer rapidement un pipeline de construction fonctionnel.

Gestion des dépendances au-delà de Java

  • La gestion des dépendances n'est pas un concept spécifique à Java.
  • Presque chaque langage dispose d'outils pour assurer une gestion adéquate des dépendances, mais quel que soit le langage, les deux composants principaux restent :
    • Un fichier local pour spécifier les dépendances
    • Un dépôt central pour obtenir des artefacts
    • Un cache local des dépendances installées
  • Python :
    • Les dépendances du projet sont spécifiées dans un fichier requirements.txt
    • Tout comme les artefacts Java, toutes les dépendances listent un identifiant unique et une version, par exemple :
mkdocs-material==9.5.31
mkdocs-material-extensions==1.3.1
mkdocs-mermaid2-plugin==1.1.1
  • L'installateur de paquets de Python, pip, peut consommer un fichier requirements.txt pour télécharger des artefacts : pip install -r requirements.txt
  • Python n'a pas de concept de dépôt local partagé, mais offre des environnements virtuels.
    • Les environnements virtuels sont des caches locaux de dépendances.
    • Ils remplacent l'interpréteur Python global et permettent l'installation de dépendances spécifiques au projet.

Littérature

Inspiration et lectures supplémentaires pour les esprits curieux :