I. Introduction▲
Selon Wikipédia, un web service est un programme informatique permettant la communication et l'échange de données entre applications et systèmes hétérogènes dans des environnements distribués. Cette communication consiste simplement à demander à un serveur d'exécuter une requête, et de nous en envoyer le résultat. Il peut s'agir d'interroger une base de données à partir de plusieurs critères, ou à l'inverse de mettre à jour la base à distance. Un web service s'appuie sur plusieurs protocoles, tant pour la requête que pour le transport de celle-ci.
CXF est ainsi un framework Java, capable de parler plusieurs de ces protocoles, mais dans ce tutoriel, nous ne parlerons que du protocole SOAP pour les requêtes et de HTTP pour transporter ces dernières. Sachez cependant que CXF est capable d'utiliser plusieurs autres protocoles de requêtes (RESTful HTTP, CORBA par exemple) et de transport (notamment JMS). Je vous renvoie à la documentation officielle de CXF, dont vous trouverez le lien en fin d'article, pour ceci.
Comme indiqué dans les exemples fournis avec CXF, Spring est un invité de première classe du framework. En permettant la déclaration et l'injection de beans dans nos objets, il facilite grandement la création de notre futur web service, tant pour le côté serveur que le côté client. C'est donc tout naturellement que nous l'utiliserons.
II. Projet▲
II-A. Objectif▲
Le projet de web service qui servira de support à notre article est un petit service de gestion de bibliothèque. Notre cahier des charges sommaire est :
- un livre se caractérise par un identifiant, son titre, son genre (policier, science-fiction…), son année de publication et ses auteurs ;
- un auteur se caractérise par un identifiant, ses prénom et nom, sa nationalité et ses dates de naissance et de mort ;
- la recherche de livres peut se faire par identifiant, titre ou auteur ;
- la recherche d'auteurs peut se faire par identifiant, titre ou livre ;
- pour créer un livre, on doit fournir son titre et la liste de ses auteurs. En retour, nous obtenons son identifiant ;
- pour créer un auteur, seul le nom est obligatoire. En retour, nous obtenons son identifiant.
II-B. Création dans Eclipse▲
Maven simplifie considérablement la vie du développeur Java, grâce notamment à sa gestion des dépendances, aussi c'est tout naturellement que nous allons l'utiliser. Mais vous pouvez aussi télécharger CXF directement depuis le site officiel, ce qui vous permettra de bénéficier des nombreux exemples d'utilisation du framework.
Créons un projet Maven BookService dans Eclipse, en choisissant comme archetype maven-archetype-webapp. Nous devrons ajouter les dépendances vers CXF et Spring. Les versions que nous utiliserons sont respectivement 2.7.3 et 3.1.3.RELEASE. Dans le cadre de ce tutoriel, nous utiliserons Java 7, même si à partir de sa version 2.7, CXF se contente de Java 6.
Voici la liste des dépendances à ajouter concernant notre projet :
<properties>
<cxf.version>
2.7.3</cxf.version>
<spring.version>
3.1.3.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>
javax</groupId>
<artifactId>
javaee-web-api</artifactId>
<version>
6.0</version>
<scope>
provided</scope>
</dependency>
<dependency>
<groupId>
org.springframework</groupId>
<artifactId>
spring-core</artifactId>
<version>
${spring.version}</version>
</dependency>
<dependency>
<groupId>
org.springframework</groupId>
<artifactId>
spring-web</artifactId>
<version>
${spring.version}</version>
</dependency>
<dependency>
<groupId>
org.apache.cxf</groupId>
<artifactId>
cxf-rt-databinding-jaxb</artifactId>
<version>
${cxf.version}</version>
</dependency>
<dependency>
<groupId>
org.apache.cxf</groupId>
<artifactId>
cxf-rt-frontend-jaxws</artifactId>
<version>
${cxf.version}</version>
</dependency>
<dependency>
<groupId>
org.apache.cxf</groupId>
<artifactId>
cxf-rt-transports-http</artifactId>
<version>
${cxf.version}</version>
</dependency>
</dependencies>
Outre les liens vers diverses ressources sur internet à propos de CXF et des services web, vous trouverez en fin d'article les projets Eclipse complets, ce qui vous permettra de disposer d'implémentations pour tester les services web que nous allons créer.
Nous sommes maintenant prêts à nous attaquer à une première façon de créer un service web.
III. Approche Java first▲
Dans l'approche Java first, nous commençons par définir l'interface de notre WebService en créant une interface Java. Nous pourrions nous passer de l'interface Java et créer directement une classe, mais cette solution est déconseillée. Un WebService étant destiné à être utilisé par de nombreux clients, une interface garantit plus surement qu'il ne variera pas dans le temps, même si nous changeons son implémentation.
III-A. Le contrat▲
Le contrat de notre service, c'est-à-dire son interface, sera double : un BookService et un AuthorService. Le premier concernera des livres, le second les auteurs de ces livres. Nous aurons donc deux web services. Voici ces interfaces :
public
interface
BookService {
Book getBook
(
Long id) throws
BookNotFoundException;
List<
Book>
getBooksByTitle
(
String title);
List<
Author>
getAuthorsFromBook
(
String bookTitle) throws
BookNotFoundException;
Long createBook
(
String title, BookType type, int
year, Long... authorsId);
}
et :
public
interface
AuthorService {
List<
Book>
getBooksFromAuthor
(
String authorFirstName, String authorLastName) throws
AuthorNotFoundException;
Author getAuthor
(
Long id) throws
AuthorNotFoundException;
List<
Author>
getAuthorsByName
(
String firstName, String lastName) throws
AuthorNotFoundException;
Long createAuthor
(
String firstName, String lastName, String nationality, Date dateOfBirth, Date dateOfDeath);
}
Nous avons besoin des classes Book et Author. Ce sont ces objets qui transiteront sur le réseau, entre le(s) client(s) et le serveur. Les voici :
public
class
Book {
private
Long id;
private
String title;
private
BookType type;
private
int
year;
private
List<
Author>
authors;
// getters et setters
}
et
public
class
Author {
private
Long id;
private
String firstName;
private
String lastName;
private
String nationality;
private
Date dateOfBirth;
private
Date dateOfDeath;
private
List<
Book>
books;
// getters et setters
}
BookType est une simple énumération :
public
enum
BookType {
SCIENCE_FICTION, POLICIER, ROMAN_HISTORIQUE, HEROIC_FANTASY
}
Nous avons aussi besoin d'exceptions en cas d'erreur :
public
class
BookNotFoundException extends
Exception {
private
Long id;
private
String title;
// getters et setters
}
et :
public
class
AuthorNotFoundException extends
Exception {
private
Long id;
private
String author;
// getters et setters
}
Pour l'instant, nous ne sommes en présence que de classes Java classiques. Il est temps de passer au WebService proprement dit.
III-B. Le WebService▲
Sa création est extrêmement simple : nul besoin de la moindre ligne de code, tout se fera par annotations et configuration.
La première annotation est @WebService, qui sert à marquer nos interfaces :
@WebService
(
name=
"BookService"
, serviceName=
"BookService"
)
public
interface
BookService {
...
}
Nous annotons AuthorService de la même manière, en adaptant les noms. Les attributs Name et ServiceName ne sont pas obligatoires, des valeurs par défaut sont fournies. Nous verrons plus tard à quoi ils servent. @WebService permet à CXF de connaitre les interfaces à instrumenter en tant que web service. C'est la seule qui soit obligatoire.
Pour décrire le service à nos clients, CXF va nous générer un fichier WSDL. Cependant, sa génération se fait à partir des classes Java compilées, et non depuis le code source. Nous perdons malheureusement ainsi le nom des paramètres, et donc leur signification. Pour les retrouver, nous utilisons une autre annotation, @WebParam. Elle se place sur les paramètres des méthodes de nos interfaces :
List<
Author>
getAuthorsFromBook
(
@WebParam
(
name=
"bookTitle"
) String bookTitle) throws
BookNotFoundException;
C'est son attribut Name qui nous permet de retrouver les noms originaux, plus parlant que arg0, arg1…
Nous n'avons besoin de rien de plus concernant notre code. Passons maintenant à notre fichier web.xml. Comme pour toute application Web, il contient la définition des servlets. Ici, nous n'en utiliserons qu'une seule, CXFServlet. Nous avons aussi besoin d'un ContextLoaderListener de Spring, à qui nous dirons où se trouve le fichier de configuration de l'application :
<?xml version="1.0" encoding="UTF-8"?>
<web-app
version
=
"2.5"
xmlns
=
"http://java.sun.com/xml/ns/javaee"
xmlns
:
xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xsi
:
schemaLocation
=
"http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
>
<display-name>
Tutoriel CXF</display-name>
<context-param>
<param-name>
contextConfigLocation</param-name>
<param-value>
WEB-INF/app-context.xml</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>
cxf</servlet-name>
<servlet-class>
org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
<load-on-startup>
1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>
cxf</servlet-name>
<url-pattern>
/services/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
60</session-timeout>
</session-config>
</web-app>
Et voici le fichier de configuration de 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"
xmlns
:
beans
=
"http://cxf.apache.org/configuration/beans"
xmlns
:
jaxws
=
"http://cxf.apache.org/jaxws"
xmlns
:
soap
=
"http://cxf.apache.org/bindings/soap"
xsi
:
schemaLocation
=
"http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.cxf.apache.org/bindings/soap
http://www.cxf.apache.org/bindings/schemas/configuration/soap.xsd
http://cxf.apache.org/jaxws
http://cxf.apache.org/schemas/jaxws.xsd"
>
<
jaxws
:
server
serviceClass
=
"fr.atatorus.bookservice.services.BookService"
address
=
"/book"
serviceBean
=
"#book"
>
</
jaxws
:
server>
<
jaxws
:
server
serviceClass
=
"fr.atatorus.bookservice.services.AuthorService"
address
=
"/author"
serviceBean
=
"#author"
>
</
jaxws
:
server>
<bean
id
=
"book"
class
=
"fr.atatorus.bookservice.services.BookServiceImpl"
/>
<bean
id
=
"author"
class
=
"fr.atatorus.bookservice.services.AuthorServiceImpl"
/>
</beans>
Notez l'utilisation du namespace jaxws dans l'en-tête du fichier. Il nous permet d'utiliser l'élément <jaxws:server>. On précise simplement l'interface et l'implémentation à utiliser.
Et voilà, tout est en place, nous pouvons lancer notre web service grâce au menu contextuel Run As > Run on server (auparavant, vous devrez avoir installé un serveur J2EE, Tomcat par exemple, et configuré Eclipse en conséquence). Une fois le serveur et l'application lancés, rendez-vous à l'URL http://localhost:8080/bookservice/services. Vous devez voir alors la liste des services disponibles :
Nous voyons bien ici nos deux services, identifiés par la valeur de l'attribut Name de l'annotation @WebService. Sans cet attribut, notre service se serait appelé du nom de l'interface, postfixé par Service : AuthorServiceService par exemple. Nous avons également un lien vers les fichiers WSDL qui décrivent nos services, dont voici un petit extrait :
<?xml version='1.0' encoding='UTF-8'?>
<
wsdl
:
definitions
xmlns
:
xsd
=
"http://www.w3.org/2001/XMLSchema"
xmlns
:
wsdl
=
"http://schemas.xmlsoap.org/wsdl/"
xmlns
:
tns
=
"http://services.bookservice.atatorus.fr/"
xmlns
:
soap
=
"http://schemas.xmlsoap.org/wsdl/soap/"
xmlns
:
ns1
=
"http://schemas.xmlsoap.org/soap/http"
name
=
"AuthorService"
targetNamespace
=
"http://services.bookservice.atatorus.fr/"
>
<
wsdl
:
types>
<
xs
:
schema
xmlns
:
xs
=
"http://www.w3.org/2001/XMLSchema"
xmlns
:
tns
=
"http://services.bookservice.atatorus.fr/"
attributeFormDefault
=
"unqualified"
elementFormDefault
=
"unqualified"
targetNamespace
=
"http://services.bookservice.atatorus.fr/"
>
<
xs
:
element
name
=
"createAuthor"
type
=
"tns:createAuthor"
/>
<
xs
:
element
name
=
"createAuthorResponse"
type
=
"tns:createAuthorResponse"
/>
...
Ce fichier au format XML décrit notre web service. On y trouve bien entendu le nom de notre web service, tel qu'indiqué par notre annotation @WebService, mais aussi l'URL à laquelle il peut être atteint, ainsi que la liste des opérations disponibles, leurs paramètres et valeur de retour. Quant à l'espace de nom, il s'agit du nom du package puisque nous n'en avons pas donné dans l'annotation.
Ce fichier a été généré par CXF, à partir de nos interfaces annotées. Nous verrons aux chapitres suivants que CXF est aussi capable du cheminement inverse : partir du fichier WSDL pour aboutir à du code Java, client ou serveur.
Il est assez complexe à interpréter, mais il est inutile d'entrer dans les détails pour créer notre service. CXF se chargera de tout. Nous en parlerons cependant au chapitre suivant, quand nous créerons le web service en commençant par ce fichier.
Et maintenant que notre service fonctionne, nous devons le tester.
III-C. Tests▲
Comme nous n'avons pas de programmes clients pour l'instant, nous allons utiliser SoapUI, un logiciel open source simple d'utilisation. Téléchargez-le, installez-le et lancez-le. Bien que vous puissiez considérer ce qui suit comme un microtutoriel SoapUI, je vous encourage à vous rendre sur le site officiel pour approfondir l'utilisation de cet outil. Vous trouverez le lien en fin d'article.
Dans la fenêtre qui s'ouvre, vous voyez à gauche la liste des projets, actuellement vide. Cliquons avec le bouton droit pour ajouter le nôtre. Dans Project Name, entrons BookService, et dans Initial WSDL/WADL, mettons l'URL d'un de nos WSDL :
Sous Projects, nous voyons notre projet BookService et son premier WebService, AuthorService et ses quatre méthodes, chacune avec une requête initiale vide. Avec le menu contextuel sur BookService, ajoutons le WSDL de notre second service pour avoir notre projet complet. Quand ceci sera fait, dépliez l'arborescence de la méthode createAuthor, et double-cliquez sur la requête :
Nous découvrons sa description et ses paramètres. C'est grâce à l'annotation @WebParam que nous voyons leur nom. Sans elle, ils s'appelleraient simplement arg0, arg1, etc.
C'est parti, nous allons créer notre premier livre. Il s'agira de « La poussière dans l'œil de Dieu », de Larry Niven et Jerry Pournelle. Commençons par créer l'auteur Larry Niven :
Larry Nivean étant toujours vivant, nous ne mettons pas sa date de mort. En réponse, nous récupérons son identifiant, que nous notons, nous en aurons besoin lors de la création du livre. Il peut aussi nous servir à vérifier notre requête en faisant un getAuthor. Passons maintenant à son collègue, né le 7 aout 1933 et lui aussi toujours vivant. Ceci fait, créons le livre :
Je vous laisse essayer la requête getBook pour vérifier que tout est correct. Vous pouvez créer votre propre implémentation, ou utiliser celle des projets Eclipse en fin d'article.
III-D. Résumé▲
La réalisation de nos deux WebService nous permet de voir la grande simplicité de l'approche Java first, puisque seules quelques annotations suffisent. Cette approche est d'autant plus intéressante si le service existe sans qu'il soit déjà exposé en tant que web service. Il nous suffit d'annoter l'interface, d'ajouter quelques lignes dans nos fichiers de configuration, et le tour est joué. Il est bien entendu possible de créer un web service sans utiliser Spring. Nous verrons comment dans le chapitre suivant, quand CXF génèrera nos classes.
Cependant, l'approche Java First souffre d'un défaut : comme soapUI nous l'indique, tous les paramètres sont optionnels. Malheureusement, l'annotation @WebParam ne permet pas de préciser ceci. Si on peut le deviner pour certains, la date de décès d'un auteur par exemple, ce n'est pas le cas de tous, cela dépend du créateur du web service. Bien qu'il s'agisse davantage d'une règle métier, et donc à mettre en œuvre dans une couche supérieure, il ne fait pas de mal de l'indiquer à nos clients dans le contrat du web service.
De plus, cette approche lie de façon irréversible notre service à la technologie, et nous force à penser à notre service comme une interface Java, alors qu'il devrait s'agir d'une entité totalement abstraite, et indépendante de la technologie utilisée.
Passons à la seconde approche pour créer un web service, l'approche WSDL first.
IV. Approche WSDL First▲
WSDL signifie Web Services Description Language. Un fichier WSDL est donc simplement un fichier XML qui décrit l'ensemble des méthodes du web service que nous allons publier. Nous en avons vu précédemment, écrit par CXF à partir de nos interfaces annotées.
Commencer par ce fichier nous permet d'être complètement agnostiques concernant la technologie à utiliser. Nous pouvons nous concentrer exclusivement sur le WebService en tant que service, sans avoir à y penser de suite en tant qu'interface Java. Car si ici nous utiliserons Java, rien ne nous interdit de changer de technologie. Le fichier WSDL nous garantira la continuité du contrat de notre service Web si on doit en changer. N'ayant pas défini à cette étape une technologie précise, nous ne penserons qu'au contrat du service à exposer, sans être perturbé par les limites et/ou avantages de la future implémentation. Nous verrons qu'il apporte également une souplesse que ne permet pas Java dans le choix des paramètres.
IV-A. Le fichier WSDL▲
IV-A-1. Création▲
Une fois le projet créé comme précédemmentCréation dans Eclipse (appelons le BookService2), allons dans src/main/resources et créons un répertoire wsdl qui contiendra notre fichier. Eclipse dispose d'un éditeur graphique de fichier wsdl, ce qui va bien nous aider pour la création de ce fichier sans que nous ayons trop à plonger dans sa complexité. On y accède, comme pour la plupart des assistants d'Eclipse, avec un Ctrl-N ou un New > Other depuis le menu Fichier ou contextuel. Il fait partie des assistants Web Services :
Cliquons sur Next, nommons le BookService.wsdl dans l'écran suivant, et cliquons à nouveau sur Next :
Renseignons les divers champs comme ci-dessus, en mettant abs comme préfixe, abs signifiant atatorus book service. En cliquant sur Finish, on se retrouve avec l'éditeur en mode Design :
On trouve à gauche notre service, nommé BookService, au centre un petit carré qui est le binding vers un portType, nommé lui aussi BookService.
Le portType est le support d'un ensemble d'opérations qui seront exposées à nos clients. Quand on génèrera nos interfaces et classes, si on veut comparer au chapitre précédent, le portType est l'équivalent de l'interface de notre service. Pour l'instant, il ne comprend qu'une seule opération, NewOperation. Bien entendu, un portType peut comprendre plusieurs opérations.
Le service est ce qui sera exposé aux clients. Il comprend un ou plusieurs ports. Pour l'instant, il n'en comprend qu'un seul, nommé BookServiceSoap. Chaque port est relié, grâce au binding (le petit carré) à un portType. Un port ne peut être relié qu'à un seul binding, mais un binding peut être relié à plusieurs ports. De l'autre côté, un binding ne conduit qu'à un portType, mais un portType peut être atteint par plusieurs bindings. Il est cependant préférable, pour plus de clarté, d'utiliser la règle suivante : un port, un binding, un portType. Je ne vois pas de cas où on serait obligé d'y déroger. Nous reviendrons plus en détail sur ces notions quand nous génèrerons nos interfaces et classes.
Pour l'instant, nous avons un squelette de WSDL. Cliquez sur l'onglet source en bas de l'éditeur pour en avoir un bref aperçu. Retournons dans l'éditeur graphique, et complétons-le en commençant par le port. Sélectionnons ce dernier, et ouvrons la vue des propriétés d'Eclipse :
L'adresse est celle où le service sera accessible, nous la remplaçons donc par la valeur correcte. Cette adresse servira à CXF pour la génération des classes, en créant notamment un serveur stand alone à l'écoute sur cette URL. En mettant en œuvre notre service à l'aide Spring, nous pouvons bien entendu utiliser une adresse différente.
La prochaine étape sera de créer les méthodes exposées par notre Web Service. Au niveau de l'éditeur, nous les retrouvons dans le portType. Cliquons sur la méthode NewOperation, et renommons-la getBooks. La récupération de livres se fera selon plusieurs critères :
- un identifiant ;
- un titre, ou une partie du titre ;
- l'identifiant d'un auteur ;
- le nom d'un auteur.
Bien entendu, ces paramètres sont mutuellement exclusifs. De plus, si on fournit l'identifiant d'un livre, nous ne récupérons pas une liste de livres, mais un seul, ou aucun. Dans ce dernier cas, nous lancerons l'exception BookNotFound. De même, on peut lancer l'exception AuthorNotFound si on cherche un livre à partir d'un auteur inconnu.
Commençons donc par les paramètres de la méthode. Passez la souris sur la flèche à droite du paramètre d'entrée input :
Une popup apparait, et nous voyons que notre paramètre d'entrée s'appelle in, et est de type String. Cliquons sur cette flèche, et un nouvel éditeur, Inline Schema of BookService.wsdl, apparait. Il ne s'agit que d'un « zoom » sur notre fichier WSDL que nous sommes en train d'éditer, et qui concerne le paramètre d'entrée de notre méthode. Supprimons le paramètre in en sélectionnant Delete dans le menu contextuel (bouton droit). Il nous reste une petite icône carrée, qui si on regarde dans les propriétés est une séquence. Toujours avec le bouton droit, cliquons dessus et choisissons AddChoice. La nouvelle icône que nous voyons représente un choix, c'est-à-dire une liste d'éléments mutuellement exclusifs. Ajoutons les éléments suivants, toujours avec le bouton droit et en sélectionnant AddElement :
Il nous reste à modifier le type de nos identifiants. Ceci se fait dans la vue des propriétés, ou en cliquant sur le type associé à l'identifiant dans l'éditeur. Dans la liste déroulante, les principaux types sont immédiatement accessibles, mais aucun ne nous intéresse. Sélectionnons Browse, et dans la popup prenons long. Nous devons ensuite mettre la cardinalité de ces paramètres, pour indiquer que si nous n'en choisissons qu'un parmi eux, celui choisi est obligatoire. Dans la vue propriétés, nous mettons 1 dans Minimum Occurrence. Il est inutile de renseigner le maximum, la cardinalité sera [1..1]. Cliquez sur l'onglet Source en bas de l'éditeur, et vérifiez que vous voyez quelque chose qui ressemble à ça :
<
xsd
:
element
name
=
"getBooks"
>
<
xsd
:
complexType>
<
xsd
:
sequence>
<
xsd
:
choice>
<
xsd
:
element
name
=
"bookId"
type
=
"xsd:long"
minOccurs
=
"1"
></
xsd
:
element>
<
xsd
:
element
name
=
"bookTitle"
type
=
"xsd:string"
minOccurs
=
"1"
></
xsd
:
element>
<
xsd
:
element
name
=
"authorId"
type
=
"xsd:long"
minOccurs
=
"1"
></
xsd
:
element>
<
xsd
:
element
name
=
"authorName"
type
=
"xsd:string"
minOccurs
=
"1"
></
xsd
:
element>
</
xsd
:
choice>
</
xsd
:
sequence>
</
xsd
:
complexType>
</
xsd
:
element>
Occupons-nous maintenant de la réponse de notre méthode. Elle nous renvoie, nous l'avons dit, un livre, ou une liste de livres. Dans la fenêtre de l'éditeur des paramètres de notre méthode, nous voyons une petite icône carrée tout en haut à gauche : . Double-cliquons dessus, nous nous retrouvons avec la liste des éléments de notre fichier WSDL :
Nous avons actuellement deux éléments : getBooks et getBooksResponse. Le premier représente les paramètres d'entrée que nous venons d'éditer, le second la réponse que nous allons adapter à nos besoins. Cliquons dessus, et nous nous retrouvons avec la même interface que précédemment. Commençons par ajouter un choix entre deux éléments : le premier nous décrit un livre complet, le second ne contenant que le minimum pour reconnaitre un livre. Voici donc ce que nous voulons :
- livre complet : identifiant, titre, année de publication, genre, liste des noms auteurs avec leur identifiant ;
- livre « basique » : identifiant, titre, liste des noms auteurs avec leur identifiant.
Commençons par supprimer le paramètre existant de la Sequence, et substituons-lui un Choice. Ajoutons à ce dernier une nouvelle Sequence comprenant quatre éléments (bookId, title, year, genre) et une autre Sequence, qui contiendra deux éléments (authorId et authorName). Tous les éléments sont de type string, sauf les identifiants qui sont des long et l'année de publication qui est un int. Au Choice, ajoutons une nouvelle Sequence, qui contiendra la même chose que la précédente à l'exception de l'année de publication et le genre.
Quant aux cardinalités, tous les éléments sont obligatoires (minimum 1, maximum non renseigné ou 1), sauf pour la séquence des auteurs. Il y a au moins un auteur, mais il peut y en avoir plusieurs. Mettons donc * dans le maximum. Quant à nos deux Sequences, elles ont chacune une cardinalité respectivement de 1 et 1 à l'infini.
Il nous reste un dernier petit détail à régler : le genre des livres est une chaine de caractères, mais restreinte à une liste de valeurs. Sélectionnons cet élément, et dans la vue des propriétés, allons dans l'onglet Constraints et ajoutons les valeurs désirées :
Voilà, nous avons fini la description de notre première méthode. Regardons le résultat :
Et en XML :
<
xsd
:
element
name
=
"getBooksResponse"
>
<
xsd
:
complexType>
<
xsd
:
sequence>
<
xsd
:
choice>
<
xsd
:
sequence
minOccurs
=
"1"
>
<
xsd
:
element
name
=
"bookId"
type
=
"xsd:long"
minOccurs
=
"1"
>
</
xsd
:
element>
<
xsd
:
element
name
=
"title"
type
=
"xsd:string"
minOccurs
=
"1"
>
</
xsd
:
element>
<
xsd
:
element
name
=
"year"
type
=
"xsd:int"
minOccurs
=
"1"
>
</
xsd
:
element>
<
xsd
:
element
name
=
"genre"
minOccurs
=
"1"
>
<
xsd
:
simpleType>
<
xsd
:
restriction
base
=
"xsd:string"
>
<
xsd
:
enumeration
value
=
"SCIENCE_FICTION"
>
</
xsd
:
enumeration>
<
xsd
:
enumeration
value
=
"POLICIER"
>
</
xsd
:
enumeration>
<
xsd
:
enumeration
value
=
"HEROIC_FANTASY"
>
</
xsd
:
enumeration>
<
xsd
:
enumeration
value
=
"ROMAN_HISTORIQUE"
>
</
xsd
:
enumeration>
</
xsd
:
restriction>
</
xsd
:
simpleType>
</
xsd
:
element>
<
xsd
:
sequence
minOccurs
=
"1"
maxOccurs
=
"unbounded"
>
<
xsd
:
element
name
=
"authorId"
type
=
"xsd:long"
minOccurs
=
"1"
>
</
xsd
:
element>
<
xsd
:
element
name
=
"authorName"
type
=
"xsd:string"
minOccurs
=
"1"
>
</
xsd
:
element>
</
xsd
:
sequence>
</
xsd
:
sequence>
<
xsd
:
sequence
minOccurs
=
"1"
maxOccurs
=
"unbounded"
>
<
xsd
:
element
name
=
"bookId"
type
=
"xsd:long"
minOccurs
=
"1"
>
</
xsd
:
element>
<
xsd
:
element
name
=
"bookTitle"
type
=
"xsd:string"
minOccurs
=
"1"
>
</
xsd
:
element>
<
xsd
:
sequence
minOccurs
=
"1"
maxOccurs
=
"unbounded"
>
<
xsd
:
element
name
=
"authorId"
type
=
"xsd:long"
minOccurs
=
"1"
>
</
xsd
:
element>
<
xsd
:
element
name
=
"authorName"
type
=
"xsd:string"
minOccurs
=
"1"
>
</
xsd
:
element>
</
xsd
:
sequence>
</
xsd
:
sequence>
</
xsd
:
choice>
</
xsd
:
sequence>
</
xsd
:
complexType>
</
xsd
:
element>
Et ce n'est pas fini : il nous faut nous occuper des exceptions. Pour ceci, retournons dans l'éditeur de notre fichier WSDL, et en cliquant avec le bouton droit sur la méthode que nous venons de créer, sélectionnons Add Fault pour ajouter une exception :
Renommons-la, dans la vue Properties, en BookNotFoundFault, puis comme pour les paramètres d'entrée et sortie de notre méthode, éditons-la. La recherche d'un livre se faisant par son identifiant ou son titre, nous remplaçons l'actuel élément par un choix entre ces deux éléments :
Recommençons pour AuthorNotFoundFault.
Ouf, nous avons terminé la première méthode de notre service. C'est quelque peu fastidieux. L'éditeur graphique nous aide à comprendre les différentes étapes plus simplement, mais reste moins facile d'accès que l'écriture d'une simple interface Java. En contrepartie, nous avons plus de possibilités, avec des paramètres optionnels, puisque la recherche d'un livre peut se faire selon quatre critères au choix. Pour faire la même chose avec une interface Java, nous aurions dû utiliser quatre méthodes différentes.
Pour se simplifier la vie, on pourrait commencer par écrire une interface Java, l'annoter, mettre en œuvre rapidement un petit service qui va nous générer un fichier WSDL, qu'il nous suffira d'adapter. Ceci est aussi quelque peu fastidieux pour un aussi petit service que le nôtre. Pour un plus complexe, c'est à voir.
Il y a une autre technique qui va nous alléger un peu le travail. Les paramètres des méthodes se ressemblent, notamment la liste des auteurs dans les livres, et nous allons très certainement les réutiliser ailleurs. Créer des types communs permet ainsi de factoriser une partie du fichier WSDL, ce qui est toujours une bonne chose en programmation.
IV-A-2. Utilisation d'un fichier type▲
Le fichier type nous permet de définir les types d'objets qui vont être manipulés par notre web service. On y trouve donc les auteurs, les livres, ainsi que les exceptions. Il n'est pas nécessaire que les types de notre service soient créés dans un fichier séparé, mais c'est une pratique qui clarifie le développement. Le fichier WSDL est plus léger, et nous gardons la possibilité de réutiliser nos mêmes types ailleurs.
Nos types sont décrits par un fichier de schéma XML. Créons un fichier types.xsd à l'aide de l'assistant d'Eclipse :
Nous voyons s'ouvrir l'éditeur XSD d'Eclipse, en mode design :
Il est très proche de ce que nous avons déjà vu lors de la création de notre fichier WSDL. Dans notre cas, tout ce qui nous intéresse est la partie Types. Commençons par remplacer le namespace utilisé (http://www.example.org/types) par le nôtre. Cliquez sur Schema, et renseignez la vue Properties comme ci-dessous :
Le préfixe abst signifie atatorus book service type. Ceci fait, retournons dans la partie Types de l'éditeur et avec le menu contextuel, ajoutons quelques types complexes, Book, Author, BasicBook, BasicAuthor, BookNotFound, AuthorNotFound, et un type simple, BookGenre :
Double-cliquons sur Author, nous nous retrouvons avec l'éditeur que nous connaissons bien pour avoir édité les paramètres de notre méthode précédemment. Modifions donc tous ces éléments pour qu'ils correspondent à nos besoins :
- Author : il est composé d'un identifiant, du prénom, du nom, de sa nationalité, et de ses dates de naissance et de mort. Seuls l'identifiant et le nom sont obligatoires (cardinalité mini à 1, maxi à 1ou non renseignée), les autres sont optionnels (mini 0, maxi 1). L'identifiant est de type long, les dates de type date, les autres sont des strings ;
- BasicAuthor : un identifiant et un nom, qui sera la concaténation du prénom et du nom. Les deux sont obligatoires ;
- Book : l'identifiant, le titre, le genre, l'année de parution, une liste de BasicAuthor. L'identifiant est un long, l'année un int, le genre est de type BookGenre. Tous sont obligatoires ;
- BasicBook : l'identifiant, le titre, la liste des BasicAuthor. Tous sont obligatoires ;
- BookNotFound : choix entre l'identifiant du livre ou son titre. Les deux sont obligatoires ;
- AuthorNotFound : choix entre l'identifiant et le nom de l'auteur, les deux sont obligatoires ;
- BookGenre : c'est un type simple à base de string, réduit aux choix que nous avons vu précédemment.
Ceci ne devrait pas vous poser de problèmes. Enregistrons nos modifications, et retournons à notre fichier WSDL, et plus particulièrement à la réponse de notre méthode. Zoomons dessus en cliquant sur la flèche à sa droite, et effaçons tout son contenu. Elle nous retournera désormais soit un Book, soit une liste de BasicBook. Pour sélectionner ces types, choisissez Browse dans la liste déroulante du type, et cochez la case Enclosing Project de la fenêtre de sélection :
Si dans la liste vous avez comme moi les types qui apparaissent en double, ceci provient du fait qu'Eclipse a généré les classes, sans doute parce que vous avez Build automatically coché dans le menu Project, ou parce que comme moi, vous avez lancé une génération des classes pour voir si votre fichier types.xsd produisait les classes attendues. Si c'est votre cas, prenez garde à sélectionner le type correspondant au fichier des types que nous venons de créer, pas celui de la classe Java générée. On peut faire la distinction en regardant dans Declaration Location.
Et voici ce que nous devons obtenir pour la réponse de notre service :
C'est nettement plus compréhensible qu'auparavant. Modifions ensuite le type de nos exceptions. Supprimons-les (ce sera plus facile), et recréons-les. Nommons la première BookNotFoundFault, avec un message du même nom, et de type BookNotFound. Même chose pour la seconde concernant les auteurs.
IV-A-3. Les autres méthodes▲
Nous procèderons de même pour les autres méthodes. Maintenant que nos types existent, ce sera beaucoup plus rapide. Voici les méthodes qui nous restent :
- createBook : elle prend en paramètres un titre, un genre, une année de publication, une liste d'auteurs. Cette liste peut se présenter sous la forme d'une liste d'identifiants ou de noms. En retour, on a l'identifiant du livre créé, ou une exception AuthorNotFound si un des auteurs de la liste n'existe pas ;
- getAuthors : elle prend en paramètres, au choix, un identifiant d'auteur, un nom, un identifiant de livre, un titre de livre. Dans le premier cas, elle nous retourne un Author, dans les autres une liste de BasicAuthor. En cas d'erreur, elle lance l'exception AuthorNotFound ou BookNotFound ;
- createAuthor : en paramètres, on trouve un prénom, un nom, une date de naissance et de décès, une nationalité. En retour, nous avons l'identifiant de l'auteur créé.
Une fois ces méthodes créées dans notre éditeur, nous devons les lier à notre binding. Sans ça, notre service ne propose tout simplement aucune méthode. Rappelez-vous notre petit carré entre le service et le portType qui contient les méthodes que nous voulons exposer. Pour que le lien se fasse entre nos méthodes et le service, nous devons mettre à jour le binding. Utilisons le menu contextuel sur le carré du binding, et choisissons Generate Binding Content. Comparez avant et après le code source du fichier, au niveau de la balise <wsd:binding /> pour voir la différence. Seules les opérations recensées dans cette balise seront exposées.
Vous trouverez à la fin de l'article les liens vers les projets Eclipse complets, avec les fichiers WSDL et de types.
Le fichier WSDL doit être un peu nettoyé. En effet, à peine avions-nous cliqué sur Add Fault dans le menu, qu'Eclipse nous génère des balises <wsdl:message /> pour par exemple createAuthorFault. Nous les avons remplacées dans l'éditeur graphique par BookNotFoundFault et AuthorNotFoundFault, mais Eclipse ne les supprime pas du code source.
Maintenant nous avons enfin tout ce dont nous avons besoin, nous pouvons passer à la génération des classes Java à l'aide de Maven.
IV-B. Génération du code▲
La génération de code Java par CXF nous donnera l'interface de notre service, ainsi que toutes les classes manipulées par notre service, qu'il s'agisse des paramètres, des valeurs de retour ou des exceptions. Une fois le code généré, nous n'avons besoin que de créer l'implémentation de notre service à partir de l'interface, et à le configurer comme dans l'approche Java FirstApproche Java first.
Pour générer ces classes, nous utiliserons le plug-in CXF pour Maven, mais il existe également un outil en ligne de commande, que vous trouverez en téléchargeant CXF depuis le site officiel.
IV-B-1. Plug-in CXF pour Maven▲
Comme nous utilisons Maven pour notre projet, l'emploi de ce plug-in est tout naturel. Voici sa configuration minimale :
<build>
...
<plugins>
...
<plugin>
<groupId>
org.apache.cxf</groupId>
<artifactId>
cxf-codegen-plugin</artifactId>
<version>
${cxf.version}</version>
<executions>
<execution>
<id>
generate-sources</id>
<phase>
generate-sources</phase>
<configuration>
<wsdlRoot>
${basedir}/src/main/resources/wsdl</wsdlRoot>
<wsdlOptions>
<wsdlOption>
<wsdl>
${basedir}/src/main/resources/wsdl/BookService.wsdl</wsdl>
</wsdlOption>
</wsdlOptions>
</configuration>
<goals>
<goal>
wsdl2java</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Les paramètres sont équivalents à ceux de l'utilitaire en ligne de commande. Ici, nous disons dans quel répertoire se trouvent les fichiers à analyser avec balise <wsdlRoot>, et quel est le fichier WSDL à utiliser avec la balise <wsdl>. La génération des sources se fera à la phase Maven generate-sources, en exécutant mvn clean generate-sources.
IV-B-2. Classes générées▲
Les classes se trouvent dans le répertoire target/generated-sources/cxf. Pensez à rafraichir le projet Eclipse si vous avez exécuté la commande depuis l'extérieur de l'IDE. Vous aurez certainement à ajouter ce répertoire parmi les sources du projet. Allez dans le menu contextuel Build Path > Use as Source Folder pour ceci.
Leurs packages sont fr.atatorus.bookservice et fr.atatorus.bookservice.types, et correspondent aux Target namespaces que nous avions définis au moment de la création de nos fichiers WSDL et XSD. Nous avons une classe pour chaque type, ainsi que pour chaque opération et sa réponse. Le plug-in nous a également généré d'autres classes.
La principale est BookService. Il s'agit tout simplement de l'interface de notre service. Nous pouvons donc dès à présent mettre en œuvre ce service, exactement comme nous l'avons vu au chapitre précédent. Nous devrons prendre garde lors de l'implémentation à gérer les paramètres optionnels, mais ceci mis à part, il n'y aura aucune différence.
Ensuite, nous trouvons une factory, qui nous permet d'instancier nos objets sans passer par l'opérateur new. Elle me semble avoir peu d'utilité en elle-même, mais il est possible d'en hériter ou de s'en inspirer si nécessaire.
Nous avons également des classes du genre GetBooks et GetBooksResponse. Il s'agit de classes container pour les paramètres et les valeurs de retour de nos méthodes.
IV-B-3. Générer un serveur▲
Le plugin cxf peut générer un serveur fonctionnel. Il nous suffit d'ajouter ces quelques lignes dans notre pom :
<configuration>
<wsdlRoot>
${basedir}/src/main/resources/wsdl</wsdlRoot>
<wsdlOptions>
<wsdlOption>
<wsdl>
${basedir}/src/main/resources/wsdl/BookService.wsdl</wsdl>
<extraargs>
<extraarg>
-impl</extraarg>
<extraarg>
-server</extraarg>
</extraargs>
</wsdlOption>
</wsdlOptions>
</configuration>
Nous obtenons deux classes supplémentaires : BookServiceImpl, une implémentation minimale de notre service, qui ne fait rien, et BookService_BookServiceSOAP_Server, un mini serveur stand alone, qui s'exécute simplement via sa méthode main(). Son implémentation est vraiment minimale, pour ne pas dire fruste, mais elle nous permet de voir comment créer un serveur qui pourra servir aux tests. Intéressons-nous cependant un peu à lui, et plus précisément à l'annotation sur la classe :
@javax
.jws.WebService
(
serviceName =
"BookService"
,
portName =
"BookServiceSOAP"
,
targetNamespace =
"http://www.atatorus.fr/BookService/"
,
wsdlLocation =
"file:/home/denis/dev/workspace/bookservice2/src/main/resources/wsdl/BookService.wsdl"
,
endpointInterface =
"fr.atatorus.bookservice.BookService"
)
public
class
BookServiceImpl implements
BookService {
...
}
Cette annotation possède un attribut particulièrement important, wsdlLocation. Comme son nom l'indique, elle signale où se trouve notre fichier WSDL. Elle n'est pas indispensable pour que notre service fonctionne, on peut même se passer de toute l'annotation, puisqu'on la trouve réduite à son minimum sur l'interface. Mais si nous n'indiquons pas où se trouve notre fichier, quand nous le demanderons à partir de l'URL http://localhost:8080/BookService/services?wsdl nous obtenons bien notre fichier WSDL, mais sans la déclaration des types du fichier types.xsd. Pour avoir les types, nous devons utiliser l'URL http://localhost:8080/BookService/services?wsdl=BookService.wsdl Nous obtenons alors un fichier WSDL intégrant les types au début. En implémentant notre service à l'aide de la servlet CXFServlet comme dans le tutoriel précédent, cette annotation est inutile, et est remplacée par l'attribut wsdlLocation dans le fichier contexte de Spring :
<
jaxws
:
server
serviceClass
=
"fr.atatorus.bookservice.BookService"
address
=
"/book"
serviceBean
=
"#book"
wsdlLocation
=
"wsdl/BookService.wsdl"
>
</
jaxws
:
server>
<bean
id
=
"book"
class
=
"fr.atatorus.services.BookServiceImpl"
/>
Nous verrons plus en détail les conséquences de l'utilisation ou non de cet attribut quand nous aborderons le client.
Vous pouvez lancer le serveur, et utiliser SoapUI comme client pour le tester. L'implémentation générée par CXF se contente de renvoyer des objets null, nous aurons donc besoin d'une implémentation un peu plus complète pour ceci. Vous pouvez la réaliser vous-même, ou prendre celle dans les projets en fin d'article.
IV-C. Résumé▲
La création d'un WebService à partir d'un fichier WSDL est un peu plus complexe comme nous venons de le voir. Mais cette complexité nous apporte davantage de souplesse qu'une simple interface Java. Nous avons besoin d'être un peu plus soigneux côté implémentation, à cause de la complexité de méthodes qui possèdent des paramètres optionnels, donc null. Quant à nos clients, la technologie utilisée par le serveur leur importe peu, tout ce qu'ils demandent c'est qu'il fonctionne. Et donc si pour une raison ou une autre nous avons besoin de changer de technologie, nous leur apportons ainsi la garantie que le service ne changera pas.
Mais avoir des web services c'est bien, avoir des clients capables de les utiliser c'est mieux.
V. Le client▲
Cette fois, nous n'avons pas à décider des possibilités du web service, nous sommes ses utilisateurs. Tout ce que nous avons, c'est l'URL où on peut le joindre, et du WSDL. Et nous avons vu que CXF peut tout à fait générer les classes nécessaires à l'appel du service. Outre l'interface du service, nous avons évidemment besoin des objets manipulés par le service. Et plutôt que de les écrire à la main, utilisons CXF pour ceci.
V-A. Génération des classes▲
Comme d'habitude, commençons par créer un projet Maven dans Eclipse, avec comme archetype maven-archetype-quickstart. Mais cette fois, comme nous ne disposons pas forcément du fichier WSDL, nous le téléchargerons.
V-A-1. Télécharger le WSDL▲
Pour ceci, nous nous servirons du plugin maven-download-plugin(1) :
<plugins>
...
<plugin>
<groupId>
com.googlecode.maven-download-plugin</groupId>
<artifactId>
maven-download-plugin</artifactId>
<version>
1.0.0</version>
<executions>
<execution>
<id>
Download wsdl</id>
<goals>
<goal>
wget</goal>
</goals>
<phase>
validate</phase>
<configuration>
<url>
http://localhost:8080/bookservice2/services/book?wsdl</url>
<outputDirectory>
${basedir}/target/resources/wsdl</outputDirectory>
<outputFileName>
BookService.wsdl</outputFileName>
</configuration>
</execution>
</executions>
</plugin>
...
</plugins>
Pour voir toutes les options de ce plugin, utilisez la commande :
mvn help:describe -Dplugin=com.googlecode.maven-download-plugin:maven-download-plugin -Ddetail=true
Dans notre cas, nous n'avons besoin que de l'URL du fichier, du répertoire où nous souhaitons l'enregistrer, ainsi que son nom. Quant à la phase choisie, il s'agit de validate, qui prend place juste avant generate-sources, et qui valide que les ressources de notre projet sont disponibles.
V-A-2. Génération des classes▲
Pour la génération des classes, nous reprenons le plugin CXF et le configurons comme dans le tutoriel précédent, sauf que nous n'avons plus besoin des options pour générer une implémentation et un serveur. À la place, nous allons générer un client :
<plugins>
...
<plugin>
<groupId>
org.apache.cxf</groupId>
<artifactId>
cxf-codegen-plugin</artifactId>
<version>
${cxf.version}</version>
<executions>
<execution>
<id>
generate-sources</id>
<phase>
generate-sources</phase>
<configuration>
<wsdlRoot>
${basedir}/target/resources/wsdl</wsdlRoot>
<wsdlOptions>
<wsdlOption>
<wsdl>
${basedir}/target/resources/wsdl/BookService.wsdl</wsdl>
<extraargs>
<extraarg>
-client</extraarg>
</extraargs>
</wsdlOption>
</wsdlOptions>
</configuration>
<goals>
<goal>
wsdl2java</goal>
</goals>
</execution>
</executions>
</plugin>
...
</plugins>
Lançons notre projet bookservice2 créé précédemment. Ensuite, exécutons la commande :
mvn clean validate generate-sources
Et voilà, nous avons notre fichier WSDL et nos classes. Je vous laisse examiner BookService_BookServicePort_Client qui montre comment générer un client. Nous utiliserons cependant une autre méthode, plus simple, avec Spring.
V-A-3. Remarques à propos des classes générées▲
Regardons l'interface de notre web service. Selon la manière dont elle est publiée par le site, elle correspond à celle que nous avions (j'ai supprimé les annotations pour plus de clarté) :
public
interface
BookService {
public
GetBooksResponse getBooks
(
GetBooks parameters) throws
BookNotFoundFault, AuthorNotFoundFault;
public
long
createAuthor
(
String firstName, String lastName, XMLGregorianCalendar dateOfBirth, XMLGregorianCalendar dateOfDeath, String nationality);
public
CreateBookResponse createBook
(
CreateBook parameters) throws
AuthorNotFoundFault;
public
GetAuthorsResponse getAuthors
(
GetAuthors parameters) throws
BookNotFoundFault, AuthorNotFoundFault;
}
ou ressemble à ça :
public
interface
BookService {
public
void
getBooks
(
Long bookId, String bookTitle, Long authorId, String authorName,
javax.xml.ws.Holder<
fr.atatorus.bookservice.types.Book>
book,
javax.xml.ws.Holder<
java.util.List<
fr.atatorus.bookservice.types.BasicBook>>
books) throws
BookNotFoundFault, AuthorNotFoundFault;
public
long
createAuthor
(
String firstName, String lastName, XMLGregorianCalendar dateOfBirth, XMLGregorianCalendar dateOfDeath, String nationality);
public
long
createBook
(
String title, BookGenre genre, int
year, List<
java.lang.Long>
authorIds, List<
java.lang.String>
authorsName) throws
AuthorNotFoundFault;
public
void
getAuthors
(
Long authorId, String authorName, Long bookId, String bookTitle,
javax.xml.ws.Holder<
fr.atatorus.bookservice.types.Author>
author,
javax.xml.ws.Holder<
java.util.List<
fr.atatorus.bookservice.types.BasicAuthor>>
basicAuthors) throws
BookNotFoundFault, AuthorNotFoundFault;
}
Dans le premier cas, nous avons indiqué explicitement à notre serveur où se trouvait le fichier WSDL, soit en annotant notre implémentation avec @WebService et en indiquant l'attribut wsdlLocation, soit en l'indiquant dans le fichier de configuration de Spring, comme nous l'avons vu au chapitre précédent.
Si nous ne précisons pas où se trouve le fichier WSDL, la servlet CXF doit se débrouiller pour le générer. Elle produit un fichier WSDL parfaitement valide, mais quand notre client l'analyse pour obtenir les valeurs de retour, pour les objets complexes il utilise un Holder(2). Nous devons fournir cet objet en paramètre lors de l'appel, et au retour, nous récupérons notre objet Book à l'intérieur. Si nous nous retrouvons avec ce genre d'interface, un petit adaptateur nous facilitera la vie. Ce n'est guère différent que d'utiliser un objet GetAuthors comme paramètre dans le premier cas. Nous avons toujours besoin d'un adaptateur.
V-B. Création du client▲
Une factory fournie par CXF nous suffit : JaxWsProxyFactoryBean. Elle va nous créer le client très simplement, à partir de deux paramètres : l'interface et l'adresse du web service.
public
class
BookClient {
public
static
void
main
(
String args[]) throws
Exception {
JaxWsProxyFactoryBean factory =
new
JaxWsProxyFactoryBean
(
);
factory.setAddress
(
"http://localhost:8080/bookservice/services/book"
);
factory.setServiceClass
(
BookService.class
);
BookService client =
(
BookService) factory.create
(
);
...
}
}
Et voilà, c'est aussi simple que ça, nous avons un client de web service prêt à l'emploi, il suffit de lui passer les paramètres qu'il attend. On peut aussi créer notre client en tant que bean Spring tout aussi facilement :
<bean
id
=
"client"
class
=
"fr.atatorus.bookservice.BookService"
factory-bean
=
"clientFactory"
factory-method
=
"create"
/>
<bean
id
=
"clientFactory"
class
=
"org.apache.cxf.jaxws.JaxWsProxyFactoryBean"
>
<property
name
=
"serviceClass"
value
=
"fr.atatorus.bookservice.BookService"
/>
<property
name
=
"address"
value
=
"http://localhost:8080/bookservice2/services/book"
/>
</bean>
Il ne vous reste plus qu'à tester le client.
V-C. Test du client▲
Pour tester le client, nous avons la possibilité d'interroger directement le web service. Mais outre le fait qu'il n'est pas forcément bien vu d'utiliser un serveur de production pour des tests, il est parfois nécessaire qu'il nous retourne des valeurs spécifiques à certains traitements particuliers. Là encore, SoapUI va nous aider, en permettant, simplement à partir du fichier WSDL, de générer un serveur, plus exactement un stub, un morceau de serveur en quelque sorte, qui nous répond exactement ce dont nous avons besoin.
Dans SoapUI, créons un nouveau projet, et après avoir ajouté le fichier WSDL, cochons la case Create MockService :
Nous devons choisir ensuite quelle méthode il implémentera. Choisissons-les toutes :
Dans la fenêtre suivante, donnons-lui un nom ou laissons le nom par défaut :
Et c'est (presque) tout, nous disposons d'un web service opérationnel :
Il ne nous reste plus qu'à définir la réponse de notre serveur à nos interrogations. Commençons par la méthode getBooks, et double-cliquons dessus. Une nouvelle fenêtre s'ouvre :
Vous avez deviné, double-cliquons sur Response 1. SoapUI nous présente alors un éditeur avec le type de réponse envoyé par le serveur, qu'il ne nous reste qu'à remplir :
Un dernier petit effort : configurons l'URL et le port d'écoute de notre stub. Cliquons sur l'icône en forme d'outils dans la fenêtre du MockService, et remplaçons le chemin par défaut (/mockBookServiceServiceSoapBinding) par quelque chose de plus simple :
Et maintenant, lançons notre MockService grâce à la petite flèche verte à gauche.
Écrivons quelques lignes de code pour vérifier que notre client (et notre stub…) fonctionne :
@SuppressWarnings
(
"restriction"
)
public
static
void
main
(
String args[]) throws
Exception {
JaxWsProxyFactoryBean factory =
new
JaxWsProxyFactoryBean
(
);
factory.setAddress
(
"http://localhost:8088/mock"
);
factory.setServiceClass
(
BookService.class
);
BookService client =
(
BookService) factory.create
(
);
Holder<
Book>
bookHolder =
new
Holder<
Book>(
);
client.getBooks
(
1
L, null
, null
, null
, bookHolder, null
);
Book book =
bookHolder.value;
System.out.println
(
"Titre : "
+
book.getTitle
(
));
System.out.println
(
"Auteurs :"
);
for
(
BasicAuthor author : book.getAuthors
(
)) {
System.out.println
(
author.getAuthorName
(
));
}
}
Et voilà le résultat :
Titre : La poussière dans l'œil de dieu
Auteurs :
Larry Nivean
Jerry Pournelle
SoapUI vous permet également de créer des réponses intelligemment, en tenant compte des paramètres de la requête. Allez sur le site officiel pour un tutoriel un peu plus poussé.
VI. Conclusion▲
Nous avons vu comment créer un serveur, selon deux approches, ainsi qu'un client, en nous appuyant sur le framework Apache CXF, ainsi que la manière tester l'ensemble avec SoapUI. Nous nous sommes limités aux web services utilisant le protocole SOAP avec le front-end JAX-WS. CXF nous propose d'autres protocoles et front-end, qui nécessiteraient chacun une série d'articles. Je vous renvoie donc au site officiel d'Apache CXF si vous voulez en apprendre davantage. Mais avec cet article, vous devriez posséder des bases suffisantes pour commencer à implémenter vos propres web services.
VII. Liens▲
Voici quelques liens qui pourront vous aider à aller plus loin :
- Apache CXF : c'est le site officiel. Vous y trouverez la documentation complète du framework, ainsi que pas mal d'exemples en téléchargeant le framework lui-même ;
- SoapUI : le site officiel de cet outil, pour le télécharger et apprendre à le maitriser ;
- maven-download-plugin : un plug-in Maven pour télécharger des fichiers lors du build ;
- wagon : un autre plug-in Maven qui peut remplacer le précédent si vous rencontrez des problèmes avec.
CXF n'est pas le seul framework pour développer des services web :
- Apache Axis : il s'agit d'une implémentation du protocole SOAP, comme nous avons utilisé avec CXF ici. Mais contrairement à ce dernier, il est limité à ce protocole. Il a évolué jusqu'à une version 2 ;
- XFire : Comme indiqué sur la page d'accueil du site, il s'agit de l'ancêtre de CXF. CXF peut même être considéré comme XFire 2.
Je ne connais pas ces frameworks. Si j'ai réussi à écrire des erreurs en aussi peu de mots, que les spécialistes me pardonnent (et me corrigent).
Quelques ressources disponibles sur DVP à propos des services Web :
- Développer un Web Service avec JAX-WS et le tester avec SOAPUI, en 5 minutes, un tutoriel de Thierry Leriche-Dessirier ;
- Architectures Orientées Services : une série de cours de Mickael Baron.
Et pour les plus paresseux d'entre vous, voici les projets Eclipse prêts à l'import :
Enfin, les articles originaux sur mon blog personnel :
VIII. Remerciements▲
Je remercie pour leur aide Robin56, thierryler et Mickael Baron, ainsi que ClaudeLeloup pour la correction orthographique.