MokaByte 76 - Luglio Agosto 2003 
Integrazione di applicazioni Enterprise
IV parte
di
Stefano
Rossini
Nei precedenti articoli si č introdotta l'integrazione a livello d'interfaccia utente (vedere [INT2]) e a livello di Object/RPC(vedere [INT2] e [INT3]); in questo articolo si parlerā dell'integrazione basata su messaggi

Message Integration
Mediante la Message Integration, per integrare i sistemi tra loro, i dati vengono inviati me-diante messaggi (JMS, SOAP, …).
Al contrario delle architetture basate su ORB, l'integrazione Message-oriented permette un forte disaccoppiamento tra i sistemi integranti e integrati per la natura intrinsecamente asin-crona dei sistemi di messaging.
Le operazioni di concatenzione e di calcolo della lunghezza della stringa (vedi[INT2]) sono entrambe di natura sincrona essendo implementate mediante componenti sincroni: gli EJB Session Stateless.
Il chiamante non può quindi proseguire nella sua elaborazione fino a quando non ha ricevu-to il risultato del servizio, che nel caso dell'EJB Calculator, è la lunghezza della stringa passa-ta come parametro.

Context ctx = new InitialContext();
Object ref = ctx.lookup("MokaCalculator");
CalculatorHome home = (CalculatorHome) PortableRemoteObject.narrow(ref,CalculatorHome.class);
Calculator calc = home.create();
int lunghezza =calc.getLength(strResult);
// altra logica elaborativa che non dipende dal contenuto della variabile lunghezza
. . .


JMS
Vediamo come è possibile disaccoppiare il client dall'operazione di calcolo della lunghezza della stringa. Si provvede quindi a sviluppare un EJB Message Driven (vedi[MOKA_MDB]) che gestisce in modo asincrono l'invocazione del servizio della lunghezza della stringa disac-coppiando il client dal componente di business.
Il client invia mediante JMS(vedi[MOKA_JMS]) un messaggio contenente la stringa di cui si vuole sapere la lunghezza, ad una destinazione JMS senza essere costretto a rimanere in atte-sa del risultato.
Il MDB provvederà a notificare il chiamante dell'avvenuta ricezione della richiesta di elabo-razione (richiesta di calcolo della lunghezza della stringa) mediante un messaggio JMS e a inoltrare il risultato (la lunghezza della stringa) via mail.

Ecco il codice dell'EJB CalculatorMDB:

public class CalculatorMDB implements MessageDrivenBean, MessageListener {
. . .

all'arrivo del messaggio sulla destinazione JMS sul quale il MDB è registrato

public void onMessage(Message msg){

viene estratto il messaggio arrivato (che contiene l'indirizzo email del richiedente e la parola di cui si vuole sapere la lunghezza)

ObjectMessage om = (ObjectMessage)msg;
MailOM mail = (MailOM)om.getObject();
String parola = mail.getBodyMail();

e la coda sulla quale il client si aspetta di ricevere il messaggio JMS di notifica.

Queue dest = (Queue) msg.getJMSReplyTo();

Sulla questa coda il MDB invierà il messaggio di conferma dell'attivazione servizio di busi-ness (il calcolo di lunghezza della parola)

this.sendJMSReply("La stringa ["+parola+"] verra' processata ...",dest);

Il metodo sendJMSReply() provvede a inviare il messaggio alla destinazione specificata, cioè l'oggetto destination classe javax.jms.Queue:

private void sendJMSReply(String result, Queue destination)throws JMSException {
QueueSender sender = session.createSender(destination);
TextMessage tm = session.createTextMessage("" + result);
sender.send(tm);
sender.close();
}

A questo punto, il MDB demanda il calcolo della lunghezza all'EJB CalculatorBean installato su JBoss

int result;
try{
  Properties properties = new Properties();
  properties.put(Context.INITIAL_CONTEXT_FACTORY,                  "org.jnp.interfaces.NamingContextFactory");
  properties.put(Context.PROVIDER_URL, "jnp://localhost:1099");
  Context ctx = new InitialContext(properties);
  Object ref = ctx.lookup(JNDI_NAME);
  CalculatorHome home =(CalculatorHome)PortableRemoteObject.narrow(
                                       ref, CalculatorHome.class);
  Calculator calc = home.create();

invocandone il metodo di business getLength()

  result = calc.getLength(tm.getText());
  mail.setBodyMail("La lunghezza di ["+parola+"] e' di: " +
                   result+".");
  this.sendEmailReply(mail);

Il risultato ottenuto viene passato come parametro al metodo sendEmailReply() che provvede a inviare una email all'opportuno destinatario mediante le API javax.mail:

private void sendEmailReply(MailOM result)
             throws NamingException, MessagingException{
  String risorsa="java:comp/env/mail/MokaMailSession";
  String recipient=result.getRecipient();
  javax.naming.Context initial = new javax.naming.InitialContext();
  javax.mail.Session session =(javax.mail.Session)   initial.lookup(risorsa);
  javax.mail.Message msg = new   javax.mail.internet.MimeMessage(session);
  msg.setRecipients(javax.mail.Message.RecipientType.TO,
  javax.mail.internet.InternetAddress.parse(recipient, false));
  msg.setSubject("Message from Calculator Service");
  msg.setText("" + new java.util.Date() + result.getBodyMail());
  msg.setHeader("X-Mailer", "CalculatorMDB");
  msg.setSentDate(timeStamp);
  javax.mail.Transport.send(msg);
}

Per il deploy del MDB bisogna indicare nel file ejb-jar.xml che il CalculatorMDB è collegato ad una risorsa JMS di tipo javax.jms.Queue e utilizza una javax.mail.Session di nome logico mail/MokaMailSession

<ejb-jar>
<enterprise-beans>
. . .
<message-driven>
<ejb-name>CalculatorMDB</ejb-name>
<ejb-class>it.mokabyte.eai.my.ejb.CalculatorMDB</ejb-class>
. . . ..
<message-driven-destination>
<destination-type>javax.jms.Queue</destination-type>
</message-driven-destination>
<resource-ref>
<description />
<res-ref-name>mail/MokaMailSession</res-ref-name>
<res-type>javax.mail.Session</res-type>
<res-auth>Container</res-auth>
</resource-ref>
</message-driven>
. . .

Nel file proprietario di JBoss (jboss.xml) si esplicita il mapping tra i nomi logici specificatinel file ejb-jar.xml e i nomi delle risorse fisiche dell'Application Server.

<?xml version="1.0" encoding="UTF-8"?>
<jboss>
<enterprise-beans>
. . .
<message-driven>
<ejb-name>CalculatorMDB</ejb-name>
<destination-jndi-name>queue/MokaEaiQueue</destination-jndi-name>
<resource-ref>
<res-ref-name>mail/MokaMailSession</res-ref-name>
<jndi-name>java:/JBossMailSession</jndi-name>
</resource-ref>
</message-driven>

. . .

Installato l'EJB è possibile invocarlo mediante un client che comunica, in modo indiretto, con il MDB inviando il messaggio sulla coda avente JNDI name queue/MokaEaiQueue:

public class CalculatorTestClient {
  public void test() {
    try {
      Context ctx = new InitialContext();
      QueueConnectionFactory queueConnectionFactory =
           (QueueConnectionFactory)ctx.lookup("ConnectionFactory");
      Queue queue = (Queue) ctx.lookup("queue/MokaEaiQueue");

      MailOM mail = new MailOM("srossini@imolinfo.it",
                               "Ciao a tutti!");
      messageToQueue.setObject(mail);
      // La coda su cui ricevere la risposta JMS
      Queue queue_RX = (Queue) ctx.lookup("queue/MokaEaiQueue_RX");
      // set della coda di risposta nel messaggio da inviare
      messageToQueue.setJMSReplyTo(queue_RX);
      // invio del messaggio
      queueSender.send(messageToQueue);
      // creazione del ricevitore
      QueueReceiver queueReceiver = queueSession.
                                    createReceiver(queue_RX);

      // Ricezione sincrona
      Message m = queueReceiver.receive(1000);
      if (m != null) {
        if (m instanceof TextMessage) {
          TextMessage message = (TextMessage) m;
          System.out.println("CalculatorTestClient.test:
                              messaggio ricevuto[" +
                              message.getText() + "]");
        }
      }

 


Figura 1 : integrazione mediante JMS

Integrazione mediante SOAP & WS
Generalmente i firewall lasciano aperte determinate porte per specifici protocolli ed in gene-rale lasciano aperta la sola porta 80 del server per permettere comunicazioni mediante HTTP.
Entrambi i protocolli IIOP e JMS hanno in comune il fatto di richiedere l'apertura di oppor-tune porte dei firewall per comunicare. Ad esempio per RMI/IIOP il server deve mettere a disposizione almeno due porte TCP: la porta per il "Bootstrap name service" (di default è la 900) e la porta del "CORBA Listener Port" (che non ha default e viene indirizzata in modo casuale su quelle libere).
Per "passare" i firewalls è possibile ricorrere all'HTTP tunnelling "imbustando" il protocol-lo IIOP o JMS in pacchetti http; questo introduce logicamente un overhead di elaborazione che si riflette sulle prestazioni.
Caratteristica principale dei Web Services è di permettere l'interoperabilità tra sistemi etero-genei (come si vedrà nel prossimo articolo) ed inoltre di essere intrinsecamente "user-friendly" rispetto ai firewall.
Infatti i Web Services (vedi[MOKA_WS]) usano SOAP come protocollo di trasporto che si appoggia ad HTTP e non richiede quindi l'apertura ad hoc di porte di firewalls.
Si può quindi decidere di esportare il servizio di calcolo della stringa come WEB Service per incapsulare ad esempio il nostro EJB Session Stateless CalculatorBean.
Di fatto si espone il servizio di business dell'EJB Calculator con una diversa tecnologia che permette una diversa modalità di accesso.


Figura 2
: Esposizione del servizio Calculator

Per fare questo useremo AXIS [AXIS], un ottimo framework per sviluppare programmi ba-sati su SOAP e in grado di esporre Web Services (WS). L'installazione di Axis è semplice (vedi[MOKA_AXIS]), basta copiare il folder webapps/axis dalla distribuzione di Axis nella directory webapps di Tomcat. I WS saranno accessibili all'URL http://hostname:port/axis/...
Se non si utilizza il JDK1.4.x e Tomcat 4.1.x, per usare Axis è necessario copiare il parser XML Xerces (xercesImpl.jar e xmlParserAPIs.jar) nella directory WEB-INF/lib della we-bApp di Axis. Con JDK1.4.x e Tomcat 1.4.x viene utilizzato l'Xerces parser presente nella directory endorsed di Tomcat.
A questo punto Tomcat è configurato per esporre web service con Axis.
Le classi che si vogliono esporre come web service devono essere inserite in WEB-INF/classes della webApp di Axis.
Installato Axis procediamo quindi a costruire un Web Service che fornisce il servizio di cal-colo della lunghezza della stringa.
Il WS in Axis è una normale classe Java; nell'esempio riportato è una classe di nome Calcula-torService che espone un metodo getLength():

public class CalculatorService {

  public CalculatorService () {}

  public int getLength(String arg) {
    int result = -1;
    try {

Una volta invocato, il WS provvede a istanziare in modo opportuno il JNDI Context

      Properties properties = new Properties();
      properties.put(Context.INITIAL_CONTEXT_FACTORY,                      "org.jnp.interfaces.NamingContextFactory");
      properties.put(Context.PROVIDER_URL, jnp://localhost:1099);
      // JndiContext
inizializzato per JBoss
      Context ctx = new InitialContext(properties);

ad effettua la lookup dell'EJB

      Object ref = ctx.lookup("MokaCalculator");
      CalculatorHome home = (CalculatorHome)
      PortableRemoteObject.narrow(ref, CalculatorHome.class);

e la relativa creazione

      Calculator calc = home.create();

per poi invocare e ritornare il risultato del metodo di business getLength()

      result = calc.getLength(arg);
    }
    catch(Exception ex) {
      ex.printStackTrace();
    }
  return result;
  }
}


Come si evince dal codice, il servizio agisce da "wrapper" dell'EJB CalculatorBean delegando ad esso l'implementazione della logica di business. Affinchè il WS sia in grado di comunicare con il CalculatorBean installato su JBoss si devono aggiungere i jar dell'EJB (Home e Remote interface) e i jar necessari per la comunicazione con JBoss (jboss.jar,jboss-j2ee.jar,jnpserver.jar e jboss-common.jar)nella directory WEB-INF/lib della WebApp di Axis.
Una volta posizionate le classi del servizio nella webApp di Axis (AXIS\WEB-INF\classes oppure in AXIS\WEB-INF\lib nel caso di jar), è possibile procedere al deploy del WS.
Per fare questo bisogna costruire un opportuno deployment descriptor(DD) che altro non è che un file XML (di estensione wsdd) che contiene le informazioni di deploy del servizio.
Nel caso del nostro esempio il DD è il seguente:

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

<service name="MokaCalculatorService" provider="java:RPC">
<parameter name="className" value="it.mokabyte.eai.my.webservices.CalculatorService"/>
<parameter name="allowedMethods" value="*"/>
</service>
</deployment>

L'elemento service definisce il WS a cui è stato dato il nome MokaCalculatorService mediante l'attributo name. L'attributo provider permette di specificare il modo in cui il servizio viene esposto; in questo caso il provider è una classe Java, quindi si specifica java:RPC.
Il deploy vero e proprio avviene mediante il tool da linea di comando AdminClient includendo nel classpath i jar di Axis:

java org.apache.axis.client.AdminClient deploy.wsdd

Installato il Web service è possibile ottenere il suo WSDL aggiungendo "?WSDL" all'en-dpoint del servizio (nel nostro esempio l'url è http://localhost:8080/axis/services/MokaCalculatorService?WSDL)


Figura 3
: La descrizione WSDL del servizio CalculatorService
(clicca sull'immagine per ingrandire)

Questa funzionalità è molto utile ed è utilizzabile da un qualsiasi client SOAP in grado di uti-lizzare WSDL. Partendo dal documento WSDL è possibile generare le classi da usare lato client utilizzando il tool WSDL2Java

java org.apache.axis.wsdl.WSDL2Java -o ..\src -p it.mokabyte.eai.my.webservices.generated http://localhost:8080/axis/services/MokaCalculatorService?wsdl

dove con il flag -o si indica dove generare i sorgenti e con il flag -p il package che conterrà le classi generate.
Per il WSDL del servizio MokaCalculatorService vengono generate le seguenti classi all'interno del package specificato da linea di comando (it.mokabyte.eai.my.webservices.generated):

CalculatorService : è l'interfaccia, che estende java.rmi.Remote
public interface CalculatorService extends java.rmi.Remote { …
che viene ottenuta a partire dal portType del file WSDL (in Axis viene detta SDI-Service Definition Interface)<wsdl:portType name="CalculatorService">
L'interfaccia pubblica con i metodi di business del WS, che nel nostro caso, è il solo:
public int getLength(java.lang.String in0) throws java.rmi.RemoteException;

CalculatorServiceService: è l'interfaccia RPC del servizio
public interface MokaCalculatorService extends javax.xml.rpc.Service { . . .
che viene derivata dal servizio definito nel documento WSDL
<wsdl:service name="CalculatorServiceService"> . . .

CalculatorServiceServiceLocator
E' la classe locator che permette di localizzare e ottenere un'istanza dello stub del servizio
public class CalculatorServiceServiceLocator
             extends org.apache.axis.client.Service
             implements CalculatorServiceService {

  private final java.lang.String MokaCalculatorService_address =
           "http://host:8080/axis/services/MokaCalculatorService";

I metodi piu' importanti sono getMokaCalculatorService() che permette di ottenere un reference al WS utilizzando l'endpoint specificato al lancio del tool WSDL2Java

public ws.axis.CalculatorService getMokaCalculatorService() throws javax.xml.rpc.ServiceException {
return getMokaCalculatorService(new java.net.URL(MokaCalculatorService_address););
}

ed il metodo overloaded che prevede l'URL dell'endpoint del WS come parametro

public ws.axis.CalculatorService getMokaCalculatorService(java.net.URL portAddress)
throws javax.xml.rpc.ServiceException {
try {
ws.axis.MokaCalculatorServiceSoapBindingStub _stub =
new ws.axis.MokaCalculatorServiceSoapBindingStub(portAddress, this);
...
return _stub;
...
}

MokaCalculatorServiceSoapBindingStub
Questa classe implementa l'interfaccia SDI CalculatorService e contiene il codice che converte le invocazioni dei metodi nelle API di Axis nascondendo allo sviluppatore i dettagli della chiamata SOAP, il tipo di codifica (encoded, literal) e lo stile del collega-mento (RPC, Document).

public class MokaCalculatorServiceSoapBindingStub extends Stub
             implements CalculatorService {

static OperationDesc [] _operations;
  static {

  oper.setName("getLength");
  oper.addParameter(new javax.xml.namespace.QName("", "in0"),
                    new QName("http://www.w3.org/2001/XMLSchema",
                              "string"),
                    String.class,
                    ParameterDesc.IN, false, false);
  oper.setReturnType(new QName("http://www.w3.org/2001/XMLSchema",
                               "int"));
  oper.setStyle(org.apache.axis.enum.Style.RPC);
  oper.setUse(org.apache.axis.enum.Use.ENCODED);
  ...

  public int getLength(java.lang.String in0)
             throws RemoteException {
    org.apache.axis.client.Call _call = createCall();
    _call.setSOAPActionURI("");
    ...
    java.lang.Object _resp = _call.invoke(new Object[] {in0});
    ...
    getResponseHeaders(_call);
    return ((java.lang.Integer) _resp).intValue();
    ...

Vediamo quindi come utilizzare le classi generate per creare un client del nostro WS.

Il client importa la classe Locator e l'interfaccia del servizio WS

import it.mokabyte.eai.my.webservices.generated.
       axis.MokaCalculatorServiceLocator;
import it.mokabyte.eai.my.webservices.generated.
       axis.CalculatorService;

si ricava il WS mediante il ServiceLocator sfruttando l'endpoint implicito

CalculatorService calc=new CalculatorServiceServiceLocator().getMokaCalculatorService();

oppure esplicitandolo come parametro

CalculatorService calc = new CalculatorServiceServiceLocator().
     getMokaCalculatorService(new URL("http://localhost:8080/
                              axis/services/MokaCalculatorService"));

per poi invocare il metodo di business getLength()

int res=calc.getLength("Ciao a tutti I lettori di Mokabyte!");

ed infine stampare il risultato

System.out.println("Risultato: " + res);
}catch(javax.xml.rpc.ServiceException rpce) { . . . }

Come si vede il codice è estremamente semplice e conciso.
Il tool WSDL2Java è quindi molto importante poichè permette di semplificare notevolmente lo sviluppo di un client Java riducendo la quantità di codice da scrivere e senza fare riferi-mento diretto alle API Axis o JAX-RPC.
Axis mette inoltre a disposizione una comoda utility, il TCPMonitor, che permette di moni-torare il traffico di rete generato dal servizio:

java org.apache.axis.utils.tcpmon [listenPort targetHost targetPort]

dove listenPort è il numero di porta su cui TCPMon si pone in ascolto per le richieste, targe-tHost è l'host su cui inoltrare le richieste e targetPort è il numero di porta utilizzato dall'host target.
In questo modo è possibile monitorare le richieste SOAP e le relative risposte.


Figura 4
: Il TCP Monitor di Axis in azione
(clicca sull'immagine per ingrandire)

Infine è utile aggiungere che Axis permette l'esposizione diretta di un EJB come WebServi-ce. In questo modo il WS espone direttamente la logica applicativa implementate dall'EJB stesso.
Rispetto al precedente deployment descriptor bisogna indicare un provider di tipo java:EJB e specificare le interfacce home e remote, il nome e le property JNDI:

<deployment xmlns="http://xml.apache.org/axis/wsdd/"
xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
<service name="MokaCalculatorServiceEJB" provider="java:EJB">
<parameter name="className" value="it.mokabyte.eai.my.ejb.CalculatorBean"/>
<parameter name="beanJndiName" value="MokaCalculator"/>
<parameter name="homeInterfaceName" value="it.mokabyte.eai.my.ejb.CalculatorHome"/>
<parameter name="remoteInterfaceName" value="it.mokabyte.eai.my.ejb.Calculator"/>
<parameter name="jndiContextClass" value="org.jnp.interfaces.NamingContextFactory"/>
<parameter name="jndiURL" value="localhost:1099"/>
<parameter name="allowedMethods" value="getLength"/>
</service>
</deployment>

 

Conclusioni
In questo articolo si è introdotta l'integrazione mediante messaggi. Si è parlato di JMS e di come sviluppare Web Service in Java utilizzando Axis.
Nel prossimo articolo si analizzeranno scenari di interoperabilità tra Web Service teconolo-gicamente eterogenei; in particolare si affronterà la comunicazione tra WS scritti in Java (A-xis), in C# (.NET) ed in C++ (gSoap).

 

Bibliografia
[INT1] S.Rossini: Integrazione di applicazioni Enterprise (I) MokabyteN.71-Aprile 2003
[INT2] S.Rossini: Integrazione di applicazioni Enterprise (II) MokabyteN.72-Maggio 2003
[INT3] S.Rossini, A. D'Angeli: Integrazione di applicazioni Enterprise (III) Mokabyte N.73-Giugno 2003
[MOKA_MDB]G.Puliti: "EJB 2.0: Message Driven Beans" Mokabyte N.69-10mbre 2002
[MOKA_JMS] S.Rossini: "JMS-La gestione dei messaggi", Mokabyte N.60, 61,68,69
[MOKA_MFP] S.Rossini : "Il pattern Message Facade" Mokabyte N.71 febbraio 2003
[AXIS] Apache Group - "Axis", http://xml.apache.org/axis
[MOKA_AXIS]A. Giovannini: Apache Axis:Il Soap per Java Mokabyet N.66 7mbre 2002
[MOKA_WS] M. Bigatti: Corso di Java Web Services Mokabyte N.71 e successivi
[WSDL] W3C - "Web Services Description Language" - http://www.w3.org/TR/wsdl

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it