Accueil Nos publications Blog Petit focus sur 2 plugins Maven

Petit focus sur 2 plugins Maven

Dans ce rapide post, je tiens à faire partager deux “petits” plugins maven que j’ai eu l’opportunité de découvrir récemment par le biais de deux Olivier :
Les deux plugins sont les suivants :
Le but de ce post est de décrire les fonctionnalités que j’ai appréciées.

Application Assembler Maven Plugin

Description

Le plugin Application Assembler est un plugin maven qui permet de générer les scripts utilisés au démarrage d’une application Java. Pour ce faire, le plugin copie toutes les dépendances et les artifacts du projet dans un répertoire utilisé par le script pour la gestion du classpath de l’application à démarrer.
Les plateformes actuellement supportées sont :
  • *Nix,
  • Windows NT
  • Java Service Wrapper

Cas d’utilisation

Ce plugin trouve parfaitement sa place pour générer les scripts d’exécution d’une application java standalone (ie. non exécutée au sein d’un conteneur de servlet ou d’un serveur d’application) ou utilisée dans un batch.
Pour être plus précis, ce plugin dispose de 3 goals :
  • appassembler:assemble qui permet de créer une arborescence de fichiers comprenant des scripts de lancement de l’application ainsi que de toutes les dépendances nécessaires à son exécution.
  • appassembler:create-repository qui permet de créer le répertoire contenant toutes les librairies nécessaires au bon fonctionnement de l’application.
  • appassembler:generate-daemons qui permet de créer une arborescence de fichiers comprenant des scripts et des librairies à utiliser pour wrapper l’application dans un service via Java Service Wrapper.

Exemple

Pour montrer comment peut être utilisé ce plugin, je vais prendre un projet simple (accessible ici) qui ne contient qu’une classe disposant d’un main et qui log un message via logback au travers de slf4j (histoire de vérifier le comportement du plugin sur les dépendances).
Dans lequel on a :
package fr.soat.maven.sample;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class FooClass {

private static final Logger LOGGER = LoggerFactory.getLogger(FooClass.class);

public static void main(String[] args) {
LOGGER.info("this is FooClass sample");
}
}

Cet exemple a pour objectif de montrer, dans un premier temps, ce que génère le goal assemble puis, dans un second temps, le goal generate-daemons.

Pour ce faire, le pom.xml suivant sera utilisé :

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>fr.soat.maven.sample</groupId>
<artifactId>sample-assembler-plugin</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<projectBaseUri>${project.baseUri}</projectBaseUri>
<java.version>1.6</java.version>

<slf4j.version>1.6.4</slf4j.version>
<logback.version>1.0.0</logback.version>
</properties>

<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>

<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>appassembler-maven-plugin</artifactId>
<version>1.2</version>
<configuration>
<daemons>
<daemon>
<id>mainService</id>
<mainClass>fr.soat.maven.sample.FooClass</mainClass>
<commandLineArguments>
<commandLineArgument>start</commandLineArgument>
</commandLineArguments>
<platforms>
<platform>jsw</platform>
</platforms>
<generatorConfigurations>
<generatorConfiguration>
<generator>jsw</generator>
<includes>
<include>linux-x86-32</include>
<include>linux-x86-64</include>
</includes>
</generatorConfiguration>
</generatorConfigurations>
</daemon>
</daemons>
</configuration>
</plugin>

<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>appassembler-maven-plugin</artifactId>
<version>1.2</version>
<executions>
<execution>
<goals>
<goal>assemble</goal>
</goals>
</execution>
</executions>
<configuration>
<programs>
<program>
<mainClass>fr.soat.maven.sample.FooClass</mainClass>
<name>main</name>
</program>
</programs>
<binFileExtensions>
<unix>.sh</unix>
</binFileExtensions>
</configuration>
</plugin>
</plugins>
</build>
</project>
Dans le pom, on constate que l’exécution du goal assemble n’est pas associée à une phase en particulier. Cependant, c’est la phase package qui est utilisée par défaut. Dans sa configuration (disponible ici), il y figure le nom de la classe qui dispose du main ainsi que, via l’élément binFileExtensions, l’extension qui doit être utilisée pour le script dans le monde *Nix.
Ainsi, à la commande suivante :
mvn package

On obtient :

Où le main.sh est le suivant :

#!/bin/sh
# ----------------------------------------------------------------------------
#  Copyright 2001-2006 The Apache Software Foundation.
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#       https://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
# ----------------------------------------------------------------------------
#
#   Copyright (c) 2001-2006 The Apache Software Foundation.  All rights
#   reserved.

BASEDIR=`dirname $0`/..
BASEDIR=`(cd "$BASEDIR"; pwd)`

# OS specific support.  $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
case "`uname`" in
CYGWIN*) cygwin=true ;;
Darwin*) darwin=true
if [ -z "$JAVA_VERSION" ] ; then
JAVA_VERSION="CurrentJDK"
else
echo "Using Java version: $JAVA_VERSION"
fi
if [ -z "$JAVA_HOME" ] ; then
JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/${JAVA_VERSION}/Home
fi
;;
esac

if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi

# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$JAVA_HOME" ]  
 JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ]  
 CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi

# If a specific java binary isn't specified search for the standard 'java' binary
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME"  ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD=`which java`
fi
fi

if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly."
echo "  We cannot execute $JAVACMD"
exit 1
fi

if [ -z "$REPO" ]
then
REPO="$BASEDIR"/repo
fi

CLASSPATH=$CLASSPATH_PREFIX:"$BASEDIR"/etc:"$REPO"/org/slf4j/slf4j-api/1.6.4/slf4j-api-1.6.4.jar:"$REPO"/ch/qos/logback/logback-classic/1.0.0/logback-classic-1.0.0.jar:"$REPO"/ch/qos/logback/logback-core/1.0.0/logback-core-1.0.0.jar:"$REPO"/fr/soat/maven/sample/sample-assembler-plugin/1.0-SNAPSHOT/sample-assembler-plugin-1.0-SNAPSHOT.jar

EXTRA_JVM_ARGUMENTS=""

# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$CLASSPATH" ]  
 CLASSPATH=`cygpath --path --windows "$CLASSPATH"
[ -n "$JAVA_HOME" ]  
 JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"
[ -n "$HOME" ]  
 HOME=`cygpath --path --windows "$HOME"`
[ -n "$BASEDIR" ]  
 BASEDIR=`cygpath --path --windows "$BASEDIR"`
[ -n "$REPO" ]  
 REPO=`cygpath --path --windows "$REPO"`
fi

exec "$JAVACMD" $JAVA_OPTS \
$EXTRA_JVM_ARGUMENTS \
-classpath "$CLASSPATH" \
-Dapp.name="main" \
-Dapp.pid="$$" \
-Dapp.repo="$REPO" \
-Dbasedir="$BASEDIR" \
fr.soat.maven.sample.FooClass \
"$@"

Après un rapide :

chmod +x target/appassembler/bin/main.sh
Le résultat escompté est obtenu.
Afin de tester la génération de la couche wrapper Java Service Wrapper, le pom.xml contient également la déclaration du plugin appassembler-maven-plugin associé aux paramètres de configuration nécessaires à la génération du service. Pour cette partie, j’ai choisi de ne pas l’associer à une phase mais de l’exécuter manuellement :
            <plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>appassembler-maven-plugin</artifactId>
<version>1.2</version>
<configuration>
<daemons>
<daemon>
<id>mainService</id>
<mainClass>fr.soat.maven.sample.FooClass</mainClass>
<commandLineArguments>
<commandLineArgument>start</commandLineArgument>
</commandLineArguments>
<platforms>
<platform>jsw</platform>
</platforms>
<generatorConfigurations>
<generatorConfiguration>
<generator>jsw</generator>
<includes>
<include>linux-x86-32</include>
<include>linux-x86-64</include>
</includes>
</generatorConfiguration>
</generatorConfigurations>
</daemon>
</daemons>
</configuration>
</plugin>

Ainsi, après l’exécution de la commande :

mvn appassembler:generate-daemons

on obtient :

Cependant, après un rapide chmod sur les fichiers mainService et wrapper-linux-x86-64, la première tentative d’exécution échoue…  🙁
chmod +x target/generated-resources/appassembler/jsw/mainService/bin/mainService
chmod +x target/generated-resources/appassembler/jsw/mainService/bin/wrapper-linux-x86-64
./target/generated-resources/appassembler/jsw/mainService/bin/mainService start

Qu’à cela ne tienne, si il manque le répertoire logs, il suffit de le créer…

Encore un échec…
A priori, il semble que le classpath soit mauvais…
Après un rapide coup d’œil sur le fichier wrapper.conf,  on peut remarquer les lignes suivantes :
wrapper.java.mainclass=org.tanukisoftware.wrapper.WrapperSimpleApp
set.default.REPO_DIR=repo
set.default.APP_BASE=.

# Java Classpath (include wrapper.jar)  Add class path elements as
#  needed starting from 1
wrapper.java.classpath.1=lib/wrapper.jar
wrapper.java.classpath.2=%REPO_DIR%/fr/soat/maven/sample/sample-assembler-plugin/1.0-SNAPSHOT/sample-assembler-plugin-1.0-SNAPSHOT.jar
wrapper.java.classpath.3=%REPO_DIR%/org/slf4j/slf4j-api/1.6.4/slf4j-api-1.6.4.jar
wrapper.java.classpath.4=%REPO_DIR%/ch/qos/logback/logback-classic/1.0.0/logback-classic-1.0.0.jar
wrapper.java.classpath.5=%REPO_DIR%/ch/qos/logback/logback-core/1.0.0/logback-core-1.0.0.jar

Il semble donc qu’il manque le répertoire repo où les librairies sont recherchées…

Qu’a cela ne tienne, la commande suivante :

mvn appassembler:appassembler:create-repository
devrait permettre de créer le fichier repo dans le répertoire target/appassembler, et un petit cp devrait tout remettre dans l’ordre… en fait, non.
Il manque l’archetype de notre application dans le répertoire repo… (d’un côté, pour les personnes qui ont suivi, cela était visible dans un des screenshots précédents…)
Bon, je vous passe les quelques commandes à base de mvn et cp que j’ai effectuées pour obtenir au final :

Conclusion

Ce qui m’a intéressé dans ce plugin est qu’il s’occupe de générer automatiquement les scripts nécessaires au lancement de l’application. En effet, il est toujours possible d’utiliser le plugin assembly (tel que je l’avais décrit ici), mais cette solution reste assez verbeuse et rébarbative.
Aussi, avoir la possibilité, en n’ayant qu’à appeler ou associer un goal à une phase du cycle de vie du projet pour générer les scripts est assez tentant.
En outre, le plugin permet également de wrapper l’application dans un service via Java Service Wrapper.
Par contre, il peut sembler dommage que le “livrable” (au sens naïf du terme) ainsi produit ne puisse pas être considéré comme un archetype (et être déployé automatiquement sur un repository manager afin qu’il puisse être directement utilisable par d’éventuels OPS). Cependant, concernant ce point, vu que l’on s’appuie alors sur un plugin maven (dont la version est, bien sûr, maîtrisée), l’utilisateur est garanti d’avoir un script fonctionnel et reproductible.
Concernant la partie Java Service Wrapper, il semble, cependant, qu’il faille “retoucher” un peu ce qui est généré puisqu’il manque le répertoire repo (utilisé par le fichier wrapper.conf (chargé par Java Service Wrapper)) nécessaire au chargement du classpath à l’exécution du service.
Autre point un peu dommage est que les droits d’exécution ne soient pas directement positionnés sur les scripts .sh.

Tomcat 7 Maven Plugin

Description

Le plugin Tomcat7 Maven Plugin permet (pour ceux qui ne le savent pas encore… 😉 ) de déployer une application web dans le conteneur de servlets Tomcat 7 via  le goal deploy.
Il permet, en outre, de démarrer directement un Tomcat de manière embedded à Maven (un peu comme le plugin jetty) via le goal run.
Cependant, ce n’est pas pour ces fonctionnalités que je tenais à parler de ce plugin.
En effet, le plugin Tomcat7 dispose d’une fonctionnalité “amusante”, à savoir la possibilité de générer un jar exécutable embarquant directement un Tomcat 7.
En outre, de manière “un peu” transverse à ce plugin, un archetype existe également. Il permet de settuper un projet exposant un service REST via Apache CXF et qui dispose de tests d’intégration via Selenium.

Cas d’utilisation

Tomcat7 Maven Plugin dispose des goals suivants (que je ne vais pas décrire…) :
  • tomcat7:deploy
  • tomcat7:deploy-only
  • tomcat7:exec-war
  • tomcat7:exec-war-only
  • tomcat7:run
  • tomcat7:run-war
  • tomcat7:run-war-only
  • tomcat7:shutdown
Concernant l’archetype, il est très bien décrit à la page suivante :

Exemple

Maven Tomcat 7 Plugin

Comme je l’ai dit précédemment, je ne reviendrai pas sur l’utilisation des goals run et deploy mais me focaliserai plutôt sur le goal exec-war.
Pour montrer comment peut être utilisé ce plugin, je vais prendre un projet simple (accessible ici) qui ne contient qu’un simple Servlet FooServlet.
Dans lequel on a :
package fr.soat.maven.tomcat.sample;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Writer;

public class FooServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Writer out = resp.getWriter();
out.append("GET FooServlet successfully called...");
out.flush();
out.close();
}
}

pour le web.xml :

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://java.sun.com/xml/ns/javaee" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">

<servlet>
<servlet-name>FooServlet</servlet-name>
<servlet-class>fr.soat.maven.tomcat.sample.FooServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>FooServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
Le pom.xml qui est utilisé est le suivant :
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>fr.soat.maven.tomcat.sample</groupId>
<artifactId>sample-tomcat-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<projectBaseUri>${project.baseUri}</projectBaseUri>
<java.version>1.6</java.version>

<servlet-api.version>3.0-alpha-1</servlet-api.version>
</properties>

<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>${servlet-api.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.0-SNAPSHOT</version>

<executions>
<execution>
<id>tomcat-war-exec</id>
<goals>
<goal>exec-war</goal>
</goals>
<phase>package</phase>
<configuration>
<warRunDependencies>
<warRunDependency>
<dependency>
<groupId>fr.soat.maven.tomcat.sample</groupId>
<artifactId>sample-tomcat-plugin</artifactId>
<version>${project.version}</version>
<type>war</type>
</dependency>
</warRunDependency>
</warRunDependencies>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

On constate que le goal exec-war est associé à la phase package.

De ce fait, en exécutant la commande suivante :

mvn package

on obtient :

Ainsi, avec la commande suivante :

cd target
java -jar sample-tomcat-plugin-1.0-SNAPSHOT-war-exec.jar

il devient alors possible via notre navigateur préféré (à l’url suivante : https://localhost:8080/sample-tomcat-plugin/) d’obtenir la page suivante :

Maven Tomcat 7 Archetype

Ce petit paragraphe a juste pour objectif de montrer ce que génère l’archetype Tomcat7.

Ainsi, la commande :

mvn archetype:generate \
-DarchetypeGroupId=org.apache.tomcat.maven \
-DarchetypeArtifactId=tomcat-maven-archetype \
-DarchetypeVersion=2.0-SNAPSHOT \
-DarchetypeRepository=https://repository.apache.org/content/repositories/snapshots/

donne le résultat suivant :

Un rapide coup d’œil permet de se rendre compte que :
  • le module basic-api contient les interfaces pour le webservice REST
  • le module basic-api-impl contient l’implémentation des webservices REST
  • le module basic-webapp contient l’application web
  • le module basic-webapp-exec permet de générer un jar exécutable embarquant l’application web ainsi que le nécessaire de tomcat pour le démarrer en standalone
  • le module basic-webapp-it permet de lancer des tests Selenium sur l’application web démarrée au sein d’un Tomcat7
Pour tester, rien de plus simple, il suffit de lancer la commande suivante :
mvn install -Pchrome

Conclusion

Ce qui m’a plu sur le plugin Apache Tomcat 7 (en plus, bien sûr de la possibilité de lancer l’application web de manière embedded à Maven et de pouvoir déployer la webapp sur un Tomcat existant) est la possibilité de créer un jar exécutable.
C’est vrai que je n’ai toujours pas trouvé d’utilité pour cela mais la performance méritait d’être applaudie.
Concernant l’archetype Apache Tomcat 7, cela fournit en une ligne de commande un excellent template de projet qui dispose de tout.

Conclusion

Comme vous avez pu vous en rendre compte, il n’y a rien de révolutionnaire dans cet article mais je tenais à mettre en avant ces deux plugins qui, ont soit des fonctionnalités utiles dans le cadre d’un projet, soit quelques features intéressantes.

Pour aller plus loin…