Il y a 9 ans -
Temps de lecture 13 minutes
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
[bash]<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>[/bash]
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 :
[java num= »1″ gutter= »true »]
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!";
}
}[/java]
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.
Commentaire
8 réponses pour " Construire une API REST avec Jersey et Spring sans web.xml, ni applicationContext.xml, ni getters/setters "
Published by chris , Il y a 9 ans
> 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
Published by Jean , Il y a 9 ans
@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 ;)
Published by chris , Il y a 9 ans
@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 :)
Published by climbfter , Il y a 9 ans
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
Published by Pierre-Jean Vardanéga , Il y a 9 ans
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.
Published by mouldblal , Il y a 8 ans
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.
Published by Pierre-Jean Vardanéga , Il y a 8 ans
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.
Published by RAHMI , Il y a 7 ans
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