Interopérabilité Enterprise Java Beans et .NET par Thomas GIL (thomas.gil@dotnetguru.org)


Introduction

Les WebServices, comme CORBA dix ans plus tôt, nous ont initialement été présentés comme un outil capable d'améliorer l'interopérabilité entre architectures et environnements hétérogènes.

Après quelques déclarations tonitruantes dans la presse et le lancement des premiers projets avides de technologies « toutes fraîches », les premiers retours d'expérience qui ont eu quelque écho n'étaient pas toujours en faveur de ce type de middleware. Généralement, on reproche aux WebServices :

Nous avons voulu revenir sur certains de ces aspects non pas à travers un article conceptuel et forcément empreint de parti pris, mais par une approche pragmatique en essayant d'implémenter un cas d'école représentatif des architectures hybrides J2EE / .NET que nous allons certainement être amenés à implémenter de plus en plus (c'est un sentiment personnel).

Bien sûr, il ne faut pas vous attendre à trouver ici une description formelle des normes SOAP, WSDL, WSIL, UDDI, BPEL4WS... d'excellents ouvrages y consacrent déjà des milliers de pages !

Conception d'architectures orientées WebServices

Sans revenir formellement sur le découpage des architectures multi-couches, posons-nous simplement la question : à quel endroit est-il judicieux d'utiliser les WebServices ? Si nous poussons le modèle à l'extrême, nous trouvons en réalité plus de candidats que prévu (par l'habitude).

Entre la base de données et la couche d'accès aux données ?

Oui, en particulier pour dialoguer avec des bases de données XML natives. Par exemple, il est typique de se connecter sur la base eXist par le biais du protocole SOAP : on envoie à la base des requêtes XQuery, dont le résultat revient sous forme de fragments XML.

Pourquoi cette base de données, écrite en Java, serait-elle utilisée exclusivement dans le contexte J2EE ? En effet, grâce aux WebServices tout client disposant d'une pile SOAP/WSDL peut normalement interpréter et interagir avec les services exposés par la base de données.

Il ne serait donc pas étonnant dans les temps qui viennent de voir des architectures à deux niveaux refaire surface, soit en mode client riche (cf. Figure A), soit en mode client léger (cf. Figure B). Le multi-couches n'est bien sûr pas interdit, mais sous prétexte de nouvelles technologies, on en oublie parfois les principes architecturaux de base...



Figure A : Client riche .NET – Serveur de bases de données XML/Java




Figure B : Client léger – Serveur de bases de données XML/Java

Critique : la notion de transaction n'étant pas encore standardisée par le protocole SOAP, accéder à une base de données via les WebServices signifie que l'on renonce à toute interaction transactionnelle. Cela n'a pas gêné les développeurs de sites Web en PHP qui se sont longtemps connectés à MySQL sans support transactionnel, mais pour la plupart des applications d'entreprise, cet aspect est pour le moins bloquant.

Certaines implémentations permettent déjà de rendre transactionnelles les invocations d'opérations de WebServices (en ajoutant des informations techniques au en-têtes des messages SOAP) mais opter aujourd'hui pour ces fonctionnalités avancées lie nos applications au produit utilisé, et nuit grandement à l'interopérabilité.

Entre la couche de services et les objets du domaine ?

Non. Le modèle objet est souvent beaucoup trop complexe pour être directement "exposable" par le biais d'interfaces distribuées. Et ce à la fois pour des raisons de performances (il n'est pas souhaitable de réaliser trop d'invocations de méthodes à distance, et ce quel que soit le middleware choisi) mais aussi de complexité (les utilisateurs de nos services ne doivent pas connaître la mécanique interne de notre modèle objet, cf encapsulation).

Entre la couche de présentation et la couche de services ?

Oui, c'est typiquement à ce niveau que l'on « sent » bien l'implantation d'un middleware d'invocation de services à distance. Dans ce cas, pourquoi ne pas utiliser les WebServices ?

N'en disons pas plus pour le moment, puisque c'est cette partie qui sera implémentée par l'exemple de la section suivante. Mais demandons-nous tout de même si les WebServices suffisent à nos besoins techniques entre ces deux couches.

Authentification

Outre la nécessité d'invoquer des services à distance (ce que SOAP permet effectivement de faire), la couche de présentation aura souvent besoin de s'authentifier pour solliciter certaines opérations à visibilité restreinte. Cette question n'est pas nouvelle, mais l'objectif d'interopérabilité multi-plateformes la rend délicate à implémenter :

Transactions

Nous avons déjà abordé cet aspect au sujet de la connexion à une base de données. Si nous souhaitons garantir l'interopérabilité entre différentes technologies et différentes piles de WebServices, mieux vaut (aujourd'hui du moins) concevoir nos applications de telle sorte que la couche de présentation n'ait jamais à débuter ou à gérer de transaction.

Notez qu'en principe, c'est la recommandation que l'on fait déjà pour les architectures distribuées utilisant d'autres middlewares : avec MTS ou un serveur d'applications EJB, il est recommandé d'invoquer des services dont les transactions sont débutées et gérées par le conteneur de composants, côté serveur, et si possible en mémoire dans le cas d'une application mono-ressource (sans commit à deux phases). Propager un contexte transactionnel ne devrait être nécessaire qu'entre implémentations de services, mais pas entre la couche de présentation et la couche de services.

Gestion d'erreurs

Le protocole SOAP, contrairement à ce que le « O » de son nom semble indiquer, n'est en aucun cas orienté objet. Il permet d'acheminer des messages, ou en mode RPC d'invoquer des fonctions à distance. La gestion des erreurs est sommaire : seule la SOAPFault permet d'informer les clients d'un problème lors d'une invocation de service. La question est donc de savoir comment transformer le paradigme Objet des exceptions (Java, C++, C#, Eiffel...) en une SOAPFault ou un autre message compatible SOAP (une Response) et comment garantir que la transformation inverse pourra avoir lieu de l'autre côté de la connexion réseau.

En pratique, ce n'est pas possible ou du moins pas standard. En effet, l'interface WSDL ne permet pas de décrire les exceptions potentiellement renvoyées par les opérations de nos services. L'interopérabilité entre les langages et les piles de WebServices est donc bien compromise.

Posons-nous les mêmes questions que précédemment. Pour implémenter une gestion d'erreur fine, nous pourrions :

Gestion de l'état conversationnel

Enfin, mais cette question n'est pas uniquement liée à l'utilisation des WebServices, où doit-on gérer le contexte d'un utilisateur ? Dans la couche de présentation ou la couche de service ? Nous nous accordons généralement à dire que pour atteindre un couplage minimal et une montée en charge maximale, il vaut mieux que la couche de services soit sans état (ou amnésique). Mais dans certains cas où le « contexte » a une volumétrie trop importante, il peut être inacceptable de le réexpédier systématiquement à chaque invocation de service...

Architectures hétérogènes par l'exemple

Maintenant que nous avons bien toutes les contraintes en tête, essayons d'implémenter un exemple de service en Java sous forme de composant EJB Session Stateless et de le consommer à partir d'une page ASP.NET qu'un client final exécutera à travers son navigateur.

Implémentation de la couche de service (EJB)

Mise en place de l'infrastructure

En termes d'outillage, nous allons nous appuyer sur :

Certains environnements de développement intègrent directement les utilitaires permettant de développer rapidement un EJB (WSAD, Jbuilder), mais à l'usage, l'intégration de quelques outils OpenSource suffisent généralement à nos besoins.

Conception du composant

En guise de petit clin d'oeil à un outil de gestion commerciale et comptable (développé sur J2EE par Pascal Coube et disponible en OpenSource à l'adresse http://sourceforge.net/projects/oxerp/), nous allons concevoir un composant EJB Session Stateless qui renvoit un tableau de mouvements comptables qui ont été réalisés entre deux dates.

Un mouvement comptable est une entité de notre application qui peut être représenté par un EJB Entité local, ou un simple JavaBean rendu persistant par JDO. Mais la couche de service renverra à ses consommateurs un simple DTO (Data Transfer Object) en prenant bien garde à ne pas le faire dépendre d'un objet propriétaire à la plateforme de développement (une collection par exemple).

Voici un extrait du diagramme de conception d'une telle application :



Figure C : MouvementComptable (DTO) et service de récupération des mouvements (EJB)

Développement du composant

Le code de la classe AccountMovement est trivial, c'est un POJO (Plain Old Java Object). Par contre, développer un EJB nécessite de rédiger son interface métier (distante dans notre exemple), son implémentation, l'interface de sa fabrique (la « Home ») ainsi que son descripteur de déploiement associé.

Pour nous simplifier la tâche, nous avons utilisé l'outil Lomboz pour créer un nouvel EJB Session Stateless. Voici ce que ce plug-in génère automatiquement (nous avons simplement enrichi le corps de la méthode getAccountMovementsBetween(...) en implémentant un « bouchon » ou un Mock Object si vous voulez ) :

package org.dotnetguru.erp;

import java.util.Date;

import javax.ejb.SessionBean;

/**

* @ejb.bean name="BookKeeping"

* jndi-name="BookKeepingBean"

* type="Stateless"

**/

public abstract class BookKeepingBean implements SessionBean {

/**

* @ejb.interface-method

* view-type="remote"

**/

public AccountMovement[] getAccountMovementsBetween(Date start, Date end) {

// Fake implementation. This methods should probably

// use local EntityBeans or JDO to retrieve data from the

// database

AccountMovement mvt1 = new AccountMovement("Billing an advertising week : Client Foo", 410000, 706000, 2000.00);

AccountMovement mvt2 = new AccountMovement("Buying a new computer : Supplier Bar", 604000, 401000, 2500.00);

return new AccountMovement[]{mvt1, mvt2};

}

}

Vous aurez reconnu la syntaxe des commentaires JavaDoc : ce sont ceux de la Xdoclet, qui génère à partir de ce fichier « enrichi » tous les artefacts dépendants (interface distante, interface de la home, descripteur de déploiement). Cette génération est déclenchée à travers Eclipse par le plug-in Lomboz :



Figure D : Génération des fichiers dépendants avec Lomboz

Décoration et déploiement

Le déploiement du composant EJB lui-même ne pose aucun problème: Lomboz le prend en charge pour peu que l'on ait bien configuré le chemin d'installation du serveur Jboss dans les propriétés du plug-in.

Que doit-on faire pour exposer ce composant sous forme d'un WebService ?

  1. Installer Jboss.NET : si l'on utilise la configuration « default » de Jboss, il suffit pour cela de copier le répertoire « .../Jboss/server/all/deploy/jboss-net.sar » dans le répertoire « .../Jboss/server/default/deploy/jboss-net.sar ». Cela peut se faire à chaud, alors que le serveur Jboss fonctionne. A l'issue de ce déploiement, la pile de WebServices Axis se retrouve activée dans le serveur Jboss.

  2. Déclarer le WebService façade : il y a plusieurs techniques pour cela, mais la moins intrusive est certainement de déployer une archive « .WSR », qui permet de déployer un WebService à chaud dans le serveur d'applications. Dans notre cas, l'archive ne jouera que le rôle de décorateur technique et de délégateur : elle exposera l'interface de notre EJB sous forme d'un contrat WSDL et acceptera les requêtes SOAP qui seront dynamiquement transformées en requêtes RMI à destination du composant EJB.

Pour cette deuxième étape, la marche à suivre est très simple :

Le contenu du fichier de configuration web-service.xml (utilisé par Axis) est le suivant :

<?xml version="1.0" encoding="iso-8859-1"?>

<deployment   

   xmlns="http://xml.apache.org/axis/wsdd/"

   xmlns:java="http://xml.apache.org/axis/wsdd/providers/java"

   xmlns:xsd="http://www.w3.org/2000/10/XMLSchema"

   xmlns:xsi="http://www.w3.org/2000/10/XMLSchema-instance">

 

       <service name="BookKeeping" provider="java:EJB">

           <parameter name="beanJndiName" value="BookKeepingBean"/>

           <parameter name="homeInterfaceName" value="org.dotnetguru.erp.BookKeepingHome"/>

           <parameter name="remoteInterfaceName" value="org.dotnetguru.erp.BookKeeping"/>

           <parameter name="allowedMethods" value="*"/>

           <parameter name="className" value="org.dotnetguru.BookKeepingBean"/>

           <beanMapping

               xmlns:ns="http://erp.dotnetguru.org"

               qname="ns:AccountMovement"

               languageSpecificType="java:org.dotnetguru.erp.AccountMovement"/>

       </service>     

</deployment>

Tout comme pour le packaging d'une application Web ou un EJB, il ne nous reste plus qu'à compresser le contenu de notre répertoire de travail en un fichier « BookKeepingDecorator.wsr », et à copier-coller ce fichier archive dans le répertoire de déploiement de Jboss (../Jboss/server/all/deploy).

Jboss déploie notre décorateur à chaud, et nous pouvons nous assurer que le WebService est bien en ligne en nous rendant à l'adresse suivante (par le biais d'un simple navigateur) : http://localhost:8080/jboss-net/services. Dans notre cas, voici le résultat affiché par le module Jboss-net :



Figure E : Liste des services disponibles via Jboss.net

En suivant le lien hypertexte du service « BookKeeping », nous découvrons son contrat WSDL :



Figure F : Contrat WSDL en façade de l'EJB Session Stateless


Il n'y a plus qu'à consommer ce WebService avec la technologie de notre choix côté client.

Implémentation d'un client .NET

La consommation de WebServices en .NET est chose facile. Pour ceux qui ont la chance d'être équipés de VisualStudio.NET ou de C#Builder, on peut même dire que c'est trivial. Allez, une dernière fois, laissons-nous guider par la démarche « .NET Evangelist » :

Pour vous faire une idée, jetez un oeil au CodeBehind :

namespace ConsumingBookKeeping {

using System;

using BookKeeping;


public class AccountMovementList : System.Web.UI.Page {

protected System.Web.UI.WebControls.DataList listMovements;


private void Page_Load(object sender, System.EventArgs e) {

BookKeepingService service = new BookKeepingService();

listMovements.DataSource = service.getAccountMovementsBetween

(new DateTime(2003, 01, 01), new DateTime(2003, 12, 31));

listMovements.DataBind();

}

}

}

Et voici le code de la page ASPX elle-même :

<%@ Page language="c#" Codebehind="AccountMovementList.aspx.cs"

AutoEventWireup="false"

Inherits="ConsumingBookKeeping.AccountMovementList" %>

<%@ Import namespace="ConsumingBookKeeping.BookKeeping" %>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >

<HTML>

<HEAD>

<title>AccountMovementList</title>

<meta content="Microsoft Visual Studio .NET 7.1" name="GENERATOR">

<meta content="C#" name="CODE_LANGUAGE">

<meta content="JavaScript" name="vs_defaultClientScript">

<meta content="http://schemas.microsoft.com/intellisense/ie5" name="vs_targetSchema">

</HEAD>

<body>

<form id="Form1" method="post" runat="server">

<H2>Account movement list</H2>

<asp:datalist id="listMovements" runat="server">

<HeaderTemplate>

<table border="1">

<tr>

<th>

Label</th>

<th>

Credit account #</th>

<th>

Debit account #</th>

<th>

Amount</th>

</tr>

</HeaderTemplate>

<FooterTemplate>

</table>

</FooterTemplate>

<ItemTemplate>

<tr>

<td>

<%# ((AccountMovement)Container.DataItem).label %>

</td>

<td>

<%# ((AccountMovement)Container.DataItem).crebitAccountNumber %>

</td>

<td>

<%# ((AccountMovement)Container.DataItem).debitAccountNumber %>

</td>

<td>

<%# ((AccountMovement)Container.DataItem).amount %>

</td>

</tr>

</ItemTemplate>

</asp:datalist>

</form>

</body>

</HTML>

Comme vous le voyez, si nous respectons nos règles habituelles en terme de conception et d'affectation des responsabilités entre couches, la construction d'architectures hybrides ne pose aucun problème. En effet, nous aurions écrit quasiment le même code pour consommer des EnterpriseServices distants, ou demain des services Indigo.

Conclusion

La plateforme J2EE offre un socle technique solide, mature et séduisant pour le développement de la couche d'objets métier, d'accès aux données et de services. Côté couche de présentation, en attendant les JSF et surtout l'outillage qui devrait les accompagner, nous sommes voués à utiliser des frameworks somme toute assez techniques tels que XMLC, Barracuda ou Struts.

La plateforme .NET se caractérise par un coût de prise en main inférieur, en particulier pour l'implémentation de la couche de présentation, qu'il s'agisse de clients riches ou léger. Les ASP.NET actuels sont supérieurs aux concurrents JSP/Servlets du monde Java: plus simples, plus abstraits et permettant un temps de développement inférieur.

Il est donc tout à fait naturel d'envisager d'utiliser le bon outil pour la réalisation de chaque partie de nos applications. En particulier, il n'est pas étonnant de voir certains projets choisir les ASP.NET comme couche de présentation et les EJB comme couche de service (la couche d'accès aux données pouvant être réalisée soit par des EJB Entités locaux, soit par JDO).

Dans cet article, l'exemple choisi était volontairement simple : les structures de données échangées entre couche de présentation et de service étaient indépendantes de toute plateforme, et les services sans état. Ce qui respecte les recommandations des architectures dites « SOA ». Si cette manière de procéder convient à vos applications, il ne vous reste plus qu'à industrialiser cette approche, et à faire quelques benchmarks pour vous assurer que la déperdition de performance n'est pas trop pénalisante pour vous. Dans le cas contraire, il faudrait vous retourner vers un canal .NET Remoting sur IIOP pour établir la communication directe avec les EJB Session. Mais quel que soit le middleware choisi, les patterns à appliquer sont les mêmes, et donc l'interface des services aussi. L'utilisation de WebServices n'a donc ici quasiment pas d'impact sur la conception de vos applications. Bref, un régal...

Auteur : Thomas Gil

Copyright - DotNetGuru - 28 Mars 2004

Code Source

Les différents exemples de code présentés dans cet article sont librement téléchargeables à cette adresse. Nous leur associons la licence LGPL, faites-en bon usage.

Références

http://eclipse.org/

http://jboss.org/index.html

http://www.objectlearn.com/index.jsp (Lomboz)

http://ws.apache.org/axis/