MokaByte 77 - 7mbre 2003 
Java Management Extentions
Gestione di applicazioni J2EE - I parte
di
Giovanni Puliti

Introduzione
La gestione delle applicazioni è un problema sempre più sentito a causa anche al crescere della complessità delle architetture moderne. Nuove tipologie di software ma soprattutto nuovi modelli architetturali stanno di giorno in giorno modificando la concezione di pro-gramma: dai classici software stand alone, client side, si sta passando a sistemi distribuiti, server side spesso basati su concetti come quello di container o provider dinamici di servizi remoti. Quest'ultime due caratteristiche in particolare hanno introdotto un altro importante concetto, quello della mutabilità delle applicazioni e della loro dinamicità: se si pensa ad e-sempio ad una web application in esecuzione all'interno di un servlet container, si è abituati a considerare l'applicazione costituita dalla sola web application, mentre il container rappre-senta il framework esecutivo indipendente dalla applicazione sviluppata. In realtà se si guar-dano le cose da un punto di vista più ampio, l'intero sistema (applicazione più container) rappresentano un solo oggetto e possono essere considerati nel loro complesso la vera "ap-plicazione". In tale ottica l'applicazione complessiva muterà il suo comportamento e caratte-ristiche ad ogni deploy e undeploy di pagine JSP, servlet o applicazioni nel complesso.
In definitiva, per come sono spesso organizzate le applicazioni attualmente, non è semplice dare una definizione che resti valida nel tempo di cosa sia e cosa non sia una applicazione; ancora più delicato è definire quindi una serie di regole per stabilire le procedure di gestione (accensione, ripartenza, reload di classi e componenti, etc..) della applicazione stessa.

 

La API JMX
JMX è stata progettata da Sun con lo scopo di fornire uno strumento di amministrazione di differenti tipologie di software tramite uno strumento unico, sufficientemente generico ma al tempo stesso potente, e che offrisse gli strumenti necessari per interagire con le risorse pre-senti senza legare l'applicazioni client di management con la risorsa stessa. In effetti uno de-gli obiettivi principali della API (tant'è che ne compare spessissimo un riferimento nella do-cumentazioni ufficiale) è il concetto di disaccoppiamento fra strati differenti di software.
Il cuore della architettura è il cosiddetto agent, che può essere visto come un bus di comuni-cazione fra le applicazioni di gestione e le risorse da gestire. L'agent può essere configurato in modo da interagire con alcuni connettori per permettere l'accesso remoto ed in genere è contornato da degli MBeans, alcuni dei quali presenti per specifica. Il componente fonda-mentale dell'Agent Layer è il MBean server, che è molto semplice ed almeno in teoria, può essere implementato su differenti piattaforme dalla J2EE alla J2ME.

L'Instrumentation Level definisce come debbano essere create le varie risorse da gestire: una risorsa JMX è quindi gestibile da una qualsiasi applicazione di management compatibile con JMX: essa può essere un oggetto, un programma, un servizio, un device ed altro ancora.
Le risorse sono create tramite la definizione di un MBeans di cui la specifica fornisce 4 diver-se tipologie: Gli standard MBeans ed i Dynamic MBeans sono i due tipi base a cui si aggiun-gono i Model MBeans, e gli Open MBeans. I primi tre sono parte integrante della specifica, e devono essere obbligatoriamente presenti in ogni implementazione JMX, mentre gli Open per il momento non sono ancora stati inseriti nella specifica ufficiale (attualmente alla versio-ne 1.1).
Altri Service MBeans proprietari possono essere sviluppati dai vari produttori ed aggiunti dinamicamente al server.
Oltre alla definizione degli MBeans, l'Instrumentation Level introduce anche il meccanismo di notifica fra beans differenti simile al sistema di comunicazione fra componenti presente nella specifica JavaBeans. Le notifiche delle modifiche possono essere propagate fra bean dif-ferenti, il server e le applicazioni di management. Per il momento la notifica remota tramite protocollo di comunicazione non è parte integrante della specifica ufficiale, anche se varie implementazioni ne offrono diverse forme.

L'Agent Level è rappresentato da un MBean Server e da una serie di agent services: il suo scopo è offrire uno strato di disaccoppiamento fra il client e la risorsa vera e propria.


Figura 1
- L'agent si pone come strumento di separazione fra client e risor-se gestite

L'agent Level è costituito anche da alcuni MBeans di servizio: quattro di questi sono obbliga-tori e definiti dalla specifica, mentre altri possono essere aggiunti dalle varie implementazioni proprietarie. I beans definiti dalla specifica sono: M-Let service (permette di caricare dalla rete altri MBeans ed aggiungerli all'agent server), Timer service (che consente di eseguire o-perazioni a determinati intervalli di tempo), Monitoring service (implementano il pattern ob-server e svolgono la funzione di osservatori delle modifiche oltre a notificarle a tutti gli ascol-tatori interessati) ed il Relation service (permette di creare relazioni fra beans).


Figura 2
- Architettura principale dell'Agent JMX


Meccanismi di invocazione
Il disaccoppiamento fra risorsa ed applicazione di management è offerto grazie al concetto di object name che rappresenta, tramite un nome, l'oggetto registrato. Ogni applicazione di management può gestire il funzionamento di un managed object, passando il suo nome all'agente per ogni operazione invocata.
Il meccanismo di invocazione remota non si basa sul modello stub-skeleton (come invece ac-cade ad esempio in RMI e EJB), ma solo sull'uso di un nome: tramite un nome si individua un oggetto e tramite un nome se ne invoca un suo metodo. Se il nome è errato l'oggetto non verrà trovato o il metodo non potrà essere invocato.
Ogni modifica introdotta nella interfaccia dell'MBean non richiede la redistribuzione della interfaccia. Questa scelta comporta una notevole semplificazione in fase di gestione di strut-ture complesse e costituite da un alto numero di oggetti, ma ovviamente elimina del tutto il type checking sui tipi sui metodi e sui valori ritornati.
Un bean può essere sostituito, ri-deployed o semplicemente aggiornato senza la necessità di aggiornare o ricreare il link con il client.

 

Un primo semplice esempio
La prima operazione da fare è quella di creare una interfaccia tramite la quale il managed object potrà essere manipolato. L'interfaccia deve esporre i metodi da invocare ed i metodi set/get corrispondenti alle proprietà che si vorranno accedere e modificare. La regola sui nomi delle proprietà segue la specifica JavaBeans, per cui per ogni coppia di metodi setXXX() e getXXX() il sistema deduce che vi sia una proprietà dal nome XXX. La mancanza di uno dei due metodi implica che la proprietà sia a sola lettura o sola scrittura. I nomi dei metodi da invocare invece non seguono nessuna regola precisa: per ogni metodo che non inizi per get o per set si assume che si tratti di un metodo di esecuzione e non di un metodo di accesso alle proprietà del bean.
L'interfaccia deve avere nome che termina per MBean, per poter informare il server che si tratta della interfaccia remota del managed object. Ad esempio se si volesse creare la versione JMX del classico HelloWorld, per prima cosa è necessario implementare la sua interfaccia e-sponendo la proprietà da stampare (name) ed il metodo da invocare, sayHello().
Ecco la definizione della interfaccia HelloMBean


public interface HelloMBean {
  public String getName();
  public void setName(String name);
  public void sayHello();
}

Fatto questo l'implementazione di tale interfaccia non presenta particolari aspetti di cui tener conto. Si dovrà fornire l'implementazione dei metodi remoti e seguire le classiche convenzio-ni per seguire le regole del contratto interfaccia-classe:

public class Hello implements HelloMBean {
  String name="";

  public static void main(String[] args) {
    Hello helloImpl1 = new Hello();
  }
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name=name;
  }
  public void sayHello() {
    System.out.println("Hello "+name);
  }
}

A questo punto è necessario creare una classe che funzioni come applicazione base e che re-gistri tale MBean all'interno dell'agent JMX.
MBeanServer è l'interfaccia offerta da tutte le implementazioni tramite la quale si può fra le altre cose aggiungere un MBean, effettuare la registrazione nel framework JMX, effettuare le invocazioni/interrogazioni dei suoi metodi.
Per prima cosa è necessario ricavare un reference all'agent, cosa che può essere fatta tramite il metodo di factory createMBeanServer() della classe MBeanServerFactory. Il valore ritornato dipende dalla implementazione utilizzata.
Tramite l'agent è possibile quindi registrare l'MBean Hello

import javax.management.*;
import com.sun.jdmk.comm.HtmlAdaptorServer;

public class Agent {
  public Agent() {
}

   public static void main(String[] args) {
    MBeanServer server = MBeanServerFactory.createMBeanServer();
    ObjectName name = null;
    try {
      name = new ObjectName("example:name=hello");
      server.registerMBean(new Hello(), name);
      System.out.println("Partito....");
    }
    catch (MalformedObjectNameException ex) {
      System.out.println("NotCompliantMBeanException "+ex1);
    }
    catch (NotCompliantMBeanException ex1) {
      System.out.println("NotCompliantMBeanException "+ex1);
    }
    catch (MBeanRegistrationException ex1) {
      System.out.println("MBeanRegistrationException "+ex1);
    }
    catch (InstanceAlreadyExistsException ex1) {
      System.out.println("InstanceAlreadyExistsException "+ex1);
    }
  }
}

A questo punto l'oggetto è registrato presso il server e potrà essere invocato in ogni momen-to tramite il meccanismo di reflection offerto dall'agent stesso. Per comprendere come ciò possa essere effettuato è necessario fare una prima importante distinzione: l'agent e di conse-guenza anche il managed object possono essere acceduti sia tramite un protocollo di connes-sione da un client remoto, sia da un client in esecuzione all'interno della stessa JVM.
Nel primo caso, anche se concettualmente più complesso, le cose da un punto di vista stret-tamente di programmazione JMX, sono più semplici dato che tutte le implementazioni di server offrono uno o più adapter per l'esecuzione remota. Ad esempio volendo interagire con l'MBean Hello tramite un browser si dovrà modificare la classe Agent aggiungendo la registra-zione dell'adapter HTTP al server. Per fare questo si potrebbe scrivere

HtmlAdaptorServer adaptor = new HtmlAdaptorServer();
name = new ObjectName("adaptor:protocol=HTTP");
server.registerMBean(adaptor, name); HtmlAdaptorServer adaptor = new HtmlAdaptorServer();
name = new ObjectName("adaptor:protocol=HTTP");
server.registerMBean(adaptor, name);
adaptor.start();

Si noti come l'adaptor venga registrato nel server in modo del tutto simile a quanto fatto per l'MBean Hello.
A tal punto si potrà utilizzare un comune browser HTTP per collegarsi alla porta 8080 dell'host su cui è in esecuzione l'agent e verificare la possibilità di interazione con il mana-ged object.


Figura 3
- la console JMX HTML offerta dall'adapter HTTP
della implementazione JMX della Sun

Quello che appare è una console HTML molto simile alla ben nota JMX-Console di JBoss, tramite la quale sarà possibile invocare i metodi di set/get sul bean, e quindi accedere alla managed property "name", così come invocare il metodo sayHello() del bean.


Figura 4
- Tramite la console HTML è possibile invocare direttamente
i metodi del bean ed impostare le sue proprietà

Nel caso in cui si voglia invece effettuare l'invocazione del managed bean da un programma in esecuzione all'interno della stessa JVM in cui opera il server, si devono eseguire alcune o-perazioni leggermente differenti. Per prima cosa si dovrà ricavare un reference al server; con la seguente istruzione si ottiene una lista di tutti i server in funzione sulla macchina

List ServerList = MBeanServerFactory.findMBeanServer(null);

Assumendo che ve ne sia uno solo effettivamente funzionante, con la seguente istruzione si ottiene il riferimento al server.

MBeanServer Server = (MBeanServer)ServerList.iterator().next();

A questo punto si può creare un ObjectName e passarlo all'agent per effettuare le ricerca fra i managed objects registrati. Al solito tramite il metodo invoke() si potrà effettuare l'invocazione del metodo sayHello().

ObjectName Name = new ObjectName("example:name=user");
Object result = Server.invoke(Name,"sayHello",null, null);

Si noti come il metodo invoke() permette di invocare il metodo esclusivamente conoscendone il nome, ma senza fare uso di nessun stub del metodo, come invece avviene in RMI.
In questo caso se qualcuno modificasse l'interfaccia HelloMBean o il bean corrispondente, per continuare a garantire al client di gestione il corretto funzionamento sarebbe necessario so-lamente conoscere il nome del metodo, ma non ridistribuire codice ai vari client.
Anche il risultato restituito è del tutto generico: una istanza di tipo Object è sufficientemente generica per evitare di dover modificare il client per ogni modifica del software sul server.

 

Notifica di eventi
Uno dei meccanismi basilari di intercomunicazione fra MBeans è quello basato sulla gestione degli eventi e della loro propagazione secondo il modello Sorgente-Ascoltatore. I messaggi (oggetto Notification) sono propagati da oggetti registrati nel server, detti broadcaster MBeans (oggetto NotificationBroadcaster) verso tutti gli ascoltatori registrati (oggetto NotificationListe-ner) su quella particolare sorgente dati e per quel particolare tipo di evento; in questo caso la selezione dei messaggi viene effettuata tramite un apposito filtro (oggetto NotificationFilter).
Notification è l'oggetto base che rappresenta un messaggio: esso deriva da java.util.EventObject, al quale aggiunge le proprietà type, sequenceNumber (un progressivo che permette di identi-ficare in modo univoco un messaggio), un time stamp, ed un campo generico nel quale poter inserire oggetti di vario tipo (la specifica non impone nessuna restrizione in tal senso ma per facilitare il compito dei connettori per protocolli remoti si consiglia di utilizzare in questo caso oggetti serializzabili).
Il notification type (ricavabile dal metodo Notification.getType()) è rappresentato da una strin-ga che in genere segue la notazione separata da punto, tipica dei nomi dei packages, anche se in questo caso non è necessario seguire nessuna regola particolare relativamente a nomi di classi o altro. Tutte le stringhe che iniziano per jmx. sono riservate per il framework JMX uti-lizzato.
Nell'esempio che segue viene utilizzata una stringa del tipo "com.mokabyte.community.user.remove" ad indicare un messaggio prodotto in corrispondenza della rimozione di un utente dalla comunità virtuale di MokaByte.
Per abilitare un MBean alla produzione di eventi si deve implementare l'interfaccia NotificationBroadcaster, che offre i due metodi

public void addNotificationListener(NotificationListener                                       notificationListener,
                                    NotificationFilter
                                      notificationFilter,
                                    Object object);
public void removeNotificationListener(NotificationListener                                        notificationListener);

utili per la registrazione e deregistrazione dei vari ascoltatori. Il metodo getNotificationInfo() restituisce informazioni aggiuntive sul messaggio ricevuto.
Gli ascoltatori di un messaggio devono implementare l'interfaccia NotificationListener che tramite il metodo

public void handleNotification(Notification notification,
                               Object handback)

possono effettuare le operazioni necessarie al ricevimento del messaggio.
Quando un ascoltatore viene registrato presso un broadcaster, esso riceve anche un filtro tramite il quale si può decidere se inoltrare o meno un messaggio agli ascoltatori: il metodo

public boolean isNotificationEnabled(Notification notification)

Permette di specificare il criterio tramite il quale determinare se un particolare evento debba essere inoltrato all'ascoltatore che ha associato tale filtro. Nell'esempio che segue tale con-trollo verifica in modo molto banale se l'evento corrisponde alla rimozione di un utente op-pure no.
Come spesso accade in Java, invece di implementare l'interfaccia NotificationBroadcaster e for-nire l'implementazione per tutti i suoi metodi, si può utilizzare la classe di supporto NotificationBroadcasterSupport che di fatto fornisce una implementazione base per questo ge-nere di oggetto.

 

Esempio
L'esempio che segue parte dalla definizione dell'MBean UserMBean visto in precedenza ed estendendolo in modo da crearne uno, BroadcastingUser, orientato alla gestione degli eventi.

public class BroadcastingUser extends User
                              implements BroadcastingUserMBean,
                                         NotificationBroadcaster{

private NotificationBroadcasterSupport Broadcaster = new NotificationBroadcasterSupport();
private long NotificationSequence=0;

  public void addNotificationListener(NotificationListener                                       notificationListener,
                                      NotificationFilter
                                      notificationFilter,
                                      Object object){
    Broadcaster.addNotificationListener(notificationListener,                                        notificationFilter, obj);
  }

  public void removeNotificationListener(NotificationListener                                          notificationListener)
              throws ListenerNotFoundException {
    Broadcaster.removeNotificationListener(notificationListener);
  }

  public MBeanNotificationInfo[] getNotificationInfo() {
    return null;
  }

  public void remove() {
    // crea un messaggio da inviare
    Notification UserNotification = new     Notification("com.mokabyte.community.user.remove",this,
                 ++NotificationSequence,  
    "User "+this.getName()+" is removed");
    // invia il messaggio tramite il broadcaster ricevuto
    // come parametro
    Broadcaster.sendNotification(UserNotification);
  }
}

Questo oggetto crea al suo interno un broadcaster per l'invio di messaggi.
L'interfaccia MBean da cui deriva è la

public interface BroadcastingUserMBean {
  public void remove();
}

Il metodo remove() rappresenta il metodo gestito come operazione JMX da un client di management: alla sua invocazione esso propagherà un messaggio di tipo "com.mokabyte.community.user.remove", in modo che tutti gli ascoltatori interessati questo ge-nere di evento provvedano alla rimozione dal server come oggetti registrati.

La classe UserListener rappresenta un ipotetico ascoltatore di Notification JMX e nel metodo handleNotification() effettua la deregistrazione dal server. L'oggetto MBeanServer in questo ca-so viene passato come parametro del costruttore alla creazione del listener stesso.

public class UserListener implements NotificationListener {
  MBeanServer Server = null;
  public UserListener(MBeanServer server) {
    Server= server;
  }

  // gestisce l'evento ricevuto
  public void handleNotification(Notification notification,
                                 Object handback) {
    String NotificationType = notification.getType();
    if (NotificationType.equals(
                         "com.mokabyte.community.user.remove")){
      System.out.println(notification.getMessage());
      try {
        Server.unregisterMBean( (ObjectName) handback);
      }
      catch (MBeanRegistrationException ex) {
        System.out.println("MBeanRegistrationException: "+ex);
      }
      catch (InstanceNotFoundException ex) {
        System.out.println("InstanceNotFoundException "+ex);
      }
    }
  }
}

infine qui di seguito è riportato il codice relativo al filtro che effettua il controllo sul tipo di messaggio da inviare. La classe UserFilter implementa il metodo isNotificationEnabled in modo da controllare se il messaggio ricevuto è di tipo "com.mokabyte.community.user.remove": solo in tal caso il broadcaster inoltra il messaggio a tale ascoltatore.

public class UserFilter implements NotificationFilter {
  public boolean isNotificationEnabled(Notification notification) {
    if (notification.getType().equals(
                              "com.mokabyte.community.user.remove"))
      return true;
    else
      return false;
  }
}

 

Conclusioni
In primo approccio al framework JMX abbiamo visto i concetti di base compresi alcuni cen-ni alle tecniche di programmazione di alcuni semplici MBeans, alla loro invocazione da re-moto ed alla gestione degli eventi. Ovviamente dato il poco spazio a dispostone all'interno di un articolo non è stato possibile affrontare nel dettaglio tutti gli aspetti che qui sono stati so-lamente introdotti.
Per chi desiderasse maggiori approfondimenti si consiglia la lettura del testo [JMXJB] che fra le altre cose riporta come esempio implementativo nella parte finale l'architettura a mi-crokernel di JBoss.
Nei prossimi mesi vedremo di affrontare nel modo più esauriente possibile gli aspetti relativi a tutte le tipologie di MBeans ed alla loro modalità di gestione da remoto.
Un'ultima nota relativa alla implementazione degli esempi mostrati in questo articolo. Dopo aver scaricato i sorgenti allegati, si dovrà procedere anche al download ed alla installazione di un Agent Provider JMX. Al momento le implementazioni forse più utilizzate sono quella Sun e quella di IBM (vedi i link nella bibliografia), mentre esiste una terza implementazione meno conosciuta di AdvanceNet (vedi [ANET]).
Per maggiori dettagli circa l'installazione ed il funzionamento di tali oggetti si faccia riferi-mento ai siti web dei produttori.

 

Bibliografia e riferimenti
[JMXSpec] - JMX Sun official reference: http://java.sun.com/products/JavaManagement
[JMXRI] - JMX Reference Implementation
http://java.sun.com/products/JavaManagement/download
[TIV] IBM JMX Tivoli implementation : http://www.alphaworks.ibm.com
[ANET] AdvanceNet JMX implementation: http://www.advancenet.com
[JMXJB] "JMX: managing J2EE with Java Management Extensions" di J. Lindfors, M.Fleury, The JBoss Group; Ed. SAMS.

 

Risorse
Gli esempi descritti in questo articolo sono organizzati in tre cartelle relative ai tre esempi (invocazione tramite connettore HTTP, invocazione in JVM, notifica di eventi). Nel file readme.txt sono presenti le istruzioni per il funzionamento.

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