Mon premier plugin Maven

Il y a des outils qui sont bien pratiques, voire parfois nécessaires à tout développeur qui se respecte. Un logiciel de gestion de build comme Ant ou Maven en fait partie. Outre l’avantage indéniable de permettre de télécharger la moitié de l’internet lors du premier build sur une machine clean, Maven permet aussi via son système de plugins d’effectuer tout un tas d’actions allant plus loin que la compilation des classes et leur packaging dans un JAR. Je vous propose de regarder comment créer son premier plugin Maven.

D’abord il faut un besoin

Eh oui, parce que se contenter de créer un plugin qui affiche “Hello World” à chaque build, c’est pas très utile… Alors tant qu’à faire, mettons nous en situation réelle. J’aurais pu prendre comme exemple un plugin qui lance l’Annotation Processing Tool à chaque compilation (par exemple pour Android Annotations), mais je vais plutôt vous parler du premier plugin que j’ai eu à développer, qui permet de faire de la génération de code.

Ce générateur prend en entrée un modèle décrit dans un fichier XML, et produit en sortie des classes Java correspondant aux entités et aux DAO CRUD, ainsi qu’un script SQL de création de tables. Ces classes sont ensuite utilisables dans les services écrits à la main. Certaines parties du code généré sont configurables :

  • le fichier XML décrivant le modèle
  • le package Java de base des classes générées
  • le nom du script SQL de créations de tables

Création du squelette de plugin Maven

Un plugin Maven n’est rien d’autre qu’un projet Maven avec un packaging de type maven-plugin :

1
2
3
4
5
6
7
8
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <groupId>com.excilys.labs.maven.plugin</groupId>
    <artifactId>generator-maven-plugin</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>maven-plugin</packaging>
</project>

La convention veut que les plugins soient nommés xyz-maven-plugin, ce qui permet d’appeler les goals de la manière suivante lors d’un build :

1
$ mvn xyz:some-goal

Notez qu’il n’est pas nécessaire de spécifier le nom complet du plugin, Maven va compléter avec “-maven-plugin”, ça fait quelques caractères en moins à écrire ;-). Si la convention de nommage n’est pas suivie, il faut impérativement spécifier le nom complet du plugin dans la ligne de commande.

Ensuite, un plugin contient un ou plusieurs MOJO, qui vont exécuter le “vrai” code du plugin. Un MOJO correspond en fait à un goal. Par exemple, dans le plugin dependency-maven-plugin, on retrouve les goals “tree” (mvn dependency:tree) et “resolve” (mvn dependency:resolve)

Créons donc un squelette de MOJO pour notre générateur de code :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.excilys.labs.maven.plugin.generator;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;

/**
 * A goal to generate code.
 *
 * @goal generate
 * @phase generate-sources
 */

public class CodeGeneratorMojo extends AbstractMojo {
    public void execute() throws MojoExecutionException, MojoFailureException {
        getLog().info("Hello World!");
    }
}

Notre MOJO étend AbstractMojo, qui fournit toute l’infrastructure utile à la création d’un goal. La méthode abstraite execute() contient le code à exécuter, c’est donc à nous de l’implémenter. Pour l’instant elle ajoute une ligne de log saluant le monde (je sais, j’ai dit tout à l’heure qu’on ne se contenterait pas de faire un hello world…). La méthode getLog() est un exemple de facilités fournies par l’AbstractMojo.

Notez également la Javadoc, qui contient deux tags particuliers. @goal spécifie le nom du goal correspondant à notre MOJO, ce tag est obligatoire. @phase permet de dire à Maven que notre plugin intervient durant la phase de génération des sources du lifecycle Maven (avant la compilation des sources, donc).

Notre plugin est prêt à être exécuté. Configurons le pom.xml pour permettre l’exécution du plugin lors du build :

1
2
3
4
5
6
7
8
9
    <build>
        <plugins>
            <plugin>
                <groupId>com.excilys.labs.maven.plugin</groupId>
                <artifactId>generator-maven-plugin</artifactId>
                <version>1.0-SNAPSHOT</version>
            </plugin>
        </plugins>
    </build>

Puis lançons un build Maven :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mbp:GeneratorMavenPlugin bastien$ mvn generator:generate
[INFO] Scanning for projects...
[INFO]                                                                        
[INFO] ------------------------------------------------------------------------
[INFO] Building generator-maven-plugin 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- generator-maven-plugin:1.0-SNAPSHOT:generate (default-cli) @ generator-maven-plugin ---
[INFO] Hello World!
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.322s
[INFO] Finished at: Mon May 14 14:47:54 CEST 2012
[INFO] Final Memory: 2M/81M
[INFO] ------------------------------------------------------------------------

Ajout de paramètres au plugin

Notre cahier des charges requière la possibilité de configurer certaines parties du générateur. Pour cela, ajoutons des paramètres à notre MOJO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CodeGeneratorMojo extends AbstractMojo {

    /**
     * @parameter alias="model.location"
     * @required
     */

    private String modelLocation;

    /**
     * @parameter
     * @required
     */

    private String basePackageName;

    /**
     * @parameter default-value="${project.build.directory}/create.sql"
     */

    private String sqlOutputLocation;

    // ...
}

Nos trois attributs utilisent également des tags Javadoc pour indiquer que ce sont des paramètres du plugin. Les deux premiers sont obligatoires, le troisième est facultatif et prend une valeur par défaut. ${project.build.directory} sera remplacé au runtime par le chemin du dossier target.
L’attribut “alias” de @parameter permet de définir le nom du paramètre s’il est différent du nom de l’attribut Java. Ainsi, par défaut il y aura un paramètre nommé “basePackageName”, puisqu’on ne lui a pas donné d’alias. Une liste plus exhaustive des tags Javadoc est disponible sur le site de Sonatype.

Ouvrons à nouveau le pom.xml pour ajouter la configuration du plugin :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    <build>
        <plugins>
            <plugin>
                <groupId>com.excilys.labs.maven.plugin</groupId>
                <artifactId>generator-maven-plugin</artifactId>
                <version>1.0-SNAPSHOT</version>

                <configuration>
                    <model.location>src/main/resources/model.xml</model.location>
                    <basePackageName>com.excilys.labs.generated</basePackageName>
                </configuration>
            </plugin>
        </plugins>
    </build>

Pour vérifier que la configuration est bien injectée au runtime, ajoutons un log :

1
getLog().info("Classes will be generated in package " + basePackageName);
1
2
3
4
5
6
7
8
mbp:GeneratorMavenPlugin bastien$ mvn generator:generate
...
[INFO] --- generator-maven-plugin:1.0-SNAPSHOT:generate (default-cli) @ generator-maven-plugin ---
[INFO] Hello World!
[INFO] Classes will be generated in package com.excilys.labs.generated
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
...

Maintenant que nous savons créer, configurer et exécuter un plugin Maven, il ne reste plus qu’à implémenter la méthode execute() pour appeler les vraies classes de génération de code. Comme ce n’est pas vraiment l’objet de cet article, je ne détaillerai pas plus cette partie. Cependant un FakeGenerator est disponibles dans les sources complètes.

J’aime pas les XDoclet !!

Comme vous l’avez vu, la configuration du Mojo se fait par défaut dans des tags Javadoc (des sortes de XDoclet). Ce n’est pas terrible dans la mesure où il est facile de faire une faute de frappe (ex @paramter au lieu de @parameter) qui ne sera pas détectée par le compilateur ni par Maven. Pour pallier ce problème, il existe un set d’annotations Java, le maven-plugin-anno. Après avoir configuré la dépendance dans votre pom, il est ensuite possible de tout annoter :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
    <dependencies>
        <dependency>
            <groupId>org.jfrog.maven.annomojo</groupId>
            <artifactId>maven-plugin-anno</artifactId>
            <version>1.4.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-plugin-plugin</artifactId>
                <version>2.6</version>
                <dependencies>
                    <dependency>
                        <groupId>org.jfrog.maven.annomojo</groupId>
                        <artifactId>maven-plugin-tools-anno</artifactId>
                        <version>1.4.0</version>
                        <scope>runtime</scope>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>

    <pluginRepositories>
        <pluginRepository>
            <id>jfrog-plugins</id>
            <name>jfrog-plugins-dist</name>
            <url>http://repo.jfrog.org/artifactory/plugins-releases-local</url>
            <releases>
                <enabled>true</enabled>
            </releases>
        </pluginRepository>
    </pluginRepositories>
    <repositories>
        <repository>
            <id>jfrog-plugins</id>
            <name>jfrog-plugins-dist</name>
            <url>http://repo.jfrog.org/artifactory/plugins-releases</url>
            <layout>default</layout>
            <releases>
                <enabled>true</enabled>
            </releases>
        </repository>
    </repositories>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@MojoGoal("generate-annotated")
@MojoPhase("generate-sources")
public class AnnotatedMojo extends AbstractMojo {
   
    @MojoParameter(alias = "model.location", required = true)
    private String modelLocation;

    @MojoParameter(required = true)
    private String basePackageName;

    @MojoParameter(defaultValue = "${project.build.directory}/create.sql")
    private String sqlOutputLocation;

    public void execute() throws MojoExecutionException, MojoFailureException {
        // ...
    }
}

Sachez également que le support des annotations est en train d’arriver nativement dans Maven (Java5 annotations support for Maven plugins).

Conclusion

Comme vous avez pu le voir, créer un plugin Maven n’est pas très compliqué. Les plugins sont suffisamment souples et configurables pour pouvoir exécuter du code déjà existant (par exemple remplacer un main() par un plugin Maven) sans trop de difficultés. La palette de plugins déjà disponibles est assez vaste, mais si un jour vous avez un besoin spécifique vous saurez que le coût de création d’un plugin n’est pas très élevé ;-).

Liens utiles

VN:R_U [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

À propos de Bastien Jansen

Vous pouvez facilement me retrouver sur twitter, linkedin, voire même le blog Excilys (l'autre ;-)). De temps en temps je poste aussi sur mon blog perso.
Ce contenu a été publié dans Maven, avec comme mot(s)-clef(s) , , . Vous pouvez le mettre en favoris avec ce permalien.

2 réponses à Mon premier plugin Maven

  1. Damien V. dit :

    J’allais justement faire des recherche sur ce sujet ce soir. Merci, tu viens de me libérer ma soirée :P

    VN:R_U [1.9.22_1171]
    Rating: 0 (from 0 votes)

Laisser un commentaire