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
<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:
<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:
<!-- 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:
<!-- 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:
<!-- 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
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:
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
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:
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):
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 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)
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:
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:
<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.
Commentaires
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 :)
great tutorial. Please could you provide us with a complete maven example project with sources attached here?
Très bon article, Merci pour cet effort.
Comme la dit webdev ça serait intéressant d'avoir le code source.
Thks.