Choses à faire

24h dans une journée, et tant de choses à faire !

VRaptor, le framework qui a les crocs

publié le 30 octobre 2009 par Killian Ebel

A l’occasion de la sortie de VRaptor 3, un framework MVC dernière génération pour Java, je me suis permis de faire un petit article de présentation.

VRaptor s’annonce très prometteur, mais n’est malheureusement pas encore disponible sur un dépôt Maven. Pour rappel, Maven est un outil de build qui fonctionne en indiquant les dépendances de son projet, appuyé par des conventions d’architecture. Ainsi, en respectant les standards proposés par Maven au niveau de l’arborescence d’un projet, il n’est plus nécessaire d’indiquer impérativement une opération de compilation, vu que Maven se charge de vous fournir le bon classpath et de télécharger les librairies manquantes si votre projet en dépend.

Si Maven vous fait peur, que le XML vous rebute, je ne peux que vous conseiller d’utiliser Gradle. Cet outil de build basé sur Groovy me paraît très flexible et simple à mettre en place ; nous le verrons rapidement à la fin de l’article.

Mise en place de Maven

Créez votre fichier de projet pom.xml :

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

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<modelVersion>4.0.0</modelVersion>
<groupId>fr.chosesafaire</groupId>
<artifactId>blog</artifactId>
<packaging>war</packaging>
<name>blog</name>
<version>0.0.1</version>
<description>Test de VRaptor</description>
<url>http://www.chosesafaire.fr</url>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>1.5</source>
                <target>1.5</target>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
        <plugin>
            <artifactId>maven-war-plugin</artifactId>
            <configuration>
                <warSourceDirectory>${basedir}/src/main/webapp</warSourceDirectory>
                <webappDirectory>${env.TOMCAT_HOME}/webapps/${project.artifactId}</webappDirectory>
            </configuration>
        </plugin>
    </plugins>
</build>

<dependencies>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.6.4</version>
    </dependency>
    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib-nodep</artifactId>
        <version>2.2</version>
    </dependency>
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.2.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-io</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>1.1.1</version>
    </dependency>
    <dependency>
        <groupId>com.google.collections</groupId>
        <artifactId>google-collections</artifactId>
        <version>1.0-rc2</version>
    </dependency>
    <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest-all</artifactId>
        <version>1.1</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>javassist</groupId>
        <artifactId>javassist</artifactId>
        <version>3.8.0.GA</version>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>1.2</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.7</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.12</version>
    </dependency>
    <dependency>
        <groupId>net.vidageek</groupId>
        <artifactId>mirror</artifactId>
        <version>1.5.1</version>
    </dependency>
    <dependency>
        <groupId>org.objenesis</groupId>
        <artifactId>objenesis</artifactId>
        <version>1.1</version>
    </dependency>
    <dependency>
        <groupId>ognl</groupId>
        <artifactId>ognl</artifactId>
        <version>2.7.3</version>
    </dependency>
        <dependency>
        <groupId>com.thoughtworks.paranamer</groupId>
        <artifactId>paranamer</artifactId>
        <version>2.1</version>
    </dependency>
    <dependency>
        <groupId>org.reflections</groupId>
        <artifactId>reflections</artifactId>
        <version>0.9.4</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.5.6</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.5.6</version>
        </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring</artifactId>
        <version>2.5.6</version>
    </dependency>
    <dependency>
        <groupId>br.com.caelum</groupId>
        <artifactId>vraptor</artifactId>
        <version>3.0.0</version>
    </dependency>
</dependencies>
</project>

Pour la liste des dépendances, j’ai pris toutes les librairies qui étaient incluses dans l’exemple de projet vide avec VRaptor (plus d’autres, comme JUnit et Hamcrest), et les ai recherchées sur http://www.mvnrepository.com/. On remarque certaines dépendances intéressantes et encore expérimentales comme Google Collections. J’ai pris soin à chaque fois de récupérer la version la plus récente (en espérant qu’il n’y aura pas de problèmes de compatibilité !).
Comme VRaptor (version 3) n’est encore disponible sur aucun dépôt Maven, il va falloir télécharger le JAR, puis l’installer dans votre dépôt local ( plus de détails ) :

mvn install:install-file -Dfile=/path/to/vraptor-3.0.0.jar -DgroupId=br.com.caelum -DartifactId=vraptor -Dversion=3.0.0 -Dpackaging=jar

De même, je n’ai pas trouvé l’archive reflections-0.9.4.jar, mais le projet est herbergé sur Google Code alors comme pour VRaptor, récupérez l’archive et installez le dans votre dépôt local :

mvn install:install-file -Dfile=/path/to/reflections-0.9.4.jar -DgroupId=org.reflections -DartifactId=reflections -Dversion=0.9.4 -Dpackaging=jar

Mises à part ces deux outils, Maven va se charger de télécharger automatiquement les versions demandées des autres et les incluera dans votre projet (que ce soit au niveau de la compilation ou de l’exécution, c’est à vous de paramétrer). Attention cependant si vous êtes derrière un proxy.

src/main/webapp/META-INF/MANIFEST.MF

Souvent passé à la trappe, ce fichier peut se révéler bien utile dans certains cas. Par défaut, il contient seulement une indication sur sa version :

Manifest-Version: 1.0

Mais comme indiqué sur le site de Sun, il peut être utilisé par exemple à des fins de signature du JAR, ou encore pour indiquer les dépendances de votre JAR. Cette dernière fonctionnalité est la base du fonctionnement des Bundles OSGi, le framework qui transforme votre application en modules indépendants, pouvant être rechargés individuellement et dynamiquement.

src/main/resources/log4j.properties

Pour cet exemple on ne va pas trop complexifier le debugger, qui affichera simplement dans la console :

log4j.rootLogger=debug, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n

src/main/webapp/web.xml

VRaptor s’installe sous la forme d’un filtre, dont le seul paramètre obligatoire est le package de base à partir duquel il va scanner vos classes :

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

    <display-name>chosesafaire-blog</display-name>

    <context-param>
        <param-name>br.com.caelum.vraptor.packages</param-name>
        <param-value>fr.chosesafaire.blog</param-value>
    </context-param>

    <filter>
        <filter-name>vraptor</filter-name>
        <filter-class>br.com.caelum.vraptor.VRaptor</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>vraptor</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>FORWARD</dispatcher>
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>

</web-app>

Ce fichier de configuration est le seul dont vous aurez besoin dans votre projet, il est malheureusement obligatoire car c’est le fonctionnement même de Java EE qui l’impose. Cependant, la sortie de Java EE 6 et des Servlets 3.0 va sûrement changer la donne, en privilégiant les annotations par rapport au XML !

src/main/java/fr/chosesafaire/blog/controllers/IndexController.java

Déclarons un contrôleur assez simple qui interceptera les requêtes :

package fr.chosesafaire.blog.controllers;

import br.com.caelum.vraptor.Path;
import br.com.caelum.vraptor.Resource;
import br.com.caelum.vraptor.Result;

@Resource
public class IndexController {

    private final Result result;

    public IndexController(Result result) {
        this.result = result;
    }

    @Path("/{name}")
    public void index(String name) {
        result.include("name", name.toUpperCase());
    }

    @Path("/")
    public void index() {
        index("Anonymous");
    }
}

src/main/webapp/jsp/index/index.jsp

Lors de l’appel de votre méthode index de l’IndexController, VRaptor va automatiquement associer le fichier index/index.jsp pour l’affichage :

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Le blog des choses à faire</title>
    </head>
    <body>
        <c:out value="${name}" /> has been kicked by the Raptor (Aoutch !)
    </body>
</html>

src/test/java/fr/chosesafaire/blog/controllers/IndexControllerTest.java

Une application de qualité ne le serait pas vraiment si elle n’était pas accompagnée de sa batterie de tests unitaires 🙂

Dans le projet d’exemple, les tests d’intégration sont réalisés avec SeleniumDSL et HtmlUnit, et les tests unitaires avec JUnit, JMock et Hamcrest (pour les Matchers). Nous nous contenterons de simples tests unitaires pour compléter l’article, mais surtout regardez le projet d’exemple, qui exploite bien les possibilités de ces librairies de tests !

package fr.chosesafaire.blog.controllers;

import static org.junit.Assert.assertThat;
import static org.hamcrest.Matchers.is;

import org.junit.Before;
import org.junit.Test;

import br.com.caelum.vraptor.util.test.MockResult;

public class IndexControllerTest {

    private IndexController controller;
    private MockResult result;

    @Before
    public void setUp() throws Exception {
        result = new MockResult();
        controller = new IndexController(result);
    }

    @Test
    public void indexWithParameter() throws Exception {
        controller.index("test");
        String name = result.included("name");
        assertThat(name, is("TEST"));
    }

    @Test
    public void indexWithoutParameter() throws Exception {
        controller.index();
        String name = result.included("name");
        assertThat(name, is("ANONYMOUS"));
    }
}

Déploiement de l’application

Un appel à maven-war-plugin vous permettra de compiler votre application dans un WAR :

mvn compile war:war

Si vous souhaitez faire une installation complète (lancer les tests unitaires etc…), vous pouvez utiliser la commande mvn clean install.

Enfin, déployez votre application dans votre serveur d’applications préféré. Si vous utilisez Tomcat par exemple, copiez juste votre WAR dans le répertoire $TOMCAT_HOME/webapps/, puis démarrez-le si ce n’est pas déjà fait (n’oubliez pas de déclarer TOMCAT_HOME !).

Pour éviter de s’embêter à redéplacer l’archive après chaque compilation, il est possible de spécifier une option au plugin (cela est déjà fait dans le pom.xml de l’article) :

...
<configuration>
...
    <webappDirectory>${env.TOMCAT_HOME}/webapps/${project.artifactId}</webappDirectory>
...
</configuration>
...

Vous pouvez aussi vous simplifier la tâche en utilisant directement le plugin Tomcat. Rendez-vous à l’adresse http://localhost:8080/blog/ (ou là où vous l’avez déployé), puis sur http://localhost:8080/blog/chuck.

Encore une astuce Maven

Si vous disposez de plusieurs processeurs, vous pouvez essayer de rendre les builds plus rapides avec une option de Maven :

# Lors de la compilation
mvn -Dmaven.artifact.threads=3 clean install

# Ou pour le rendre définitif
export MAVEN_OPTS=-Dmaven.artifact.threads=3

Bonus : Gradle

Si Maven vous rebute un peu, vous pouvez toujours tenter Gradle, un nouvel outil de build dans l’ère du temps. L’avantage d’écrire votre build en Groovy par rapport au XML est assez évident : plus léger, c’est un langage de script qui peut accéder aux classes Java, tout en apportant une syntaxe dynamique et simplifiée. La documentation de Gradle vous montrera mieux que moi toutes les possibilités offertes (d’ailleurs, Gradle sera étendu pour permettre d’écrire ses scripts en Ruby ou en Python… vraiment génial !).

A la racine de votre projet, créez un fichier build.gradle (l’équivalent du pom.xml, en plus concis !) :

group = 'fr.chosesafaire'
version = '0.0.1'
sourceCompatibility = 1.5

usePlugin('war')

repositories {
    mavenCentral()
    flatDir name: 'localRepository', dirs: 'lib'
}

dependencies {
    compile 'org.aspectj:aspectjrt:1.6.4'
    compile 'cglib:cglib-nodep:2.2'
    compile 'commons-fileupload:commons-fileupload:1.2.1'
    compile 'commons-io:commons-io:1.3.2'
    compile 'commons-logging:commons-logging:1.1.1'
    compile 'com.google.collections:google-collections:1.0-rc2'
    compile 'javassist:javassist:3.8.0.GA'
    compile 'javax.servlet:jstl:1.2'
    compile 'log4j:log4j:1.2.12'
    compile 'net.vidageek:mirror:1.5.1'
    compile 'org.objenesis:objenesis:1.1'
    compile 'ognl:ognl:2.7.3'
    compile 'com.thoughtworks.paranamer:paranamer:2.1'
    compile 'org.reflections:reflections:0.9.4'
    compile 'org.slf4j:slf4j-api:1.5.6'
    compile 'org.slf4j:slf4j-log4j12:1.5.6'
    compile 'org.springframework:spring:2.5.6'
    compile 'br.com.caelum:vraptor:3.0.0'

    testCompile 'org.hamcrest:hamcrest-all:1.1'
    testCompile 'junit:junit:4.7'
}

Faîtes attention si vous êtes derrière un proxy sans l’indiquer, il ne pourra pas télécharger les dépendances. Gradle peut utiliser les dépôts Maven et Ivy ; pour nos deux archives introuvables, on peut simplement déclarer un repértoire (ici lib) qui servira de dépôt (pas besoin d’installer quoi que ce soit). Ainsi, si vous souhaitez récupérer vos librairies manuellement, il suffit d’indiquer à Gradle le répertoire, qu’il scannera automatiquement.

Pour compiler, lancez la commande :

# pour seulement compiler le war
gradle war

# pour la construction totale (tests etc)
gradle build

L’archive WAR se retrouvera dans build/libs/ ; je n’ai pas encore repéré les options comme le nom du war généré et son emplacement. Dans la version actuelle de Gradle, certaines fonctionnalités ne sont pas encore bien implémentées (génération de rapports…), mais je ne doute pas (et j’espère !) que cet outil évoluera assez vite vers sa version finale.

Conclusion

Voilà, votre projet est opérationnel, et fonctionne avec Maven (ou Gradle), ce qui simplifiera tous vos builds futurs. Je n’ai pas vraiment montré les possibilités du framework, la documentation officielle le fera bien mieux que moi, mais en tout cas, il simplifie énormément le travail d’un développeur Java : quasiment rien à configurer, les injections sont automatiques, les intercepteurs aussi… A surveiller de près !

Avez-vous déjà utilisé ce framework dans une précédente version ? Que vaut-il face à Spring MVC ou Struts 2 ?

Les textes, illustrations et démonstrations présents sur ce site sont la propriété de leurs auteurs respectifs, sauf mention contraire (photo de la bannière).
Chosesafaire.fr, un site propulsé par Wordpress, vous est proposé par Pierre Quillery & Killian Ebel.

Valid XHTML 1.0 Strict