Il y a 3 années · 13 minutes · Back, Craft

Construire une API REST avec Jersey et Spring sans web.xml, ni applicationContext.xml, ni getters/setters

Les API REST font légion de nos jours et sont très souvent découpées en plusieurs couches : contrôleurs (traitant les requêtes HTTP), services (exécutant la logique métier) et accès aux données (pour interagir avec la ou les bases de données). Pour cet exemple, nous utiliserons Jersey pour la couche REST, Spring pour l’injection de dépendances et Jongo pour accéder à une base MongoDB. 

L’objectif de cette article est de montrer qu’il est possible de simplifier la configuration de Jersey et Spring en utilisant uniquement des annotations, pas de web.xml ni d’applicationContext.xml. Nous utiliserons Jongo pour sa simplicité d’utilisation (pas de fichier persistence.xml). Nous montrerons aussi qu’il est possible de se passer des getters/setters, qui polluent nos objets, en passant par les constructeurs. En bonus, nous verrons comment ajouter de la validation sur nos ressources.

Création du projet

Nous allons utiliser maven pour gérer les dépendances du projet mais aussi pour démarrer un serveur et y déployer l’application. Rien de bien méchant ici, il suffit d’utiliser l’archetype maven permettant de générer une webapp comme suit :

mvn archetype:generate -DgroupId=fr.xebia.blog -DartifactId=jersey-spring -DarchetypeArtifactId=maven-archetype-webapp

Durant la création, quelques questions sont posées pour définir les métadonnées du projet. L’arborescence suivante a été générée :

Le fichier index.jsp ne nous servira pas car nous allons développer une API REST, nous pouvons donc le supprimer.

Ajout de Jersey

Jersey est l’implémentation de référence du JAX-RS, c’est pourquoi nous l’utilisons dans cet article. Pour l’ajouter, il va nous falloir l’importer comme suit en modifiant le fichier pom.xml

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <jersey.version>2.7</jersey.version>
    <servlet.version>3.0.1</servlet.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.glassfish.jersey</groupId>
            <artifactId>jersey-bom</artifactId>
            <version>${jersey.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
 
<dependencies>
    <dependency>
        <groupId>org.glassfish.jersey.containers</groupId>
        <artifactId>jersey-container-servlet</artifactId>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>${servlet.version}</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

Nous utilisons le BOM de jersey pour nous simplifier la concordance des versions.

Nous pouvons ajouter notre première ressource ; elle permettra de valider que l’application est bien démarrée :

package fr.xebia.blog.jerseyspring.business;
 
import javax.ws.rs.GET;
import javax.ws.rs.Path;
 
@Path("healthcheck")
public class HealthCheck {

    @GET
    public String doesItWorks() {
        return "It works!";
    }
}

Il faut maintenant configurer la servlet qui va réceptionner les requêtes HTTP. Tout d’abord, supprimons le fichier web.xml, nous n’allons pas l’utiliser. Ensuite, il va nous falloir définir quelle servlet utiliser ainsi que le chemin qu’elle va observer. Pour se faire, il suffit de créer la classe suivante :

package fr.xebia.blog.jerseyspring.config;

import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;

@ApplicationPath("api")
public class RestConfig extends ResourceConfig {

    public RestConfig() {
        packages("fr.xebia.blog.jerseyspring");
    }
}

En héritant de ResourceConfig.java et grâce à l’api servlet 3, on hérite d’un contexte JAX-RS par défaut. Il nous suffit donc de spécifier le chemin à scanner avec l’annotation @ApplicationPath. Au démarrage de l’application, tout le classpath va être scanné afin de trouver des ressources REST. Il est toutefois possible de limiter la recherche à un ou plusieurs répertoire en utilisant la méthode packages(String packages).

Enfin, pour pouvoir tester rapidement le résultat, nous ajoutons le plugin tomcat7-maven-plugin ainsi que la propriété failOnMissingWebXml :

<properties>
    [...]
    <failOnMissingWebXml>false</failOnMissingWebXml>
</properties>
[...]
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.tomcat.maven</groupId>
            <artifactId>tomcat7-maven-plugin</artifactId>
            <version>2.0</version>
            <configuration>
                <path>/</path>
            </configuration>
        </plugin>
    </plugins>
</build>

L’ajout de la propriété failOnMissingWebXml avec la valeur false est nécessaire car l’application ne démarrera pas si elle ne trouve pas le fichier web.xml. Nous avons aussi spécifié le path dans la configuration du plugin tomcat7 afin que le contexte de l’application soit "/" et non pas "jersey-spring" par défaut.

Nous pouvons maintenant générer les sources, démarrer un tomcat embarqué et accéder à notre API à l’adresse http://localhost:8080/api/healthcheck :

mvn tomcat7:run

Ajout de Spring

Nous allons ajouter la dépendance à Spring dans le pom.xml (nous rajoutons aussi jackson pour la sérialisation/désérialisation d’objets de et vers le format JSON) :

<properties>    
    [...]
    <jackson.version>2.1.4</jackson.version>
</properties>

<dependencies>
    [...]
    <dependency>
        <groupId>org.glassfish.jersey.ext</groupId>
        <artifactId>jersey-spring3</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.jaxrs</groupId>
        <artifactId>jackson-jaxrs-json-provider</artifactId>
        <version>${jackson.version}</version>
    </dependency>
</dependencies>

Tout d’abord, nous allons configurer Jackson. C’est très simple puisqu’il suffit de configurer Jersey pour utiliser le provider JacksonJsonProvider :

@ApplicationPath("api")
public class RestConfig extends ResourceConfig {
    public RestConfig() {
        packages("fr.xebia.blog.jerseyspring.business");
        register(JacksonJsonProvider.class);
    }
}

Ce qui nous permet de créer des objets sérialisables (à noter que nous n’utilisons aucun getter/setter) :

@JsonAutoDetect(
        fieldVisibility = JsonAutoDetect.Visibility.ANY // mandatory for serialization
)
public class User {
    
    private final String firstname;
    private final String lastname;
    
    @JsonCreator
    public User(@JsonProperty("firstname") String firstname,
                @JsonProperty("lastname") String lastname) {
        this.firstname = firstname;
        this.lastname = lastname;
    }
}

Maintenant, nous pouvons rajouter une ressource qui va faire appel à un service injecté :

@Path("users")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Component
public class UserResource {
    
    @Autowired
    private UserService userService;
    
    @GET
    public List<User> listAllUsers() {
        return userService.listAll();
    }
}

Nous observons quatre nouvelles annotations :

  • @Consumes et @Produces permettent de configurer respectivement les valeurs des headers Content-Type et Accept autorisées,
  • @Component permet de déclarer que le bean courant doit être géré par Spring,
  • @Autowired permet d’injecter le bean UserService, défini dans notre classpath et annoté de @Service (par exemple).

Enfin, notre classe de service qui va réaliser la logique (très complexe ici) :

@Service
class UserService {
    public List<User> listAll() {
        return Arrays.asList(
                new User("Sandro", "Mancuso"),
                new User("Robert", "Martin")
        );
    }
}

Avant de pouvoir tester le tout, nous devons nous assurer que le contexte Spring est bien configuré. Sachant que nous n’avons pas de fichier web.xml, nous ne pouvons pas nous baser sur le fichier applicationContext.xml. De toute façon, nous ne voulons pas l’utiliser. Nous utiliserons donc l’annotation @Configuration. Nous pourrons y définir nos beans ou bien nos sources de données (cf. chapitre suivant) :

@Configuration
@ComponentScan(basePackages = "fr.xebia.blog.jerseyspring.business")
public class SpringConfig {
}

Maintenant, nous pouvons démarrer notre application :

mvn tomcat7:run

Et là, c’est le drame… 

SEVERE: Context initialization failed 
[...]
Caused by: java.io.FileNotFoundException: class path resource [applicationContext.xml] cannot be opened because it does not exist
[...]

Que se passe-t-il donc ? Déjà, nous avons omis une étape, l’instanciation du contexte. Servlet 3 facilite la vie, mais là, nous en demandons trop. Auparavant, nous devions configurer la classe SpringServlet dans le fichier web.xml. Maintenant, il faut déclarer une classe héritant de WebApplicationInitializer. Ensuite, si au démarrage, nous avons un composant qui cherche le fichier applicationContext.xml, c’est qu’il y a une initialisation du contexte Spring de faite quelque part. En fouillant un peu, on se rend compte que jersey propose une implémentation par défaut pour instancier le contexte Spring. Le problème est que cette classe scanne le classpath pour trouver le fichier applicationContext.xml que nous ne souhaitons pas. Pour pallier à ce problème, nous allons instancier notre propre contexte qui va utiliser la configuration se trouvant dans le package config et qui se chargera à la place du contexte par défaut grâce à l’annotation @Order(HIGHEST_PRECEDENCE).

@Order(Ordered.HIGHEST_PRECEDENCE)
public class SpringContextInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        servletContext.setInitParameter("contextConfigLocation", "fr.xebia.blog.jerseyspring.config");
        WebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext();
        if (rootAppContext != null) {
            servletContext.addListener(new ContextLoaderListener(rootAppContext));
        }
    }
}

Réessayons de démarrer notre application et testons les ressources suivante : http://localhost:8080/api/healthcheck et http://localhost:8080/api/users.

mvn tomcat7:run

Ajout de Jongo

Pour ajouter Jongo à notre projet, il nous suffit d’ajouter la dépendance qui va bien dans notre pom.xml :

<properties>    
    [...]
    <jongo.version>1.0</jongo.version>
    <mongo-java-driver.version>2.11.4</mongo-java-driver.version>
</properties>

<dependencies>
    [...]        
    <dependency>
        <groupId>org.jongo</groupId>
        <artifactId>jongo</artifactId>
        <version>${jongo.version}</version>
    </dependency>
    <dependency>
        <groupId>org.mongodb</groupId>
        <artifactId>mongo-java-driver</artifactId>
        <version>${mongo-java-driver.version}</version>
    </dependency>
</dependencies>

Ensuite, comme écrit plus haut, nous allons déclarer un bean Spring représentant notre source de données dans la configuration de Spring :

@Configuration
@ComponentScan(basePackages = "fr.xebia.blog.jerseyspring.business")
public class SpringConfig {
    
    @Bean(name = "usersCollection")
    public MongoCollection getScoresCollection() throws UnknownHostException {
        DB db = new MongoClient().getDB("xebians");
        Jongo jongo = new Jongo(db);
        return jongo.getCollection("users");
    }
}

Il faut ensuite injecter le bean correspondant à notre collection MongoDB dans la classe UserService, puis faire la requête qui sélectionnera tous les utilisateurs :

@Service
class UserService {
    
    @Autowired
    private MongoCollection usersCollection;
    
    public Iterable<User> listAll() {
        return usersCollection.find().as(User.class);
    }
}

Il ne nous reste plus qu’à compiler, démarrer un serveur mongodb (en local, sinon, il faudra modifier la configuration du bean "usersCollection"), lancer le serveur et tester : http://localhost:8080/api/healthcheck et http://localhost:8080/api/users.

mvn tomcat7:run

Ajout de validation sur la ressource

Nous allons maintenant mettre à disposition une API permettant d’ajouter un utilisateur. Sachant que tous les champs d’un utilisateur sont obligatoires, il nous faut valider les valeurs passées en paramètres. Pour se faire, nous allons utiliser bean validation en ajoutant la dépendance dans notre pom.xml :

<dependencies>
    [...]
    <dependency>
        <groupId>org.glassfish.jersey.ext</groupId>
        <artifactId>jersey-bean-validation</artifactId>
    </dependency>
</dependencies>

Nous allons donc ajouter la validation sur la nouvelle API :

@Path("users")
[...]
public class UserResource {
    
    @Autowired
    private UserService userService;
    
    [...]

    @PUT
    public Response addUser(@NotNull @Valid User newUser) {
        userService.addUser(newUser);
        return ok().status(CREATED).build();
    }
}

Nous avons ajouté les annotations @NotNull et @Valid qui vont respectivement vérifier que l’utilisateur n’est pas null et que chacun de ses attributs respecte les potentielles annotations de validation qui lui sont affectées :

[...]
public class User {
 
    @Email
    @NotBlank
    protected final String email;
    @NotBlank
    private final String firstname;
    @NotBlank
    private final String lastname;
 
    [...]
}

Nous pouvons maintenant tester :

mvn tomcat7:run
curl -X PUT -H "Content-Type: application/json" -H "Accept: application/json" http://localhost:8080/api/users -d '{"email":"jdoe@xebia.fr", "firstname": "John", "lastname": "Doe"}' -v
curl -X PUT -H "Content-Type: application/json" -H "Accept: application/json" http://localhost:8080/api/users -d '{}' -v

Nous obtenons une réponse avec le code http 201 pour la première requête puis une avec le code http 400 pour la seconde requête (sans trop d’explication sur le problème). Si en cas d’erreur, vous souhaitez avoir le détail des validations qui ont échouées, il suffit de configurer Jersey en ajoutant la propriété ServerProperties.BV_SEND_ERROR_IN_RESPONSE à true :

@ApplicationPath("api")
public class RestConfig extends ResourceConfig {
    
    public RestConfig() {
        packages("fr.xebia.blog.jerseyspring.business");
        property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
        register(JacksonJsonProvider.class);
    }
}

En ré-exécutant le dernier appel, nous obtenons le résultat suivant :

mvn tomcat7:run
curl -X PUT -H "Content-Type: application/json" -H "Accept: application/json" http://localhost:8080/api/users -d '{"email":"jdoeATxebia.fr", "firstname": "John", "lastname": " "}' -v
 
[
    {
        "message":"not a well-formed email address",
        "messageTemplate":"{org.hibernate.validator.constraints.Email.message}",
        "path":"UserResource.addUser.arg0.email",
        "invalidValue":"jdoeATebia.fr"
    },
    {
        "message":"may not be empty",
        "messageTemplate":"{org.hibernate.validator.constraints.NotBlank.message}",
        "path":"UserResource.addUser.arg0.lastname",
        "invalidValue":" "
    }
]

Ajout d’un filtre

Les filtres permettant de réaliser des traitements spécifiques sur toutes les requêtes comme par exemple ajouter des headers CORS pour autoriser n’importe quelle application hébergée sur un autre domaine d’utiliser notre API. Pour réaliser un filtre avec Jersey, il suffit de créer une classe implémentant ContainerRequestFilter et/ou ContainerResponseFilter, comme dans l’exemple ci-dessous. Pour enregistrer le filtre, il y a deux possibilités : l’annoter avec @Provider ou bien l’ajouter dans la configuration Jersey en utilisant la méthode register(MyFilter.class).

@Provider
public class CORSResponseFilter implements ContainerResponseFilter {
    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
        responseContext.getHeaders().add("Access-Control-Allow-Origin", "*");
    }
}

Conclusion

Nous avons maintenant une base de code stable et légère nous permettant d’ajouter des API REST à volonté. Le contrat est rempli : la configuration est simple, mais a nécessité un petit hack pour démarrer le contexte Spring, nous n’avons ni getter/setter, ni fichier web.xml ou encore applicationContext.xml. 

Les sources sont disponibles sur github.

Pierre-Jean Vardanéga
Passionné par les technologies Java/JEE et mobile depuis sa sortie de l'ENSSAT, Pierre-Jean Vardanéga est aujourd'hui consultant chez Xebia. Il intervient en mission où l'agilité fait foi. Il est également attentif à l'actualité mobile, notamment au système d'exploitation Android et son intégration en entreprise.

8 réflexions au sujet de « Construire une API REST avec Jersey et Spring sans web.xml, ni applicationContext.xml, ni getters/setters »

  1. Publié par chris, Il y a 3 années

    > Nous avons maintenant une base de code stable et légère

    … associée à plusieurs Mo de jars en dépendances :)

    Tout dépend de l’usage final mais pour ce genre de micro-services je préfère utiliser Spark (ou même à Mo équivalents, Dropwizard).

    PS : faudrait mettre display_errors à Off dans votre php.ini, sinon on voit votre arboresence PHP lors d’une erreur ce qui a été le cas ce matin

  2. Publié par Jean, Il y a 3 années

    @chris je soupçonne qu’il ne s’agisse que l’objectif soit de simplifier la config massive d’énormes applications pas de faire un simple micro service.

    @Pierre-jean: il ne reste plus à spring qu’a offrir un mode de résolution de dépendances à la compilation pour éviter les mauvaises surprises style les cycles qui plantent au démarrage en prod seulement . Un peu comme un cake pattern en scala ;)

  3. Publié par chris, Il y a 3 années

    @Jean : pour simplifier la config massive d’énorme applications, les classes de configuration sont effectivement un plus indéniable.

    Cependant, selon moi, l’abus de @Autowired mène au final à une situation encore plus difficile à gérer que l’abus de XML !

    Le plus efficace est alors (selon moi) de diviser la grosse application en plus petites applications indépendantes.

    De fait, quand j’en ai la possibilité, je n’utilise plus Spring ni Jersey ni JEE. En appelant « manuellement » les constructeurs et en visualisant le « flux », on se rend rapidement compte qu’on a une application trop grosse et on n’arrive jamais à une situation extrême.

    C’est sûr que sur une legacy, c’est plus difficile :)

  4. Publié par climbfter, Il y a 3 années

    Bonjour,
    J’ai eu un premier soucis lors de cette exemple, j’ai du ajouter spring-webmvc pour que mon contexte démarre sans encombre.
    Maintenant quand je fais un inject j’ai une belle erreur « rg.glassfish.hk2.api.UnsatisfiedDependencyException: There was no object available for injection at Injectee(requiredType=ClientService,parent=TestRest,qualifiers={}) », l’injection échoue :(
    Sur le net ceux ayant le même problème disent qu’il faut utiliser SpringServlet mais je ne vois pas comment faire dans cet exemple.

    Ps : j’utilise jetty et non tomcat

  5. Publié par Pierre-Jean Vardanéga, Il y a 3 années

    Bonjour climbfter et merci pour ton commentaire.

    As-tu comparé tes sources avec celle sur github ? Tu pourrais y trouver des éléments de réponse à tes questions.

    Normalement, tu n’as nullement besoin de Spring MVC (dépendence spring-webmvc) car on ne l’utilise pas dans cette exemple. Je pense que l’erreur se situe ailleurs. Je suppose aussi qu’en résolvant ce premier problème, celui d’injection de dépendence disparaitra.

    D’autre part, l’utilisation de Jetty ne pose pas de problème si tant est que tu utilises une version supportant servlet 3.X. J’ai mis à jour les sources pour pouvoir utiliser Jetty en rajoutant un plugin et une dépendance.

    N’hésite pas à me faire un retour pour savoir si tu as réussi à solutionner tes soucis.

    Pierre-Jean.

  6. Publié par mouldblal, Il y a 2 années

    Bonsoir,
    Peut on utiliser cet exemple pour un déploiement sur un serveur Tomcat 6? il me semble que ce dernier ne supportait que les servlet 2.5 ? y a t-il un contournement dans ce cas ?
    Me conseilleriez vous d’utiliser jongo pour une petite base de données Microsoft sql server ?

    D’avance merci.

  7. Publié par Pierre-Jean Vardanéga, Il y a 2 années

    Bonjour,

    En effet, le fichier web.xml n’est plus obligatoire à partir de la version 3.0 de l’API servlet, qui quant à elle, est compatible avec les versions 7 et plus de Tomcat. Donc vous ne pourrez vous séparez de ce fichier avec tomcat 6. Je ne connais malheureusement pas, à ce jour, de contournement possible. J’ai pensé à surcharger le version de l’API servlet de Tomcat mais ça semble être une mauvaise idée car elle est centrale au fonctionnement de Tomcat.

    Enfin, concernant l’utilisation de Jongo avec MS SQL Server, je crains que ce ne soit pas possible car cette librairie est conçue spécifiquement pour intéragir avec une base de données MongoDB.

    Cordialement,
    Pierre-Jean.

  8. Publié par RAHMI, Il y a 2 années

    Bonjour,

    On utilise Jersey 2.22 dans l’un de nos projet.

    Si vous utilisez ResourceConfig pour la configuration de votre webapp il suffit d’ajouter la propriété contextConfig dans le constructeur de la ResourceConfig.
    ex :
    property(« contextConfig », new AnnotationConfigApplicationContext(VotreClassDeConfig.class));

    Rahmi

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *