Web services SOAP avec Java 6 (JAX-WS)

Hyacinthe MENIET
www.dotmyself.net

Les Web services permettent par exemple à des programmes Java d'appeler des procédures .Net à distance ou d'échanger des messages asynchrones avec ces derniers. Les messages sont généralement du XML et ils transitent via le protocole HTTP. De manière générale les Web services peuvent être considérés comme un moyen du Web sémantique. C'est à dire qu'ils appartiennent aux technologies utilisables via HTTP et qui proposent du contenu compréhensible par des programmes.

Pour votre information, sachez que l'implémentation des Web services dans Java 6 (e.g. JAX-WS pour Java APIs for XML Web Services) est conforme au WS-I Basic Profile en version 1.1. Dans ce document je vais expliquer comment déployer un Web service SOAP+WSDL sur Tomcat 6. Puis j'indiquerai comment consommer ce Web service depuis un programme Java.

1. Pré-requis

2. Vue d'ensemble

2.1 Présentation rapide des Web services SOAP

SOAP permet de construire des Web services orientés action. C'est-à-dire qu'avec SOAP vous vous concentrez sur les actions que vous pourriez effectuer plutôt que sur les ressources sur lesquelles elles agissent. Un exemple simple d'un service orienté action serait une transaction bancaire dans laquelle un client transfère des fonds d'un compte vers un autre. Dans ce cas de figure, le client ne souhaite pas manipuler directement les ressources (l'argent et les comptes bancaires), il veut simplement passer un ordre et entend que la banque fasse ce qu'il faut pour qu'il soit satisfait.

Parce que les Web services SOAP sont orientés action, les services qu'ils proposent (ici les actions) sont fortement liés à l'activité. C'est ainsi qu'un Web service bancaire ne proposera pas les mêmes services qu'un Web service bibliothécaire.

Ci-dessous un exemple de requête SOAP :

POST /ws/soap.php HTTP/1.1
Host: www.dotmyself.net
Content-Type: text/xml; charset=utf-8
Content-Length: 19
SOAPAction: "http://www.dotmyself.net/HelloWorld"

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
 xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
   <soap:Body>
     <HelloWorld xmlns="http://www.dotmyself.net/" />
   </soap:Body>
</soap:Envelope>

C'est la requête envoyée par le client au serveur. Dans cette requête le client invoque la méthode HelloWorld. La réponse associée :

HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: 14

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
 xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
   <soap:Body>
   <HelloWorldResponse xmlns="http://www.dotmyself.net/">
     <HelloWorldResult>Hello World</HelloWorldResult>
   </HelloWorldResponse>
   </soap:Body>
</soap:Envelope>

En guise de réponse, le serveur lui retourne la message « Hello World »

2.2 Présentation de l'article

Dans ce document je propose une vision centrée sur Java des Web services SOAP. C'est-à-dire que je vais expliquer comment faire du SOAP sans se préoccuper ni du XML généré, ni de la sérialisation, ni de la désérialisation des objets Java en XML (et vice versa). Consultez la littérature dédiée à SOAP pour obtenir plus d'informations oreintées SOAP.

Dans cette optique, cet article se base sur 4 éléments indispensables au bon fonctionnement de la chaîne Web service SOAP :

Pour rendre ce document digeste, je vais parcourir les capacités de JAX-WS à travers l'exemple d'un site Internet qui donne pour chaque département français le nombre d'habitants, la superficie et une indication sur le niveau d'urbanisation. Les résultats seront aléatoires ceci pour rester indépendant des sources externes de données.

3. Le Web service côté serveur

3.1 Le bean associé à un département

Créez le Java bean qui contiendra les données relatives à un département (Department.java) :

/**
 * @author Hyacinthe MENIET
 * Created on 21 juil. 07
 */
package net.dotmyself.ws;

import javax.xml.bind.annotation.XmlElement;

/**
 * Contains the data relating to a Department
 */
public class Department {
	
	/**
	 * Department's code.
	 */
	private int code;
	/**
	 * Department's population number.
	 */
	private int population;
	/**
	 * Department's surface in km2.
	 */
	private float surface;
	/**
	 * A comment on the urbanization's level.
	 */
	private String urbanization;
	/**
	 * @return the code
	 */
	@XmlElement(name="code")
	public int getCode() {
		return code;
	}
	/**
	 * @param code the code to set
	 */
	public void setCode(int code) {
		this.code = code;
	}
	/**
	 * @return the population
	 */
	@XmlElement(name="population")
	public int getPopulation() {
		return population;
	}
	/**
	 * @param population the population to set
	 */
	public void setPopulation(int population) {
		this.population = population;
	}
	/**
	 * @return the surface
	 */
	@XmlElement(name="surface")
	public float getSurface() {
		return surface;
	}
	/**
	 * @param surface the surface to set
	 */
	public void setSurface(float surface) {
		this.surface = surface;
	}
	/**
	 * @return the urbanization
	 */
	@XmlElement(name="urbanization")
	public String getUrbanization() {
		return urbanization;
	}
	/**
	 * @param urbanization the urbanization to set
	 */
	public void setUrbanization(String urbanization) {
		this.urbanization = urbanization;
	}
}

Chaque assesseur (getXXX) est annoté grâce au tag @XmlElement. C'est un tag JAXB qui permet d'indiquer que le l'attribut correspondant doit apparaître dans le flux SOAP produit et le nom de la balise XML associée.

3.2 Le Web service

Créez la classe qui sera exposée comme Web service (DepartmentInformation.java) :

/**
 * @author Hyacinthe MENIET
 * Created on 21 juil. 07
 */
package net.dotmyself.ws;

import java.util.Random;

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;

/**
 * Randomly generates useful data about the given Department.
 */
@WebService(name="DepartmentService")
public class DepartmentInformation {
	
	/**
	 * Used to retrieve random data.
	 */
	private Random random;
	/**
	 * Default constructor.
	 */
	public DepartmentInformation() {
		this.random = new Random();
	}
	/**
	 * Retrieves random data from the given department's code.
	 * @param departmentCode the department's code.
	 * @return a {@link Department}
	 */
	@WebMethod
	public @WebResult(name="department") Department getDepartment(
		@WebParam(name="departmentcode") int departmentCode) {
		
		// we set the seed so that the sequence can be 
		// reproduced for the same department
		random.setSeed(departmentCode);
		// fills the department
		String urbanization = "campagnard";
		Department department = new Department();
		department.setCode(departmentCode);
		department.setPopulation(random.nextInt(10000000));
		department.setSurface(random.nextFloat()*10);
		if (random.nextBoolean()) {
			urbanization = "citadin";
		}
		department.setUrbanization(urbanization);
		return department;
	}
	
}

Le code est massivement annoté cette fois par des tags JAX-WS :

3.3 L'enveloppe SOAP

Exécutez wsgen pour générer l'enveloppe SOAP. La commande ci-dessous doit être exécutée dans le dossier qui contient les .class de votre application (chez moi c'est bin) :

> wsgen -cp . net.dotmyself.ws.DepartmentInformation -s ../src/

Dans le dossier src de votre projet, la commande ci-dessus a généré les classes suivantes :

net/dotmyself/ws/jaxws/GetDepartment.java
net/dotmyself/ws/jaxws/GetDepartmentResponse.java

3.4 Fichiers JSP et XML

A ce stade vous avez un bean (e.g. Department), une classe qui retourne des informations sur les départements français (e.g. DepartmentInformation) et un groupe de classes qui permettent d'interroger DepartmentInformation comme un Web service (e.g. GetDepartment et GetDepartmentResponse).

Vous allez maintenant déployer votre groupe de classes (e.g. Department, DepartmentInformation, GetDepartment, GetDepartmentResponse) sur Tomcat, pour cela il vous manque :

index.jsp : La page d'accueil de votre application web.

web.xml : Le descripteur de déploiement de l'application Web :

<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
    version="2.4">

    <display-name>French departments Web service application</display-name>
    <description>
	Randomly generates useful data about the given French department.
    </description>
    
    <listener>
         <listener-class>
	    	com.sun.xml.ws.transport.http.servlet.WSServletContextListener
         </listener-class>
    </listener>

    <servlet>
        <servlet-name>jaxservlet</servlet-name>
        <servlet-class>com.sun.xml.ws.transport.http.servlet.WSServlet</servlet-class>
	<load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>jaxservlet</servlet-name>
        <url-pattern>/department</url-pattern>
    </servlet-mapping>

</web-app>

sun-jaxws.xml : le descripteur de déploiement de JAX-WS RI :

<?xml version="1.0" encoding="ISO-8859-1"?>
<endpoints xmlns="http://java.sun.com/xml/ns/jax-ws/ri/runtime" version="2.0">
   <endpoint
      name="department"
      implementation="net.dotmyself.ws.DepartmentInformation"
      url-pattern="/department"/>
</endpoints>

4. Déploiement du Web service

4.1 Mise à jour de Tomcat 6

La version de JAX-WS livrée avec le JDK 6 ne contient pas toutes les classes nécessaires au fonctionnement d'un Web service dans un conteneur de Servlet comme Tomcat. Il est en revanche suffisant pour consommer un Web service côté client ou pour déployer votre Web service via le serveur web interne au JDK 6.

Pour compléter votre Tomcat 6, téléchargez la dernière version de JAX-WS RI 2.0.x et décompressez-la ainsi :

> java -jar jaxws-2_0.1.jar

Récupérez tous les jars qui sont dans le sous répertoire lib/ et placez-les dans $CATALINA_HOME/lib/

4.2 Préparation et déploiement du war

Utilisez ant ou l'outil de votre choix pour créer un war avec l'arborescence suivante :

index.jsp
WEB-INF/web.xml
WEB-INF/sun-jaxws.xml
WEB-INF/classes/net/dotmyself/ws/Department.class
WEB-INF/classes/net/dotmyself/ws/DepartmentInformation.class
WEB-INF/classes/net/dotmyself/ws/jaxws/GetDepartment.class
WEB-INF/classes/net/dotmyself/ws/jaxws/GetDepartmentResponse.class

Quand vous avez terminé poussez simplement votre war dans CATALINA_HOME/webapps/ et redémarrez Tomcat. Vous pouvez tester l'application web en vous connectant à l'adresse http://localhost:8080/department/.

5. Le client Web service

Il y a trivialement deux méthodes pour consommer un Web service :

Dans la suite j'indique comment créer les deux.

5.1 Génération des classes dérivées (stubs)

Avant de créer les clients synchrones et asynchrones vous allez générer, grâce à wsimport, les classes dérivées du Web service. Par défaut wsimport ne génère pas les classes qui supportent les appels asynchrones, vous allez l'y obliger à l'aide du fichier bindings.xml :

<?xml version="1.0" encoding="ISO-8859-1"?>
<bindings
    wsdlLocation="http://localhost:8080/department/department?wsdl"
    xmlns="http://java.sun.com/xml/ns/jaxws">
	<enableAsyncMapping>true</enableAsyncMapping>
</bindings>

		

Appelez wsimport depuis le dossier qui contient les .class de votre client (chez moi c'est bin):

> wsimport -b bindings.xml http://localhost:8080/department/department?wsdl

Ou si vous souhaitez que wsimport génère également les sources java :

> wsimport -s ../src -b bindings.xml http://localhost:8080/department/department?wsdl

5.2 Le client synchrone

Le client synchrone est une simple classe Java pourvue d'un main (SynchDepartmentWSClient.java) :

/**
 * @author Hyacinthe MENIET
 * Created on 22 juil. 07
 */
package net.dotmyself.wsclient;
import net.dotmyself.ws.Department;
import net.dotmyself.ws.DepartmentInformationService;
import net.dotmyself.ws.DepartmentService;

/**
 * Synchronous Client for Department's Web service
 */
public class SynchDepartmentWSClient {
	
	/**
	 * The main method.
	 * @param args the department's code
	 * @throws Exception
	 */
	public static void main(String[] args) throws Exception {
		if(args == null || args.length < 1) {
			throw new IllegalArgumentException("You must indicate a department code");
		}
		int code = Integer.parseInt(args[0]);
		// Synchronous Invocation
		DepartmentInformationService departInfoService =  new DepartmentInformationService();
		DepartmentService departService = departInfoService.getDepartmentServicePort();
		Department dept = departService.getDepartment(code);
		System.out.println("Population ="+dept.getPopulation()+" habs, "
        		+"Surface="+dept.getSurface()+" km2, "
        		+"Urbanization="+dept.getUrbanization());
	}
}

Après compilation, le résultat de l'exécution :

> java net.dotmyself.wsclient.SynchDepartmentWSClient 38
Population =3200628 habs, Surface=9.785364 km2, Urbanization=campagnard

5.3 Le client asynchrone

Le client asynchrone reprend le code ci-dessus et le complète par un appel asynchrone (ASynchDepartmentWSClient.java) :

/**
 * @author Hyacinthe MENIET
 * Created on 22 juil. 07
 */
package net.dotmyself.wsclient;

import javax.xml.ws.AsyncHandler;
import javax.xml.ws.Response;

import net.dotmyself.ws.Department;
import net.dotmyself.ws.DepartmentInformationService;
import net.dotmyself.ws.DepartmentService;
import net.dotmyself.ws.GetDepartmentResponse;

/**
 * Asynchronous Client for Department's Web service
 */
public class AsynchDepartmentWSClient {

	/**
	 * The main method.
	 * @param args the department's code
	 * @throws Exception
	 */
	public static void main(String[] args) throws Exception {
		if(args == null || args.length < 1) {
			throw new IllegalArgumentException("You must indicate a department code");
		}
		int code = Integer.parseInt(args[0]);
		
		DepartmentInformationService departInfoService =  new DepartmentInformationService();
		DepartmentService departService = departInfoService.getDepartmentServicePort();
		
		departService.getDepartmentAsync(code, new AsyncHandler<GetDepartmentResponse>()
		{
		
				@Override
				public void handleResponse(Response<GetDepartmentResponse> res) {

					// Asynchronous Invocation
					try {
						if (!res.isCancelled() && res.isDone()) {
							
							GetDepartmentResponse message = res.get();
							Department dept = message.getDepartment();
							
							System.out.println("Population ="+dept.getPopulation()+" habs, "
					        		+"Surface="+dept.getSurface()+" km2, "
					        		+"Urbanization="+dept.getUrbanization());
							
						}
					} catch (Exception ex) {
						ex.printStackTrace();
					}
				}
		});
		
	// give 10 secondes to asynchronous call to complete
        Thread.sleep(10000);
	}

}

Après compilation, le résultat de l'exécution :

> java net.dotmyself.wsclient.AsynchDepartmentWSClient 38
Population =3200628 habs, Surface=9.785364 km2, Urbanization=campagnard