Java:JMX
Sommaire
But
JMX est une spécification de SUN permettant la supervision et l'administration d'applications Java, c'est à dire:
- modifier dynamiquement son comportement,
- générer des statistiques en temps réel sur son fonctionnement,
- et notifier des dysfonctionnements.
Les MBeans (Manageable Beans)
Les MBeans sont des objets contenant les données et les méthodes de supervision et d'administration. Ces données peuvent être consultables et/ou modifiables et ces méthodes peuvent être accessibles par la ou les consoles d'administration.
Il existe 4 types de MBean:
- les MBeans standards qui ont leurs données et méthodes définies au moment de la conception.
- les MBeans dynamiques qui ont leurs données et méthodes définies au cours de l'exécution de l'application.
- les MBeans ouverts sont des MBeans dynamiques mais qui utilisent des classes normalisés: ainsi l'application de management n'a pas besoin d'avoir accès aux classes des MBeans ouverts de l'agent.
- les Mbeans modèles qui sont des instances d'un MBean dynamique générique fournis par JMX et configurable.
Chaque MBean peut être défini persistent en implémentant l'interface javax.management.PersistentMBean (les métriques sont sauvegardés à chaque arrêt de l'agent et rechargés au redémarrage).
MBeans standards
Pour définir un MBeans standards, il faut:
- créer la classe contenant les données (avec un accès via des getters et setters) et les méthodes de supervision et d'administration,
- créer l'interface que la classe précédemment créée doit implémenter avec un nom préfixé par le nom de la classe et suffixé par "MBean".
ATTENTION: La classe et son interface doivent se trouver dans le même package.
Exemple
Fichier ExampleMBean.java:
package org.minetti.example.mbean; public interface ExampleMBean { public int getValue(); public void setValue(int value); public boolean isEnabled(); public void action(int param); }
Fichier Example.java
package org.minetti.example.mbean; import org.minetti.example.AppObj; public class Example implements ExampleMBean { private int value = 0; private boolean enabled = false; public int getValue() { return value; } public void setValue(int value) { this.value = value; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public void action(int param) { AppObj.getInstance().action(param); } }
Exemple avec une description des membres
Il est possible de fournir à un MBean standards une description pour chaque attribut et méthode (comme pour les MBeans dynamiques) en héritant de la classe javax.management.StandardMBean:
Fichier Example.java
package org.minetti.example.mbean; import org.minetti.example.AppObj; import javax.management.StandardMBean; import javax.management.MBeanInfo; public class Example extends StandardMBean implements ExampleMBean { private int value = 0; private boolean enabled = false; public Example() { super(ExampleMBean.class); cacheMBeanInfo(new MBeanInfo( ... )); } public int getValue() { return value; } public void setValue(int value) { this.value = value; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public void action(int param) { AppObj.getInstance().action(param); } }
L'instance de la classe javax.management.MBeanInfo va contenir la description de chaque membre du MBean.
MBeans dynamiques
Pour définir un MBeans dynamiques, il faut créer une classe implémentant l'interface javax.management.DynamicMBean. Cette interface impose de définir les méthodes suivantes pour gérer les données:
Object getAttribute(String attribute) { // Traitement pour renvoyer la valeur d'une donnée } void setAttribute(Attribute attribute) { // Traitement pour mettre à jour la valeur d'une donnée } AttributeList getAttributes(String[] attributes) { // Traitement pour renvoyer les valeurs de plusieurs données } AttributeList setAttributes(AttributeList attributes) { // Traitement pour mettre à jour les valeurs de plusieurs données et retourne les nouvelles valeurs }
pour gérer les actions:
Object invoke(String actionName, Object[] params, String[] signature) { // Traitement pour exécuter des actions }
et pour obtenir des informations sur le MBean:
MBeanInfo getMBeanInfo() { // Traitement pour renvoyer des informations sur le MBean }
La classe javax.management.Attribute est une classe qui contient le nom et la valeur d'une donnée.
La classe javax.management.AttributeList est une liste d'objet javax.management.Attribute.
La classe javax.management.MBeanInfo est une classe qui contient des informations sur le MBean.
MBeans ouverts
Comme pour les MBeans dynamiques, pour définir un MBean ouvert il faut créer une classe implémentant l'interface javax.management.DynamicMBean. Mais à la différence d'un MBean dynamique, la méthode getMBeanInfo doit retourner une instance implémentant l'interface javax.management.openmbean.OpenMBeanInfo: dans la plupart des cas, on renverra une instance de la classe javax.management.openmbean.OpenMBeanInfoSupport.
Les MBeans ouverts ne supportent que les types de données suivants:
- boolean ou java.lang.Boolean,
- byte ou java.lang.Byte,
- short ou java.lang.Short,
- int ou java.lang.Integer,
- long ou java.lang.Long,
- java.math.BigInteger,
- float ou java.lang.Float,
- double ou java.lang.Double,
- java.math.BigDecimal,
- char ou java.lang.Character,
- java.lang.String,
- java.util.Date,
- java.lang.Void (uniquement pour les retours d’opérations),
- toutes les types implémentant l'interface javax.management.openmbean.CompositeData (équivalent à une Map),
- toutes les types implémentant l'interface javax.management.openmbean.TabularData (équivalent à un tableau de CompositeData),
- javax.management.ObjectName.
MBeans modèles
Pour définir un MBean modèle, il faut créer une instance de javax.management.modelmbean.RequiredModelMBean. Cette classe est en quelque sorte un MBean dynamique générique. Afin de le personnaliser, on passera une instance de javax.management.modelmbean.ModelMBeanInfoSupport au constructeur.
Les notifications
Chaque MBean peut émettre des notifications vers l'agent JMX. Ce dernier se chargera de les renvoyer vers les différents clients abonnés aux notifications.
Emission d'une notification
Tout d'abord, une notification est créée en instanciant la classe javax.management.Notification. Les différents constructeurs de cette classe possèdent les paramètres suivants:
- le type (String) qui indique la nature de la notification (par exemple: alert.level.critical),
- la source (Object) qui contient généralement l'ObjectName du MBean émetteur de la notification,
- la sequenceNumber (long) qui est un numéro de séquence qui doit s'auto-incrémenter pour identifier la notification,
- le timeStamp (long) qui est le moment où la notification a eu lieu (nombre de millisecondes écoulées depuis le 1er janvier 1970 00:00:00 UTC),
- le message (String) qui contient une explication détaillée sur la notification.
Il est possible d'embarquer d'autres données dans la notification à l'aide de la méthode setUserData(Object). Dans le cas de plusieurs données, on passera à la méthode une Map.
A noter qu'un MBean peut connaître son ObjectName s'il implémente l'interface javax.management.MBeanRegistration (voir méthode preRegister(MBeanServer, ObjectName)).
Une fois la notification prête, il faut l'envoyer à l'agent JMX. Pour pouvoir l'envoyer, le MBean doit hériter de la classe javax.management.NotificationBroadcasterSupport. Ensuite, il suffit d'appeler la méthode sendNotification(Notification).
Il est possible de donner une description de chaque notification pouvant être émis par le MBean. Pour cela, on redéfinira la méthode getNotificationInfo():
@Override public MBeanNotificationInfo[] getNotificationInfo () { final MBeanNotificationInfo info = new MBeanNotificationInfo ( new String []{"alert.level.unknown", "alert.level.ok", "alert.level.warning", "alert.level.critical"}, Notification.class.getName (), "Notification indiquant ..."); return new MBeanNotificationInfo []{info}; }
Abonnement aux notifications
Un client connecté à l'agent JMX peut s'abonner aux notifications émisent par les MBeans. Pour cela, il faudra définir une classe d'écoute qui doit implémenter l'interface javax.management.NotificationListener.
L'abonnement se fait en enregistrant une instance de cette classe d'écoute auprès de l'agent JMX. Pour cela, on fera appel à la méthode addNotificationListener(...) de l'agent JMX (javax.management.MBeanServer - voir le chapitre sur l'agent JMX). Cette méthode prend les paramètres suivants:
- name qui indique l'ObjectName du MBean émetteur des notifications,
- listener qui contient une instance de notre classe d'écoute précédemment définie,
- filter qui contient une instance implémentant l'interface javax.management.NotificationFilter pour filtrer les notifications émises (NULL pour tout recevoir),
- handback de type Object qui contient un contexte à envoyer systématiquement à notre classe d'écoute.
Pour un client distant, l'enregistrement se fera en appelant la méthode addConnectionNotificationListener(...) d'une instance de la classe javax.management.remote.rmi.RMIConnector.
Abonnement aux notifications de l'agent JMX
L'agent JMX émet lui aussi toutes sortes de notifications. On retiendra la javax.management.MBeanServerNotification qui est émise à chaque:
- enregistrement d'un MBean (type JMX.mbean.registered),
- et désenregistrement d'un MBean (type JMX.mbean.unregistered).
l'ObjectName du MBean concerné par l'enregistrement ou désenregistrement est retourné par la méthode getMBeanName() de la notification.
L'abonnement pourra se faire de la façon suivante:
final MBeanServerListener listener = new MBeanServerListener (); final MBeanServerNotificationFilter filter = new MBeanServerNotificationFilter (); filter.enableAllObjectNames (); agent.addNotificationListener (MBeanServerDelegate.DELEGATE_NAME, listener, filter, null);
Notons l'utilisation du filtre javax.management.relation.MBeanServerNotificationFilter afin de recevoir uniquement des javax.management.MBeanServerNotification.
Le désabonnement se fera de la façon suivante:
agent.removeNotificationListener (MBeanServerDelegate.DELEGATE_NAME, listener);
Les javax.management.MBeanServerNotification sont parfois utilisés par les adaptateurs JMX.
L'agent JMX
Quelque soit l'API choisi, l'agent JMX est obtenu par la fabrique java.lang.management.ManagementFactory et implémente l'interface javax.management.MBeanServer.
final MBeanServer myAgent = ManagementFactory.getPlatformMBeanServer ();
ATTENTION: Concernant les applications WEB, l'agent JMX obtenu par la commande précédente est le même pour toutes les applications s'exécutant dans un même serveur d'applications (Apache Tomcat, JBoss, etc...). Il faudra donc s'assurer que tous les noms des MBeans (et aussi des adaptateurs JMX) soient bien unique. Dans le même registre et qui peut s'avérer très utile, le code suivant permet de tester si un MBean appartient bien à l'application:
if (this.getClass ().getClassLoader ().equals (myAgent.getClassLoaderFor (objectName))) { ... }
Il est possible aussi d'obtenir un nouvel agent JMX par la fabrique javax.management.MBeanServerFactory:
MBeanServer myAgent = MBeanServerFactory.createMBeanServer();
Enregistrement d'un MBean
L'enregistrement de MBeans se fait à l'aide de la méthode registerMBean de l'agent JMX. La classe javax.management.ObjectName permet d'indiquer un nom au MBean.
MyStatistics example = new MyStatistics(); ObjectName exampleMBeanName = new ObjectName("org.minetti.test:type=Statistics,name=example"); myAgent.registerMBean(example, exampleMBeanName);
Enregistrement d'un adaptateur
L'enregistrement d'un adaptateur (ou connecteur) se fait de la même façon que le MBean.
MyAdapter myAdapter = new MyAdapter(); ObjectName myAdapterName = new ObjectName("org.minetti.test:adaptor=MyAdapter"); myAgent.registerMBean(myAdapter, myAdapterName);
Désenregistrement d'un MBean ou d'un adaptateur
Le désenregistrement est vivement recommandé pour les applications WEB. Il se fait à l'aide de la méthode unregisterMBean de l'agent JMX.
myAgent.unregisterMBean (objectName);
Quelques commandes utiles
Pour obtenir la liste de tous les MBeans enregistrés auprès de l'agent JMX:
final Set<ObjectName> mBeanNameList = myAgent.queryNames (null, null);
Pour obtenir le nom de la classe d'un MBean:
final String className = myAgent.getObjectInstance (objectName).getClassName ();
Pour obtenir la valeur d'un métrique:
final Object attrValue = myAgent.getAttribute (objectName, attributeName);
Pour changer la valeur d'un métrique:
final Attribute attribute = new Attribute (attributeName, attrValue); myAgent.setAttribute (objectName, attribute);
Les adaptateurs JMX
Les adaptateurs (ou connecteurs) sont des objets chargés de la publication des métriques à travers un protocole réseau (HTML, SNMP, RMI, etc...).
La plupart des adaptateurs JMX:
- implementent l'interface javax.management.MBeanRegistration afin de répondre à chaque enregistrement ou désenregistrement de l'adaptateur auprès d'un agent JMX,
- et fournissent une méthode start et stop qui permet d'activer ou d'arrêter le service réseau.
Adaptateur RMI
L'adaptateur RMI est très intéressant car il permet de donner un accès distant à n'importe qu'elle application Java vers un agent JMX, en fournissant toutes les fonctionalités qu'offre JMX. On utilisera la classe javax.management.remote.JMXConnectorServer pour mettre en oeuvre le protocole RMI. La fabrique javax.management.remote.JMXConnectorServerFactory permet d'obtenir une instance de celle-ci. Le paramétrage du protocole se fera sous forme d'URL encapsulée dans la classe javax.management.remote.JMXServiceURL.
final MBeanServer myAgent = ManagementFactory.getPlatformMBeanServer (); final JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:9000/server"); final JMXConnectorServer connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, null, myAgent); connectorServer.start();
Avant chaque démarrage de l'application, il faudra s'assurer d'avoir démarrer le RMI Registry:
$JAVA_HOME/bin/rmiregistry 9000
Maintenant, on peut utiliser l'application jconsole pour se connecter à l'agent JMX via RMI:
$JAVA_HOME/bin/jconsole
Dans remote process on indiquera notre URL:
service:jmx:rmi:///jndi/rmi://localhost:9000/server
Le code qui suit permet d'obtenir notre agent JMX via RMI à partir d'une application distante:
final JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:9000/server"); final JMXConnector connector = JMXConnectorFactory.connect(url, null); final MBeanServerConnection agent = connector.getMBeanServerConnection();
La classe javax.management.MBeanServerInvocationHandler permet d'obtenir l'instance d'un MBean:
final ExampleMBean mbean = (ExampleMBean) MBeanServerInvocationHandler.newProxyInstance(agent, objectName, ExampleMBean.class, false);
Adaptateur JMXMP
La mise en oeuvre de l'adaptateur JMXMP est identique à celle de l'adaptateur RMI. Seul l'URL change:
final JMXServiceURL url = new JMXServiceURL("service:jmx:jmxmp://localhost:9998");
De plus, lors de l'exécution de l'agent et/ou du client il sera nécéssaire d'indiquer dans le classpath le JAR suivant:
L'avantage du protocole JMXMP par rapport à RMI est qu'il n'a pas besoin d'autres applications comme le rmiregistry pour fonctionner.
Adaptateur HTML
Adaptateurs SNMP
- net.sf.snmpadaptor4j.SnmpAdaptor
- com.sun.jdmk.comm.SnmpAdaptorServer
- com.sun.jdmk.comm.SnmpV3AdaptorServer
- com.sun.jmx.snmp.daemon.SnmpAdaptorServer