Web service code first avec jax-ws

Les projets évoluent durant les développements, l’architecture aussi…

Rappel des faits

Lorsque notre projet a débuté, nous avons utilisé une architecture standard : portail Liferay, portlet applicative contenant les jar dont elle a besoin (y compris les jar d’accès à la couche métier), le tout se basant sur le couple magique Spring / Hibernate.

Mais voilà, au fur et à mesure de l’avancement du projet, nous nous sommes rendu compte que :

  • Cette architecture comportait quelques failles (notamment la gestion du cache, de la taille des portlets et de l’impact d’une modification d’un jar métier sur le re-déploiement de toutes les portlets).
  • De nouveaux besoins sont apparus comme l’ouverture de notre système à une application externe (Intalio en l’occurence), le seul moyen de communication étant les web services.

Le constat était clair : il faut migrer vers une architecture en web services avec des portlets ne comprenant que les jar de présentation, les composants métiers étant regroupés au sein d’une web app, la communication entre ces deux couches étant réalisée par l’intermédiaire de web services.

Le choix du “code first”

Nombreux sont les articles sur la toile présentant l’approche “contract first” comme la bonne pratique à utiliser pour la mise en oeuvre des web services. Spring offre d’ailleurs un outillage très intéressant dans ce domaine Spring Web Services – Reference Documentation.

Théoriquement cette solution est la plus pertinente mais voilà, nous ne sommes pas en début de projet :

  • les délais sont, comme dans la majorité des projets, serrés.
  • tous les services métiers existent déjà. Dans l’optique de la mise en oeuvre “contract first” il serait nécessaire de définir les xsd adéquats, le marshalling/unmarshalling, …

Nos recherches se sont donc aiguillées en fonction des critères de temps et lourdeur de mise en oeuvre ; au bout d’un certains temps, il faut savoir devenir pragmatique. Nous sommes alors tombés sur jax-ws, qui répondait pratiquement à tous nos critères (hormis le best practices).

Cet article va présenter la mise en oeuvre de web services avec spring jax-ws. Il existe déjà certains posts sur la toile sur ce sujet mais aucun n’est utilisable tel quel (notamment au niveau des dépendances).

**Le pom.xml

**

Les exemples d’utilisation de jax-ws avec Spring sont nombreux sur la toile mais aucun de précise exactement les dépendances à inclure au niveau du projet. Le plus délicat a donc été de concevoir le pom.xml suivant :

4.0.0 war sample-jax-ws-spring fr.ippon.sandbox 1.0-SNAPSHOT Ippon Sandbox - Sample JAX WS spring maven-compiler-plugin 1.6 1.6 ISO-8859-1 true org.apache.maven.plugins maven-resources-plugin ISO-8859-1 org.springframework spring-web 2.5.5 org.springframework spring-remoting 2.0.8 javax.xml jaxrpc-api 1.1 org.springframework.ws spring-ws-core 1.5.8 org.springframework.ws spring-xml 1.5.8 org.jvnet.jax-ws-commons.spring jaxws-spring 1.8 org.springframework spring com.sun.xml.stream.buffer streambuffer org.jvnet.staxex stax-ex streambuffer com.sun.xml.stream.buffer 1.0 com.sun.xml.ws jaxws-rt 2.2 com.sun.istack istack-commons-runtime woodstox wstx-asl com.sun.istack istack-commons-runtime 2.2 javax.servlet jsp-api 2.0 provided javax.servlet servlet-api 2.4 provided javax.servlet jstl 1.1.2 runtime commons-logging commons-logging 1.1.1 log4j log4j logkit logkit avalon-framework avalon-framework javax.servlet servlet-api log4j log4j 1.2.13 junit junit 4.4 org.springframework spring-test 2.5.5 test jboss http://repository.jboss.org/maven2 Java.net-maven2 http://download.java.net/maven/2

La couche business

Afin d’être le plus proche possible d’une véritable application, nous utilisons la couche business suivante :

  • Le POJO

package fr.ippon.sandbox.sample.model; import java.util.Date; public class Sample { private String code; private String label; private Date date; public void setCode(String code) { this.code = code; } public String getCode() { return code; } public void setLabel(String label) { this.label = label; } public String getLabel() { return label; } public void setDate(Date date) { this.date = date; } public Date getDate() { return date; } }

  • L’interface du service métier

package fr.ippon.sandbox.sample.service; import fr.ippon.sandbox.sample.model.Sample; public interface SampleService { Sample[] findSamples(String aCode); }

  • L’implémentation du service métier

package fr.ippon.sandbox.sample.service; import java.util.ArrayList; import java.util.Date; import java.util.List; import fr.ippon.sandbox.sample.model.Sample; public class SampleServiceImpl implements SampleService { public Sample[] findSamples(String aCode) { List samples = new ArrayList(); samples.add(createSample("1")); samples.add(createSample("2")); samples.add(createSample("3")); return (Sample[])samples.toArray(new Sample[] {}); } private Sample createSample(String aValue) { Sample sample = new Sample(); sample.setCode("Code " + aValue); sample.setLabel("Label " + aValue); sample.setDate(new Date(System.currentTimeMillis())); return sample; } }

  • Le fichier applicationContext-service.xml

Exposition du service en tant que web service

Maintenant que nous avons défini la couche business, nous allons exposer le service Sample[] findSamples(String) en tant que web services.

  • Définition de l’interface du web service

package fr.ippon.sandbox.sample.ws; import javax.jws.WebService; import javax.jws.soap.SOAPBinding; import javax.jws.soap.SOAPBinding.Style; import javax.jws.soap.SOAPBinding.Use; import fr.ippon.sandbox.sample.model.Sample; @WebService(targetNamespace="http://wwww.ippon.fr/sample/", serviceName = "sampleWebService", portName = "sampleWebServicePort") @SOAPBinding(style=Style.RPC, use=Use.LITERAL) public interface SampleWebService { /** * @see fr.nantesmetropole.sandbox.sample.service.SampleServiceImpl#findSamples(java.lang.String) */ public Sample[] findSamples(String aCode); }

  • Définition de l’implémentation du web service

package fr.ippon.sandbox.sample.ws; import javax.jws.WebMethod; import javax.jws.WebService; import javax.jws.soap.SOAPBinding; import javax.jws.soap.SOAPBinding.Style; import javax.jws.soap.SOAPBinding.Use; import fr.ippon.sandbox.sample.model.Sample; import fr.ippon.sandbox.sample.service.SampleService; import fr.ippon.sandbox.sample.service.SampleServiceImpl; @WebService(targetNamespace="http://wwww.ippon.fr/sample/", serviceName = "sampleWebService", portName = "sampleWebServicePort") @SOAPBinding(style=Style.RPC, use=Use.LITERAL) public class SampleWebServiceImpl extends SampleServiceImpl implements SampleWebService { /** * The business service to call / private SampleService sampleService; /* * @see fr.nantesmetropole.sandbox.sample.ws.SampleWebService#findSamples(java.lang.String) / @Override public Sample[] findSamples(String aCode) { return getSampleService().findSamples(aCode); } /* * @param sampleService the sampleService to set / @WebMethod(exclude=true) public void setSampleService(SampleService sampleService) { this.sampleService = sampleService; } /* * @return the sampleService */ @WebMethod(exclude=true) public SampleService getSampleService() { return sampleService; } }

Il est important que les déclarations (Cf annotations utilisées) soient strictement identiques entre l’interface et l’implémentation.

L’annotation @WebService permet de préciser la déclaration du web service tel qu’il apparaîtra dans le fichier WSDL.

L’annotation @WebMethod(exclude=true) permet d’exclure la méthode de l’introspection de JAX-WS qui expose les web services.

  • Le fichier applicationContext-ws.xml

la balise wss:binding permet d’exposer un bean sous la forme de web service.

Paramétrage de l’application

  • le fichier applicationContext.xml
  • le web.xml
jax-ws-spring contextConfigLocation classpath*:/context/applicationContext.xml org.springframework.web.context.ContextLoaderListener jaxws-servlet com.sun.xml.ws.transport.http.servlet.WSSpringServlet jaxws-servlet /services/*

**Vérification **

  • Intégration d’un serveur jetty

Pour réaliser nos tests, nous allons utiliser le serveur Jetty. Pour cela il est nécessaire de modifier le pom.xml du projet.

... org.mortbay.jetty maven-jetty-plugin 6.1.10 10 STOP 9999 ...

Il suffit ensuite de lancer le serveur jetty grâce à la commande mvn jetty:run.

  • Vérification de la disponibilité du web service

Pour afficher le wsdl correspondant au web service, il suffit d’aller à l’url http://localhost:8080/sample-jax-ws-spring/services/sampleWebService?wsdl

Le client

Nul besoin de développer des classes spécifiques pour créer un client, tout est réalisé par l’intermédiaire d’une configuration Spring.

Tous les paramètres déclarés correspondent à ceux présents dans le fichier wsdl exposé.

Pour tester l’ensemble, nous développons le test unitaire suivant :

package fr.ippon.sandbox.sample; import junit.framework.Assert; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; import fr.ippon.sandbox.sample.model.Sample; import fr.ippon.sandbox.sample.ws.SampleWebService; @ContextConfiguration(locations = {"classpath:/context/applicationContext-test.xml"}) public class SampleWebServiceTest extends AbstractJUnit4SpringContextTests { private Log logger = LogFactory.getLog(getClass()); @Autowired @Qualifier("sampleWebService") private SampleWebService sampleWebService; @Test public void callWebService() { if (logger.isDebugEnabled()) { logger.debug("call web service"); } Sample[] samples = sampleWebService.findSamples("A code"); Assert.assertNotNull("The list must be not null", samples); Assert.assertTrue("The liste must contains items", samples.length>0); if (logger.isDebugEnabled()) { logger.debug(samples.length + " items."); for (Sample sample:samples) { logger.debug("sample : " + sample.getCode() + ", " + sample.getLabel() + "," + sample.getDate() ); } } } }

Pensez à lancer le serveur jetty avant de lancer le test unitaire. Mais si vous êtes un bon ingénieur, vous êtes faignant … donc pour éviter de lancer d’abord le serveur jetty avant l’exécution du test unitaire, il est nécessaire de modifier le pom.xml de la manière suivante :

... org.apache.maven.plugins maven-surefire-plugin /*Test.java integration-tests integration-test test false none /*Test.java org.mortbay.jetty maven-jetty-plugin 6.1.10 10 STOP 9999 start-jetty pre-integration-test run 0 true stop-jetty post-integration-test stop

Vous trouverez en fichier attaché l’ensemble des sources.

Bien que l’utilisation de jax-ws nous ait rendu un grand (web) service, il a tout de même fallu légèrement modifier notre modèle afin de transformer les listes (java.util.List) en tableau d’objets.

Blog à part j’espère que ce post pourra aider l’une ou l’un d’entre vous.