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 :

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>war</packaging>

<artifactId>sample-jax-ws-spring</artifactId>
<groupId>fr.ippon.sandbox</groupId>
<version>1.0-SNAPSHOT</version>

<name>Ippon Sandbox - Sample JAX WS spring</name>

<!-- ================= -->
<!-- = Build plugins = -->
<!-- ================= -->
<build>

<!-- Definition des options de compilation -->
<plugins>
  <plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
      <source>1.6</source>
      <target>1.6</target>
      <encoding>ISO-8859-1</encoding>
      <debug>true</debug>
    </configuration>
  </plugin>
  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-resources-plugin</artifactId>
    <configuration>
      <encoding>ISO-8859-1</encoding>
    </configuration>
  </plugin>

</plugins>

</build>

<!-- ================= -->
<!-- = Dependencies = -->
<!-- ================= -->
<dependencies>

  <!-- Spring dependencies -->
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>2.5.5</version>
  </dependency>

  <!-- Spring remoting dependencies -->
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-remoting</artifactId>
    <version>2.0.8</version>
  </dependency>
  <dependency>
    <groupId>javax.xml</groupId>
    <artifactId>jaxrpc-api</artifactId>
    <version>1.1</version>
  </dependency>

  <!-- Spring web services dependencies -->
  <dependency>
    <groupId>org.springframework.ws</groupId>
    <artifactId>spring-ws-core</artifactId>
    <version>1.5.8</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.ws</groupId>
    <artifactId>spring-xml</artifactId>
    <version>1.5.8</version>
  </dependency>

  <!-- JAX-WS dependencies -->
  <dependency>
    <groupId>org.jvnet.jax-ws-commons.spring</groupId>
    <artifactId>jaxws-spring</artifactId>
    <version>1.8</version>
    <exclusions>
      <exclusion>
        <groupId>org.springframework</groupId>
        <artifactId>spring</artifactId>
      </exclusion>
      <exclusion>
        <groupId>com.sun.xml.stream.buffer</groupId>
        <artifactId>streambuffer</artifactId>
      </exclusion>
      <exclusion>
        <groupId>org.jvnet.staxex</groupId>
        <artifactId>stax-ex</artifactId>
      </exclusion>
    </exclusions>
  </dependency>
  <dependency>
    <artifactId>streambuffer</artifactId>
    <groupId>com.sun.xml.stream.buffer</groupId>
    <version>1.0</version>
  </dependency>
  <dependency>
    <groupId>com.sun.xml.ws</groupId>
    <artifactId>jaxws-rt</artifactId>
    <version>2.2</version>
    <exclusions>
      <exclusion>
        <groupId>com.sun.istack</groupId>
        <artifactId>istack-commons-runtime</artifactId>
      </exclusion>
      <exclusion>
        <groupId>woodstox</groupId>
        <artifactId>wstx-asl</artifactId>
      </exclusion>
    </exclusions>
  </dependency>
  <dependency>
    <groupId>com.sun.istack</groupId>
    <artifactId>istack-commons-runtime</artifactId>
    <version>2.2</version>
  </dependency>

  <!-- Web -->
  <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jsp-api</artifactId>
    <version>2.0</version>
    <scope>provided</scope>
  </dependency>
  <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.4</version>
    <scope>provided</scope>
  </dependency>
  <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.1.2</version>
    <scope>runtime</scope>
  </dependency>

  <!-- Commons dependencies -->
  <dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.1.1</version>
    <exclusions>
      <exclusion>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
      </exclusion>
      <exclusion>
        <groupId>logkit</groupId>
        <artifactId>logkit</artifactId>
      </exclusion>
      <exclusion>
        <groupId>avalon-framework</groupId>
        <artifactId>avalon-framework</artifactId>
      </exclusion>
      <exclusion>
        <groupId>javax.servlet</groupId>
        <artifactId>servlet-api</artifactId>
      </exclusion>
    </exclusions>
  </dependency>
  <dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.13</version>
  </dependency>

  <!-- Testing -->
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.4</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>2.5.5</version>
    <scope>test</scope>
  </dependency>

</dependencies>

<!-- ================ -->
<!-- = repositories = -->
<!-- ================ -->
<repositories>
  <repository>
    <id>jboss</id>
    <url>http://repository.jboss.org/maven2</url>
  </repository>
  <repository>
    <id>Java.net-maven2</id>
    <url>http://download.java.net/maven/2</url>
  </repository>
</repositories>

</project>

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<sample> samples = new ArrayList<sample>();
    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
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

<bean id="sampleService" class="fr.ippon.sandbox.sample.service.SampleServiceImpl" />

</beans>

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
<?xml version='1.0'?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ws="http://jax-ws.dev.java.net/spring/core"
xmlns:wss="http://jax-ws.dev.java.net/spring/servlet"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://jax-ws.dev.java.net/spring/core http://jax-ws.dev.java.net/spring/core.xsd http://jax-ws.dev.java.net/spring/servlet http://jax-ws.dev.java.net/spring/servlet.xsd">

<bean id="sampleWebService" class="fr.ippon.sandbox.sample.ws.SampleWebServiceImpl" scope="prototype">
  <property name="sampleService" ref="sampleService" />
</bean>

<wss:binding url="/services/sampleWebService">
  <wss:service>
    <ws:service bean="#sampleWebService" />
  </wss:service>
</wss:binding>

</beans>

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

Paramétrage de l’application

  • le fichier applicationContext.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

<import resource="applicationContext-service.xml" />
<import resource="applicationContext-ws.xml" />

</beans>
  • le web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">

<display-name>jax-ws-spring</display-name>

<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>classpath*:/context/applicationContext.xml</param-value>
</context-param>

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- these are for JAX-WS -->
<servlet>
  <servlet-name>jaxws-servlet</servlet-name>
  <servlet-class>com.sun.xml.ws.transport.http.servlet.WSSpringServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>jaxws-servlet</servlet-name>
  <url-pattern>/services/*</url-pattern>
</servlet-mapping>

</web-app>

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.

<build>
...
<plugins>
  <plugin>
    <groupId>org.mortbay.jetty</groupId>
    <artifactId>maven-jetty-plugin</artifactId>
    <version>6.1.10</version>
    <configuration>
      <scanIntervalSeconds>10</scanIntervalSeconds>
      <stopKey>STOP</stopKey>
      <stopPort>9999</stopPort>
    </configuration>
  </plugin>
</plugins>
...
</build>

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

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsp="http://www.w3.org/ns/ws-policy"
xmlns:wsp1_2="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:tns="http://wwww.ippon.fr/sample/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.xmlsoap.org/wsdl/"
targetNamespace="http://wwww.ippon.fr/sample/" name="sampleWebService">
<types>
  <xsd:schema>
    <xsd:import namespace="http://wwww.ippon.fr/sample/" schemaLocation="http://localhost:8080/sample-jax-ws-spring/services/sampleWebService?xsd=1"></xsd:import>
  </xsd:schema>
</types>
<message name="findSamples">
  <part name="arg0" type="xsd:string"></part>
</message>
<message name="findSamplesResponse">
  <part name="return" type="tns:sampleArray"></part>
</message>
<portType name="SampleWebServiceImpl">
  <operation name="findSamples">
    <input wsam:Action="http://wwww.ippon.fr/sample/SampleWebServiceImpl/findSamplesRequest" message="tns:findSamples"></input>
    <output wsam:Action="http://wwww.ippon.fr/sample/SampleWebServiceImpl/findSamplesResponse" message="tns:findSamplesResponse"></output>
  </operation>
</portType>
<binding name="sampleWebServicePortBinding" type="tns:SampleWebServiceImpl">
  <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="rpc"></soap:binding>
  <operation name="findSamples">
    <soap:operation soapAction=""></soap:operation>
    <input>
      <soap:body use="literal" namespace="http://wwww.ippon.fr/sample/"></soap:body>
    </input>
    <output>
      <soap:body use="literal" namespace="http://wwww.ippon.fr/sample/"></soap:body>
    </output>
  </operation>
</binding>
<service name="sampleWebService">
  <port name="sampleWebServicePort" binding="tns:sampleWebServicePortBinding">
    <soap:address location="http://localhost:8080/sample-jax-ws-spring/services/sampleWebService"></soap:address>
  </port>
</service>
</definitions>

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.

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

<bean id="sampleWebService" class="org.springframework.remoting.jaxws.JaxWsPortProxyFactoryBean">
  <property name="wsdlDocumentUrl" value="http://localhost:8080/sample-jax-ws-spring/services/sampleWebService?wsdl"/>
  <property name="namespaceUri" value="http://wwww.ippon.fr/sample/"/>
  <property name="serviceInterface" value="fr.ippon.sandbox.sample.ws.SampleWebService"/>
  <property name="serviceName" value="sampleWebService"/>
  <property name="portName" value="sampleWebServicePort"/>
</bean>
</beans>

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 :

...
  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
      <excludes><exclude>**/*Test.java</exclude></excludes>
    </configuration>
    <executions>
      <execution>
        <id>integration-tests</id>
        <phase>integration-test</phase>
        <goals><goal>test</goal></goals>
        <configuration>
          <skip>false</skip>
          <excludes><exclude>none</exclude></excludes>
          <includes><include>**/*Test.java</include></includes>
        </configuration>
      </execution>
    </executions>
  </plugin>

  <plugin>
    <groupId>org.mortbay.jetty</groupId>
    <artifactId>maven-jetty-plugin</artifactId>
    <version>6.1.10</version>
    <configuration>
      <scanIntervalSeconds>10</scanIntervalSeconds>
      <stopKey>STOP</stopKey>
      <stopPort>9999</stopPort>
    </configuration>
    <executions>
      <execution>
        <id>start-jetty</id>
        <phase>pre-integration-test</phase>
        <goals><goal>run</goal></goals>
        <configuration>
          <scanIntervalSeconds>0</scanIntervalSeconds>
          <daemon>true</daemon>
        </configuration>
      </execution>
      <execution>
        <id>stop-jetty</id>
        <phase>post-integration-test</phase>
        <goals><goal>stop</goal></goals>
      </execution>
    </executions>
</plugin>

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.

Tweet about this on TwitterShare on FacebookGoogle+Share on LinkedIn

2 réflexions au sujet de « Web service code first avec jax-ws »

  1. Article très intéressant et très clair !

    N’étant pas un spécialiste, je me pose la question de l’importance de l’annotation suivante dans la partie serveur du web service :
    <code>@SOAPBinding(style = Style.RPC, use = Use.LITERAL) </code>

    Si on supprime cette ligne, on peut manipuler des listes java sans souci. De plus, tous les paramètres de chaque méthode du service sont encapsulées dans un conteneur ce qui permet de manipuler des données potentiellement nulles.

Laisser un commentaire

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


*