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
|