Les implémentations JAX-RS (1)

le 02/03/2011 par Cyril Poder
Tags: Software Engineering

Il y a quelque temps l'envie de développer une petite application Java RESTFul m'a pris subitement. Je me suis donc penché sur les différentes implémentations JAX-RS et voici le résultat de mon investigation.

Je me suis tout de même posé quelques contraintes :

  • que mon code Java soit complètement indépendant de l'implémentation JAX-RS choisie ;
  • pouvoir facilement intégrer mes services avec Spring ;
  • ne produire et consommer que du JSON en utilisant l'API Jackson ;
  • pouvoir facilement mettre en place des tests unitaires.

Ce qui m'amène à comparer trois implémentations : CXF (version 2.3.2), Jersey (version 1.5), RESTEasy (version 2.1.0.GA) et Restlet (version 2.0.5). CXF, Jersey et RESTEasy utilisent Jettison pour produire et consommer du JSON. Or Jettison est un outil de mapping JSON/XML or je souhaite effectuer un simple mapping POJO/JSON. Raison pour laquelle j'opte pour Jackson.

Je partirai du principe qu'une certaine connaissance de JAX-RS, Spring et Maven est acquise par mes lecteurs.

Concernant les performances, je ne m'attarderai pas dessus car il s'avère qu'elles se tiennent dans un mouchoir de poche.

Les sources sont disponibles sur Github.

Sans plus attendre, revêtons notre bleu de travail, calons notre crayon derrière l'oreille et mettons les mains dans le cambouis...

Le service

Voici le code - on ne peut plus simple - du service que l'on se propose de déployer :

@Path("/someResource")
@Service
public class SomeService {
	@Resource
	private BeanRepo beanRepo;

	@GET
	@Produces(MediaType.APPLICATION_JSON)
	public List<SomeBean> getSomeBeans() {
		return beanRepo.getAll();
	}
	
	@GET
	@Produces(MediaType.APPLICATION_JSON)
	@Path("{index}")
	public SomeBean getSomeBean(@PathParam("index") int index) {
		return beanRepo.get(index);
	}
	
	@PUT
	@Consumes(MediaType.APPLICATION_JSON)
	public void saveSomeBean(SomeBean someBean) {
		beanRepo.save(someBean);
	}
}

On remarquera l'utilisation des annotations @Produces et @Consumes qui indiquent que l'on ne produit et consomme ici que du JSON.

CXF

On ne présente plus CXF, la stack par excellence pour développer des services en Java supportant tous types de transports et de protocoles et aux performances qui ne sont plus à démontrer.

Et si vous n'êtes toujours pas convaincus, allez donc jeter un œil aux articles référencés ici ou .

Certains argueront qu'utiliser CXF pour du REST c'est comme aller chercher un couteau suisse pour ouvrir une bière quand un simple décapsuleur était à portée de main...

Dépendances

La documentation de CXF est très bien fournie à ce niveau. Cependant l'artifact Maven cxf-rt-frontend-jaxrs ramène toutes les dépendances permettant d'effectuer des mappings XML ainsi que Jettison, ce dont je n'ai pas besoin.

Afin de n'importer que le strict nécessaire mon POM ressemble donc à ceci :

<dependency>
	<groupId>org.apache.cxf</groupId>
	<artifactId>cxf-rt-frontend-jaxrs</artifactId>
	<version>${cxf.version}</version>
	<!-- Since we use jackson, we can get rid of Jettison and all XML stuff -->
	<exclusions>
		<exclusion>
			<groupId>org.apache.xmlbeans</groupId>
			<artifactId>xmlbeans</artifactId>
		</exclusion>
		<exclusion>
			<groupId>com.sun.xml.bind</groupId>
			<artifactId>jaxb-impl</artifactId>
		</exclusion>
		<exclusion>
			<groupId>org.codehaus.woodstox</groupId>
			<artifactId>woodstox-core-asl</artifactId>
		</exclusion>
		<exclusion>
			<groupId>org.codehaus.jettison</groupId>
			<artifactId>jettison</artifactId>
		</exclusion>
		<exclusion>
			<artifactId>cxf-rt-databinding-aegis</artifactId>
			<groupId>org.apache.cxf</groupId>
		</exclusion>
	</exclusions>
</dependency>

Configuration Spring

CXF gérant différents types de transports et de protocoles, il nécessite davantage de configuration que ses "concurrents" avec notamment l'import des fichiers de configuration CXF, le namespace jaxrs à définir et enfin la déclaration et configuration systématique de chaque service...

En outre la documentation n'explique pas très clairement comment intégrer Jackson et si vous êtes nouveau sur CXF il vous faudra parcourir toute la documentation avant de comprendre comment ajouter un provider à un service REST (ce qui en fin de compte n'est pas bien compliqué comme le montre la suite).

L'essentiel de la documentation se trouve ici et . Pour faire court, le principe est de fournir un provider JSON à nos services :

<jaxrs:server id="someService" transportId="http://cxf.apache.org/transports/http">
	<jaxrs:serviceBeans>
		<bean class="com.octo.rest.SomeService" />
	</jaxrs:serviceBeans>
	<jaxrs:providers>
		<bean class="org.codehaus.jackson.jaxrs.JacksonJsonProvider" />
	</jaxrs:providers>
</jaxrs:server>

Simple certes, mais il faut le faire pour chaque service !

On notera l'attribut transportId sur la balise jaxrs:server, qui par défaut vaut http://schemas.xmlsoap.org/wsdl/http (donc du SOAP, ce qui n'est pas vraiment ce que l'on veut).

Il est possible de ne pas avoir à redéfinir le provider pour chacun de ses services, mais il faut creuser un peu dans les classes de CXF. En l'occurrence il suffit de manipuler directement la ProviderFactory :

<bean class="org.apache.cxf.jaxrs.provider.ProviderFactory" factory-method="getSharedInstance">
	<property name="userProviders">
		<bean class="org.codehaus.jackson.jaxrs.JacksonJsonProvider" />
	</property>
</bean>

Bref CXF qui est génial pour faire du SOAP et du RPC dans tous les sens dans des architectures hétérogènes tout en garantissant d'excellentes performances n'est ici clairement pas la solution la plus pratique à mettre en œuvre.

Jersey

C'est l'implémentation de référence de JAX-RS, simple, efficace et performante.

Afin d'utiliser Jackson avec Jersey il nous faut :

  • importer l'artefact jersey-json
  • mettre à true la propriété com.sun.jersey.api.json.POJOMappingFeature au niveau de la servlet Jersey dans le web.xml. Cette propriété indique que l'on souhaite mapper nos POJO directement sur du JSON au lieu du XML.

Ce dernier point est parfaitement expliqué dans la documentation par ailleurs assez complète.

Enfin, la configuration Spring est on ne peut plus simple puisqu'il suffit d'activer la gestion des annotations.

Dépendances

Là encore un nettoyage est nécessaire si l'on ne souhaite importer que l'essentiel :

<dependency>
	<groupId>com.sun.jersey.contribs</groupId>
	<artifactId>jersey-spring</artifactId>
	<version>1.5</version>
</dependency>
<dependency>
	<groupId>com.sun.jersey</groupId>
	<artifactId>jersey-json</artifactId>
	<version>1.5</version>
	<exclusions>
		<exclusion>
			<groupId>org.codehaus.jackson</groupId>
			<artifactId>jackson-core-asl</artifactId>
		</exclusion>
		<exclusion>
			<groupId>org.codehaus.jackson</groupId>
			<artifactId>jackson-mapper-asl</artifactId>
		</exclusion>
		<exclusion>
			<artifactId>jaxb-impl</artifactId>
			<groupId>com.sun.xml.bind</groupId>
		</exclusion>
		<exclusion>
			<artifactId>jettison</artifactId>
			<groupId>org.codehaus.jettison</groupId>
		</exclusion>
	</exclusions>
</dependency>

Jersey est donc extrêmement simple à mettre en œuvre et s'avère largement suffisant pour mon utilisation.

RESTEasy

Pour finir voici l'implémention de JBoss de JAX-RS. Ses avantages sont principalement :

  • son intégration assez poussée avec le serveur JBoss
  • son intégration avec, outre Spring, Spring-MVC, Guice, Seam

Comme les autres implémentations, RESTEasy utilise Jettison par défaut comme provider JSON. Pour utiliser Jackson, il suffit d'importer l'artefact resteasy-jackson-provider et le tour est joué.

J'ajouterai que RESTEasy fournit probablement la documentation la plus complète des trois implémentations.

Dépendances

Une fois de plus il faudra faire du nettoyage dans le POM :

<dependency>
	<groupId>org.jboss.resteasy</groupId>
	<artifactId>resteasy-spring</artifactId>
	<version>2.1.0.GA</version>
	<exclusions>
		<exclusion>
			<groupId>org.jboss.resteasy</groupId>
			<artifactId>resteasy-jaxb-provider</artifactId>
		</exclusion>
		<exclusion>
			<artifactId>resteasy-jettison-provider</artifactId>
			<groupId>org.jboss.resteasy</groupId>
		</exclusion>
	</exclusions>
</dependency>
<dependency>
	<groupId>org.jboss.resteasy</groupId>
	<artifactId>resteasy-jackson-provider</artifactId>
	<version>2.1.0.GA</version>
	<exclusions>
		<exclusion>
			<groupId>org.codehaus.jackson</groupId>
			<artifactId>jackson-core-asl</artifactId>
		</exclusion>
		<exclusion>
			<groupId>org.codehaus.jackson</groupId>
			<artifactId>jackson-mapper-asl</artifactId>
		</exclusion>
	</exclusions>
</dependency>

On notera qu'alors que l'artefact resteasy-jackson-provider n'importe bien que l'essentiel, ce qui offre un avantage intéressant comparé à ses concurrents. resteasy-spring importe malheureusement indirectement des providers via resteasy-core qu'il me faut exclure.

Restlet

Restlet est initialement une solution Java RESTFul conçue pour fonctionner de manière autonome contrairement aux frameworks précédents. Restlet propose néanmoins diverses extensions dont une offrant la possibilité de le faire fonctionner au sein d'un conteneur de servlets, une autre offrant le support JAX-RS et une autre le support de Spring. Par conséquent cette partie de l'article n'est pas forcément pertinente dans le sens où elle montre une utilisation de Restlet plutôt bancale, mais me semble néanmoins intéressante.

Dépendances

Un petit nettoyage du POM s'impose pour ne garder que l'essentiel :

<dependency>
	<groupId>org.restlet.jee</groupId>
	<artifactId>org.restlet.ext.jaxrs</artifactId>
	<version>2.0.5</version>
	<exclusions>
		<exclusion>
			<artifactId>jaxb-impl</artifactId>
			<groupId>com.sun.xml.bind</groupId>
		</exclusion>
		<exclusion>
			<artifactId>jaxb-api</artifactId>
			<groupId>javax.xml.bind</groupId>
		</exclusion>
		<exclusion>
			<artifactId>org.restlet.lib.org.json</artifactId>
			<groupId>org.restlet.jee</groupId>
		</exclusion>
		<exclusion>
			<artifactId>stax-api</artifactId>
			<groupId>javax.xml.stream</groupId>
		</exclusion>
	</exclusions>
</dependency>
<dependency>
	<groupId>org.restlet.jee</groupId>
	<artifactId>org.restlet.ext.spring</artifactId>
	<version>2.0.5</version>
	<exclusions>
		<exclusion>
			<artifactId>spring-webmvc</artifactId>
			<groupId>org.springframework</groupId>
		</exclusion>
	</exclusions>
</dependency>

Un peu de code

Il va nous falloir un minimum de code supplémentaire qui va servir de glue entre Restlet, JAX-RS, Spring et mon conteneur de servlets. Cette "glue" se limite à une classe :

@Component("jaxrsApp")
public class MyJaxrsApplication extends JaxRsApplication {
	@Autowired
	public MyJaxrsApplication(final ApplicationContext context) {
		super();
		add(new Application() {
			private Set<Class<?>> classes = new HashSet<Class<?>>();
			private Set<Object> singletons = new HashSet<Object>();
			{
				if (context.getBeansWithAnnotation(Path.class) != null)
					for (Object bean : context.getBeansWithAnnotation(Path.class).values())
						classes.add(bean.getClass());
				singletons.add(new JacksonJsonProvider());
			}

			@Override
			public Set<Class<?>> getClasses() {
				return classes;
			}

			@Override
			public Set<Object> getSingletons() {
				return singletons;
			}
		});

		setObjectFactory(new ObjectFactory() {
			public <T> T getInstance(Class<T> jaxRsClass)
					throws InstantiateException {
				return context.getBean(jaxRsClass);
			}
		});
	}
}

Je n'entrerai pas ici dans le détail du fonctionnement de Restlet - ce qui serait hors sujet -, néanmoins certains éléments nécessitent quelques précisions. La classe Application est celle de la JSR-311 dont les méthodes getClasses et getSingletons sont censées retourner les classes ou singletons correspondant à des ressources REST ou des providers. Il semblerait logique de fournir les ressources REST via la méthode getSingletons en allant les chercher dans le conteneur IoC de Spring. Malheureusement Restlet ne permet cela pour l'instant que pour les providers, les ressources devant nécessairement être fournies par la méthode getClasses. Fort heureusement, la classe JaxRsApplication possède la méthode setObjectFactory qui va nous permettre de définir de quelle manière Restlet va instancier les classes renvoyées par la méthode getClasses. Il faut ensuite configurer la servlet SpringServerServlet dans le web.xml en lui fournissant en paramètre le nom du bean Spring correspondant à l'application Restlet :

<servlet>
	<servlet-name>Restlet</servlet-name>
	<servlet-class>org.restlet.ext.spring.SpringServerServlet</servlet-class>
	<init-param>
		<param-name>org.restlet.application</param-name>
		<param-value>jaxrsApp</param-value>
	</init-param>
	<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
	<servlet-name>Restlet</servlet-name>
	<url-pattern>/*</url-pattern>
</servlet-mapping>

Conclusion

Nous voici arrivés à la fin de la première partie de ce tour d'horizon des implémentations JAX-RS.

Que peut-on conclure à l'issue de cette première partie ?

CXF est incontestablement très puissant, et justement trop en l'occurrence. D'un autre côté, si pour une raison quelconque je devais passer à une architecture hétérogène, je sais qu'un passage vers CXF serait parfaitement envisageable et que la transition se ferait sans douleurs, puisque consistant essentiellement en un travail de configuration Spring (certes rébarbatif).

Concernant Restlet, comme dit précédemment, ce n'est pas vraiment une utilisation normale du framework qui est proposée ici, celui-ci se positionnant plus comme une alternative à JAX-RS.

Jersey et RESTEasy offrent simplicité et performance et collent parfaitement à mes besoins.

A mon sens, le choix entre RESTEasy et Jersey dépendra en partie du choix du serveur d'application : si vous optez pour JBoss, RESTEasy offre des avantages certains.

D'un autre côté la communauté derrière le projet Jersey, dont la dernière version date de janvier, semble bien plus active que celle de RESTEasy.

Reste toutefois un point essentiel à aborder : les tests, que nous verrons dans un prochain article et qui pourraient bien faire pencher la balance.

En attendant vous pouvez déjà jeter un oeil aux sources.