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 programma: 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 esempio 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 rappresenta il framework esecutivo indipendente dalla applicazione sviluppata. In realtà se si guardano 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 "applicazione".

In tale ottica l'applicazione complessiva muterà il suo comportamento e caratteristiche 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 presenti senza legare l'applicazioni client di management con la risorsa stessa. In effetti uno degli obiettivi principali della API (tant'è che ne compare spessissimo un riferimento nella documentazioni 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 comunicazione 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 fondamentale 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 diverse tipologie:

Gli standard MBeans ed i Dynamic MBeans sono i due tipi base a cui si aggiungono 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 versione 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 differenti, 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 risorse gestite
Figura 1 - L'agent si pone come strumento di separazione fra client e risorse gestite

L'agent Level è costituito anche da alcuni MBeans di servizio: quattro di questi sono obbligatori 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 operazioni a determinati intervalli di tempo), Monitoring service (implementano il pattern observer e svolgono la funzione di osservatori delle modifiche oltre a notificarle a tutti gli ascoltatori interessati) ed il Relation service (permette di creare relazioni fra beans).

Figura 2 - Architettura principale dell'Agent JMX
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 accade 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 strutture 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 esponendo 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 convenzioni 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 registri 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 momento 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 conseguenza anche il managed object possono essere acceduti sia tramite un protocollo di connessione 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 strettamente 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 registrazione 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);
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 managed object.

Figura 3 - La console JMX HTML offerta dall'adapter HTTP della implementazione JMX della Sun
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à
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 operazioni 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 solamente 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 NotificationListener) 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 identificare 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 stringa 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 utilizzato.

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 controllo verifica in modo molto banale se l'evento corrisponde alla rimozione di un utente oppure no.

Come spesso accade in Java, invece di implementare l'interfaccia NotificationBroadcaster e fornire l'implementazione per tutti i suoi metodi, si può utilizzare la classe di supporto NotificationBroadcasterSupport che di fatto fornisce una implementazione base per questo genere 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 genere 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 caso 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 cenni alle tecniche di programmazione di alcuni semplici MBeans, alla loro invocazione da remoto 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 solamente 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 microkernel 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 riferimento 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.