JAX-RS web service REST avec Spring (implémentation RestEasy)

L'objet de ce billet: Illustrer avec un exemple assez complet la mise en pratique du web service REST (JAX-RS) s'appuyant sur l'implémentation RestEasy de JBoss avec Spring 2.5.
L'exemple repose sur les briques (api) suivantes. Notez bien la version lorsqu'elle est mentionnée.

  • RestEasy: L'implémentation Jboss de jax-rs (JSR 311),
  • Spring 2.5 et les annotations,
  • Hibernate pour la partie persistence
  • L'api Dozer v4.0 pour les DTO (Data Transfert Objetc) ou VO((Value Object).
  • Junit 4.4,
  • HttpUnit,
  • XMLUnit.

Nota:La version Dozer 4.0 a renommé complètement ses packages. Certains tutos sur le web sont donc caduques.
C'est à la fin de l'étape 5 que nous détaillons l'emploi de l'api Dozer.

Quelques repères:

REST (Representational State Transfer) développé par Roy Fielding qui est l’un des fondateurs du protocol HTTP.
JSR 311 est la spec JAX-RS: Java API for RESTful Web Service. Finalisée en mars 2008.

Les CINQ principes de REST

  • P1: Tout est ressource, un identifiant unique à chaque ressource (http://localhost:8888/clients/2 pointe sur le client ayant id=2),
  • P2: Utiliser les méthodes HTTP (HEAD/GET/POST/PUT/DELETE). Et les erreurs standards HTTP,
  • P3: Les échanges avec plusieurs représentations ( xml,(x)html, json,..),
  • P4: Échanges sans état (stateless),
  • P5: Lier les ressources entre elles.

PRÉ-REQUIS: Java5.

Mise en pratique

L'exemple ci-après a pour but d'aller plus loin que l'éternel "HelloWorld".
Car je trouve que le fameux "Helloworld" ne permet pas de d'aborder les notions intéressantes.

Voici donc les étapes de mise en œuvre d'un exemple assez complet. Celui-ci répond aux cas d'utilisation suivants:

- Rechercher dans la base (mysql ) un ou plusieurs contacts,
- Créer un nouveau contact,
- Mettre à jour un contact existant.

Les méthodes HTTP GET et POST seront illustrées.

LES ETAPES DU PROJETWEB SOUS MAVEN2

Etape 1: pom.xml du projet maven

Etape 2: web.xml

Etape 3: Configurer fichier de Spring

Etape 4: Classe entités (POJO) Contact.java

Etape 5: Classe DTO (Data Transfert Object) ContactDto.java.

Etape 6: Classe java ContactResource (le webservice)

Etape 7: Classe de test JUnit 4.4 & XMLUnit

Etape 8: Conclusion

PS. La suite suppose l'existence d'une base mysql avec une table nommée contact avec les champs indiqués dans le POJO Contact.java

Détaillons ensemble ces étapes:

Etape 1: Configurer pom.xml

[xml]
  <dependencies>
	<!--  DOZER  DTO Data Transfert Object -->
	<dependency>
			<groupId>net.sf.dozer</groupId>
			<artifactId>dozer</artifactId>
			<version>4.0</version>
				<exclusions>
					<exclusion>
						<groupId>commons-collections</groupId>
						<artifactId>commons-collections</artifactId>
					</exclusion>
				</exclusions>
	</dependency>
    <!--  resteasy webservice dep -->
    <dependency>
		<groupId>org.jboss.resteasy</groupId>
		<artifactId>resteasy-jaxrs</artifactId>
		<version>1.2.GA</version>
	</dependency>
	<!--  JAXB manipuler xml   -->
	<dependency>
	<groupId>org.jboss.resteasy</groupId>
	<artifactId>resteasy-jaxb-provider</artifactId>
	<version>1.2.GA</version>
   </dependency>
	<!-- spring resteasy -->
	<dependency>
	  <groupId>org.jboss.resteasy</groupId>
	  <artifactId>resteasy-spring</artifactId>
	  <version>1.2.GA</version>
	</dependency>
	<!--  SPRING -->
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-web</artifactId>
		<version>2.5.5</version>
		
	</dependency>
	<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-beans</artifactId>
			<version>2.5.5</version>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-jdbc</artifactId>
		<version>2.5.5</version>
	</dependency>		
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-orm</artifactId>
		<version>2.5.5</version>
	</dependency>		
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-tx</artifactId>
		<version>2.5.5</version>
	</dependency>	
	<!--  hibernate -->
	<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate</artifactId>
			<version>3.2.6.ga</version>
	</dependency>
	<dependency>
			<groupId>asm</groupId>
			<artifactId>asm</artifactId>
			<version>3.0</version>
	</dependency>
 
	<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-annotations</artifactId>
			<version>3.3.1.GA</version>
	</dependency>
	<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-entitymanager</artifactId>
			<version>3.3.1.ga</version>
	</dependency>
	<dependency>
			<groupId>cglib</groupId>
			<artifactId>cglib-nodep</artifactId>
			<version>2.1_3</version>
	</dependency>
	<!--  connector mysql -->    
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.6</version>
	     </dependency>
	<!-- LOG4J -->
	<dependency>
		<groupId>log4j</groupId>
		<artifactId>log4j</artifactId>
		<version>1.2.15</version>
	</dependency>

         <!-- NOTA dependences commons de jakarta OMIS -->
   
	<!--  test et spring-test -->			
	<dependency>
		      <groupId>org.springframework</groupId>
		      <artifactId>spring-test</artifactId>
		      <version>2.5.4</version>
		      <scope>test</scope>
	</dependency>   
	<dependency>
           <groupId>httpunit</groupId>
          <artifactId>httpunit</artifactId>
           <version>1.6.2</version>
           <scope>test</scope>
       </dependency>
	<dependency>
          <groupId>junit</groupId>
         <artifactId>junit</artifactId>
         <version>4.4</version>
        <scope>test</scope>
      </dependency>
     <dependency>
    	<groupId>xmlunit</groupId>
    	<artifactId>xmlunit</artifactId>
    	<version>1.2</version>
     </dependency>
  </dependencies>

Tout est commenté.
Je signale juste que j'ai choisi la version 4.4 de Junit avec Spring 2.5 pour contourner un bug de JUnit4.5 avec Spring 2.5.

Etape 2 :Le fichier web.xml de l'application web:

[xml]	
 	<web-app>
           <!-- Premiere option: Configurer les classes resources-->
		<context-param>
	        <param-name>resteasy.resources</param-name>
	        <param-value>fr.netapsys.rest.webservice.rs.ContactResource</param-value>
	    </context-param>
            <!-- PREFIX pour les appels de web service rest -->
		<context-param>
			<param-name>resteasy.servlet.mapping.prefix</param-name>
			<param-value>/rest</param-value>
		</context-param>
	   <!--  LISTENERS  ATTENTION L'ORDRE DES LISTENERS EST IMPORTANT-->
		<listener>
	  	 <listener-class>
	  			org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap
	  	 </listener-class>
		</listener>
	    <!-- Spring listener -->
		<listener>
	  	 <listener-class>
	       org.jboss.resteasy.plugins.spring.SpringContextLoaderListener
	     </listener-class>
	    </listener>		
	    <!--Servlet RESTeasy -->
		<servlet>
				<servlet-name>Resteasy</servlet-name>
				<servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
	 	</servlet>			
		<!-- Les urls debutant par /rest/ seront traitees par la servlet RESTeasy-->
		<servlet-mapping>
				<servlet-name>Resteasy</servlet-name>
				<url-pattern>/rest/*</url-pattern>
		</servlet-mapping>		
 </web-app>

Les lignes sont commentées clairement.

Etape 3: Configurer le fichier de spring nommé applicationContext.xml. Il importe deux autres : spring.xml et dozer_spring.xml:

[xml]
	<!-- file applicationContext.xml sous WEB-INF.xml -->
	<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xmlns:p="http://www.springframework.org/schema/p" 
                xmlns:context="http://www.springframework.org/schema/context"
		xsi:schemaLocation="http://www.springframework.org/schema/beans 
                http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
		http://www.springframework.org/schema/context 
                http://www.springframework.org/schema/context/spring-context-2.5.xsd" 
                default-autowire="byName">
  <import resource="classpath:spring.xml"/>
  <import resource="classpath:dozer_spring.xml"/>
</beans>

Le fichier spring.xml localisé sous src/main/resources/ et il doit contenir:

[xml]
<!-- ENTETE OMIS -->
	<!--   packages autowiring -->
 	<context:component-scan base-package="fr.netapsys.rest"/>
	<context:property-placeholder location="classpath:rest.properties" />
	<bean id="dataSource"
		class="org.springframework.jdbc.datasource.DriverManagerDataSource"
		p:driverClassName="${datasource.driver}" p:url="${datasource.url}"
		p:username="${datasource.username}" p:password="${datasource.password}" />
	<tx:annotation-driven transaction-manager="transactionManager" />
	<bean id="transactionManager"
		class="org.springframework.orm.hibernate3.HibernateTransactionManager">
		<property name="sessionFactory">
			<ref bean="sessionFactory" />
		</property>
	</bean>
	<bean id="sessionFactory"
		class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
		<property name="dataSource">
			<ref bean="dataSource" />
		</property>
		<property name="hibernateProperties">
			<props>
				<prop key="hibernate.dialect">${hibernate.dialect}</prop>
				<prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>
				<prop key="hibernate.jdbc.batch.size">${hibernate.jdbc.batch.size}</prop>
				<prop key="hibernate.show.sql">${hibernate.show.sql}</prop>
				
			</props>
		</property>
		<property name="mappingResources">
      	<list>
        <value>Contact.hbm.xml</value>
      	</list>
    </property>	
		<property name="annotatedClasses">
			<list>
				<value>fr.netapsys.rest.entites.Contact</value>
			</list>
		</property>
	</bean>
	<bean id="hibernateTemplate" class="org.springframework.orm.hibernate3.HibernateTemplate">
		<property name="sessionFactory">
			<ref bean="sessionFactory" />
		</property>
	</bean>
	<bean id="hibernateDao"
		class="org.springframework.orm.hibernate3.support.HibernateDaoSupport"
		abstract="true">
		<property name="sessionFactory">
			<ref bean="sessionFactory" />
		</property>
	</bean>
</beans>

Pour rappel, les variables ${} sont à déclarer dans le fichier jdbc.properties qui n'est pas détaillé ici.

Enfin voici le contenu de dozer_spring.xml localisé sous src/main/resources:

[xml]
<!-- entete omis-->

	<bean id="dozerBeanMapper"
		class="net.sf.dozer.util.mapping.DozerBeanMapper">
		<!-- OPTIONNEL car les noms des attributs sont identiq -->
            <!--
                <property name="mappingFiles">
			<list>
				<value>dozerBeanMapping.xml</value>
			</list>
		</property>
            -->
	</bean>
</beans>

Pour être complet, nous reviendrons sur le fichier de configuration de Dozer nommé dozerBeanMapping.xml a la fin de l'étape5.

Etape 4: Classe entités (POJO) Contact.java

[java]
package fr.netapsys.rest.entites;
import java.util.Date;
import fr.netapsys.rest.common.BaseObject;
public class Contact extends BaseObject  {
	private static final long serialVersionUID = 1L;
	private int id;
	private String nom;
	private String prenom;
	private String mail;
	private Date date;
	
	public Contact() {
	}	
	//setters/getters omis...
} 

A signaler que la classe BaseObject du package commons d'Apache permet de définir toString comme suit:

[java]
package fr.netapsys.rest.common;
import org.apache.commons.lang.builder.*;
import java.io.Serializable;
public class BaseObject implements Serializable {
	private static final long serialVersionUID = 1L;
	public String toString() {
		return ToStringBuilder.reflectionToString(this,
				ToStringStyle.MULTI_LINE_STYLE);
	}
}

Etape 5: Classe DTO ContactDto.java

[java]
package fr.netapsys.rest.dto;
import java.util.Date;
import javax.xml.bind.annotation.*;
import fr.netapsys.rest.common.BaseObject;
@XmlRootElement(name="contact")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(propOrder = { "id", "nom","prenom","mail","date",})
public class ContactDto extends BaseObject {
	private static final long serialVersionUID = 1L;
	@XmlElement(name = "identifant", required = true) private int id;
	@XmlElement(name = "nom", required = true) private String nom;
	@XmlElement(name = "prenom", required = false) private String prenom;
	@XmlElement(name = "email", required = false) private String mail;
	@XmlElement(name = "date", required = false) private Date date;
	//getters / setters omis...			
}

Comme vous le constatez c'est dans cette classe DTO que les annotations JAXB sont introduites.

Dozer pour faire du mapping de BEANS

Nous allons comme promis revenir à l'explication de l'api Dozer et de son fichier de config dozerBeanMapping.xml.
L'api Dozer permet de s'affranchir de la tâche fastidieuse de recopier les valeurs des variables (attributs) d'un bean vers un autre.
Ceux ayant travaillé avec des frameworks MVC ont été amené à écrire plusieurs lignes de code rien que pour recopier les variables d'une entité de persistence vers des bean de la couche de présentation.
C'est bien évidemment fastidieux, répétitif et complètement inutile sans parler du temps perdu consacré à corriger les bugs.
Pour faire simple, deux lignes de code avec l'api Dozer:

[java]
DozerBeanMapper dozerBeanMapper=new DozerBeanMapper (); 

TargetObject targetObj = (TargetObject) dozerBeanMapper.map(SourceObject,TargetObject.class);

Ce qui établit le mapping bidirectionnel entre la classe TargetObject et la classe source SourceObject.
Si nous l'appliquons à notre cas, ceci donne (voir étape 6 à deux endroits):

[java]
ContactDto dto=(ContactDto) dozerBeanMapper.map(contact,ContactDto.class);
//ou encore
Contact contact = (Contact) dozerBeanMapper.map(contactDto,Contact.class);

Le fichier de config de Dozer, dans notre exemple, est vide et il n'est donc pas nécessaire de le déclarer.
Il doit, lorsque ces attributs ne portent pas les mêmes noms, contenir les noms de classes concernées par le mapping et les correspondances entre les attributs.
Auquel cas, le fichier doit ressembler à l'extrait suivant:

[xml]
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mappings PUBLIC "-//DOZER//DTD MAPPINGS//EN"
   "http://dozer.sourceforge.net/dtd/dozerbeanmapping.dtd">
<mappings>
	<mapping>
               <class-a>fr.netapsys.rest.entites.Contact</class-a>
		<class-b>fr.netapsys.rest.dto.ContactDto</class-b>
	</mapping>
          <field>
		      <a>NOM_ATTRIBUT_a</a>
		      <b>NOM_ATTRIBUT_b</b>
	 </field>	
          .....
</mappings>

Etape 6: Classe java ContactResource (classe du web service Rest)

[java]
package fr.netapsys.rest.webservice.rs;
import java.net.URI;
import java.util.List;
import javax.ws.rs.*;
import net.sf.dozer.util.mapping.DozerBeanMapper;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import fr.netapsys.rest.dto.*;
import fr.netapsys.rest.entites.Contact;
import fr.netapsys.rest.interfaces.IContactService;
import fr.netapsys.rest.webservice.rs.utils.UtilsMapper;
@Component
@Path("/contacts")
public class ContactResource {
	private Logger logger = Logger.getLogger(fr.netapsys.rest.webservice.rs.ContactResource.class);	
	IContactService contactService;
	@Autowired
	public void setContactService(IContactService contactService) {
		this.contactService = contactService;
	}
	private DozerBeanMapper dozerBeanMapper;
	@Autowired
	public void setDozerBeanMapper(DozerBeanMapper dozerBeanMapper) {
		this.dozerBeanMapper = dozerBeanMapper;
	}
	@GET
	@Path("/{id}")
	@Produces (MediaType.APPLICATION_XML)
	public  ContactDto getContactById(@PathParam("id") int id)  {
		logger.debug("Call getById with id : " + id);
		Contact contact = contactService.find(id);
		return (ContactDto) dozerBeanMapper.map(contact,ContactDto.class);
	}

	@POST
	@Path("/create/")
	@Produces(MediaType.APPLICATION_XML)
	@Consumes(MediaType.APPLICATION_XML)
	public Response post4CreateNewContact(final ContactDto contactDto,@Context UriInfo uriInfo) {
		Contact contact = (Contact) dozerBeanMapper.map(contactDto,Contact.class);
		contactService.save(contact);
		contactDto.setId(contact.getId());		
		// Building URI: recuperer le path courant
		UriBuilder uriBuilder = uriInfo.getAbsolutePathBuilder();
		//Cree l uri pour acceder à la new resource contactDto 
		URI uri = uriBuilder.path( String.valueOf(contactDto.getId())).build();
		//retourne la reponse avec la new resource contactDto
		return Response.created(uri).entity(contactDto).build();
	}
}

La ligne Contact contact = (Contact) dozerBeanMapper.map(contactDto,Contact.class); établit le mapping bidirectionnel via l'api dozer entre la classe de persistence Contact et la le pojo ContactDto qui est exposé à la couche de présentation.

NOTE: Les classes des couches Service et Dao: ContactDao et ContactService.java sont bien ordinaires et ne sont pas détaillées ici.

Etape 7: Classe de test JUnit & XMLUnit

L'exécution de ces tests nécessitent deux pré requis:

 # D'avoir une instance de la base MYSQL  démarrée. On suppose que la table CONTACT contient au moins une ligne. Utiliser l'identifiant de cette ligne pour la méthode testContactGetresource.
#  D'exécuter, via une console dos la commande mvn jetty:run afin de lancer le serveur web, puis dans l'environnement Eclipse exécute le test JUnit.

Bien évidemment, on peut rendre ces tests d'intégration automatisés. Dans un prochain billet je détaillerai les étapes de configuration.

Le code de la classe Junit est comme suit:

[java]

public class TestContactResourceTest extends XMLTestCase{
	final static String URL_BASE="http://localhost:8888/restspring/rest/";
	@Test
	public void testContactGetResource() throws HttpException, IOException, SAXException{
		final String id="1";
		GetMethod method = new GetMethod(URL_BASE + "contacts/"+id+"/");
		method.setRequestHeader("Accept", MediaType.APPLICATION_XML);
		HttpClient client = new HttpClient();
		int status=client.executeMethod(method);
		assertEquals(200, status);
		String str = method.getResponseBodyAsString();
		method.releaseConnection();
		final String responseExpected="<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"+
		"<contact><identifant>"+ id +
		"</identifant><nom>ach</nom><prenom>ab</prenom><email>a.chine@netapsys.fr</email><date>2009-10-18T00:00:00Z</date></contact>";
		assertXMLEqual(str,responseExpected );
	}
        // Utilisation de la methode POSt pour créer un contact
	 @Test 
	 public void testCreateContact() throws HttpException, IOException{

		final String request2Post="<contact><identifant/><nom>TEST130110</nom><prenom>ab</prenom><email>a.chine@netapsys.fr</email><date>2010-01-13T00:00:00Z</date></contact>";

		PostMethod method = new PostMethod(URL_BASE + "contacts/create/");
		method.setRequestEntity(
				new StringRequestEntity(request2Post,"application/xml", null));
				HttpClient client = new HttpClient();
				int status = client.executeMethod(method);
				method.releaseConnection();
				assertEquals(HttpStatus.SC_CREATED, status);
	}
}

Notez que la classe de test étend XMLTestCase afin de pouvoir de profiter de l'api XmlUnit et tester les retours XML des appels aux webservice ContactResource.
Notez que vous pouvez lancer, sous la console dos, la commande mvn jetty:run; puis, dans le navigateur Firefox ou IE, saisir l'url suivante:
http://localhost:8888/restspring/rest/contacts/9

Et vous obtiendrez la sortie xml ci-après:

[xml]
<contact>
<identifant>9</identifant>
<nom>ach</nom>
<prenom>ab</prenom>
<email>a.chine@netapsys.fr</email>
<date>2009-10-18T00:00:00Z</date>
</contact>

Etape 8: Conclusion

Ce billet montre la facilité de création des webservices REST avec l'implémentation opensource RestEasy de Jboss combiné avec Spring.
La classe de test, qui s'appuie sur l'api HttpClient, prouve tout l'intérêt de pratiquer les tests d'intégration qui peuvent être facilement automatisés en les insérant dans un "goal maven".

Enfin, signalons que certaines étapes ne sont pas suffisamment explicitées néanmoins je pourrais y revenir avec plus de détails si vous le demandez.

Annexe:

Voici les sources du projet en zip restspring.zip.

7 commentaires

  1. très bon billet
    j’apprécie beaucoup Dozer, je viens de l’essayer sur la conversion des VOs Externalizable vers les VOs Serialisable dans un projet flex-Spring; ça marche impecc ! avant c’était une vrais galère surtout avec des VOs de plus de 40 attribues
    Merci 🙂

  2. Très bon article, Merci pour cet effort.
    Comme la dit webdev ça serait intéressant d’avoir le code source.
    Thks.

  3. Merci bcq pour ce tutorial.
    Tres bon boulot
    Malheureusement les sources du projet fournies dans le zip ne correspondent pas au tutorial

Laisser un commentaire

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

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.