jeudi 20 décembre 2012

Appel gc (Garbage Collector) et autres fonctions depuis JMX

Voici un court article de trucs et astuces pour JMX.

Sommaire :



Utilisation des composants Graphiques

Par exemple, on peut vouloir appeler le garbage collector sur une machine distante, en se connectant à JConsole c'est assez simple :

Appel GC depuis JConsole
 Ou en utilisant explicitement des Beans :

Appel au Garbage Collector depuis JConsole

Le même exemple avec Java VisualVM qui est fourni par défaut dans les JDK récents au même titre que JConsole :

Appel au Garbage Collecter en utilisant Java VisualVM


Effectuer une connexion distante

 Je remets ici comment lancer un programme Java pour permettre la connexion distante à JConsole, pour plus de détails se référer à mes précédents articles JMX - Partie 1 - Partie 2 et Partie 3 :
java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9021 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -jar mon_programme.jar
Une fois les paramètres ajoutés à la ligne de lancement, pour se connecter avec JConsole, ou Jvisualvm il suffit de rentrer l'adresse de la machine et le port :
Connexion à la JVM avec JConsole
Voici le même exemple avec JVisualvm qui est plus attrayant :

Connexion avec Java VisualVM
Pour afficher cette boîte de dialogue, il faut d'abord faire un clic-droit sur Local et cliquer sur "Add JMX Connection..."

En utilisant la ligne de commande (Jmxsh)

Jmxsh que je vous invite à télécharger sur google code est une petite bibliothèque permettant d'effectuer des appels JMX, sans être miraculeuse elle est bien pratique. En gardant l'exemple de l'appel au GarbageCollector :
java -jar jmxsh-R5.jar
History file null/.jmxsh_history not writable, command-line history disabled.
jmxsh v1.0, Tue Jan 22 17:23:12 CET 2008

Type 'help' for help.  Give the option '-?' to any command
for usage help.

Starting up in shell mode.
%
Le shell est démarré, maintenant on peut se connecter et invoquer le GarbageCollector
% jmx_connect -h localhost -p 9021
Connected to service:jmx:rmi:///jndi/rmi://localhost:9021/jmxrmi.

% jmx_invoke --mbean java.lang:type=Memory gc
can't read "MBEAN": no such variable
Le message can't read "MBEAN": no such variable ne pose pas de problèmes, après vérification le GC est bien effectué. On peut également automatiser l'appel, en copiant notre appel précédent dans un fichier :
$ echo jmx_invoke --mbean java.lang:type=Memory gc > script
$ java -jar jmxsh-R5.jar -l gc.log -h localhost -p 9021 script
Bien sûr dans le script il y a moyen d'automatiser plus de choses, on peut par exemple imaginer une série d'appel au GarbageCollector sur toutes les machines gérées :
#Contenu du fichier script
jmx_connect -h machine1 -p 9021 
jmx_invoke --mbean java.lang:type=Memory gc
jmx_close
jmx_connect -h machine2 -p 9021 
jmx_invoke --mbean java.lang:type=Memory gc
jmx_close
jmx_connect -h machine3 -p 9021 
jmx_invoke --mbean java.lang:type=Memory gc
jmx_close
jmx_connect -h machine4 -p 9021 
jmx_invoke --mbean java.lang:type=Memory gc
jmx_close
jmx_connect -h machine4 -p 9022 #Plusieurs JVM sur la même machine on change de port 
jmx_invoke --mbean java.lang:type=Memory gc
jmx_close
#Fin du fichier script

$ java -jar jmxsh-R5.jar -l gc.log script

mercredi 12 septembre 2012

Oeufs de pâques : Bitbucket.org

Je viens de trouver un oeuf de pâques chez Bitbucket.org, il est plutôt surprenant par son contenu... Pour le faire apparaître il suffit de saisir de le code Konami . Voici ce que j'ai obtenu :
Le monde petit poney est surprenant sur un site comme ça, mais c'est marrant. Le code Konami est souvent présent sur de nombreux site pour cacher des fonctionnalités. Amusez-vous bien à les chercher, pour vous aider, un site qui liste plusieurs : Konamicodesites.com

mardi 17 juillet 2012

Java : Gérer la JVM avec Java Management Extensions (JMX) Technology - Partie 3 - Création d'un serveur, intéraction avec vos beans

Après avoir lu les précédents articles sur JMX vous devez maintenant tout savoir sur le fonctionnement de JMX et sur l'interrogation d'un serveur de Beans Managables. Si ce n'est pas le cas, je vous invite à relire depuis le début :

Java : Gérer la JVM avec Java Management Extensions (JMX) Technology - Partie 1 - Présentation
Java : Gérer la JVM avec Java Management Extensions (JMX) Technology - Partie 2 - Création d'un client

Cet article va aborder la question d'un serveur JMX, il sera question de créer et d'enregistrer des MBeans ainsi que de gérer les notifications.

Lancement du serveur :

Comme indiqué dans la deuxième partie, on peut lancer le serveur depuis la ligne de commande, c'est la méthode la plus simple, pas de code à gérer, ni de droits au travers de policies Java. Le code suivant est un lancement du serveur JMX sur le port 9021 sans authentification ni cryptage :
java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9021 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -jar mon_programme.jar
L'autre méthode consiste à lancer un registre RMI et d'y enregistrer notre serveur.
package fr.technorevue.jmx;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.ExportException;

import javax.management.MBeanServer;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;

/**
* Cette classe a la particularité de pouvoir lancer le connecteur RMI pour
* l'accès distant même s'il n'a pas été initié dans la ligne de commande.
* Il reste toutefois fonctionnel dans le cas contraire.
*/
public class JMXServerStarter {
 private int port;
 private MBeanServer serveur;
 private JMXConnectorServer connection;
 // URL de connexion en prenant en compte le port.
 private final static String CONNEXION_URL = "service:jmx:rmi:///jndi/rmi://localhost:%d/jmxrmi";
 private String connecturl;

 public JMXServerStarter(int port) {
  this.port = port;
  this.connecturl = String.format(CONNEXION_URL, this.port);
 }

 public MBeanServer getServeur() throws IOException {
  if (this.serveur == null){
   startConnection();
  }
  return this.serveur;
 }


 private void startConnection() throws IOException {
  this.serveur = ManagementFactory.getPlatformMBeanServer();// Création du serveur.
  try {
   LocateRegistry.createRegistry(this.port); // La création du registre RMI 
   JMXServiceURL url = new JMXServiceURL(this.connecturl);
   // On crée le connecteur et on "démarre" le serveur.
   connection = JMXConnectorServerFactory
     .newJMXConnectorServer(url, null, this.serveur);
   connection.start();
   //Enregistrement du serveur si aucune erreur ne se produit.
  } catch (ExportException ex){}  // Si la création échoue, souvent le registre est déjà lancé.
 }
 
 /**
  * Le serveur doit être arrêté pour l'application accepte de se quitter
  * @throws IOException 
  */
 public void killServer() throws IOException{
  this.serveur = null;
  if(this.connection != null) this.connection.stop();
 }

}
Dans une classe à part on lance l'application avec le serveur :
package fr.technorevue.jmx;
import java.io.IOException;

import javax.management.MBeanServer;

public class Application {
 public static void main(String[] args) {
  JMXServerStarter starter = new JMXServerStarter(9876); // On démarre sur
                // le port 9876.
  try {
   MBeanServer serveur = starter.getServeur();
   System.out.println("Serveur démarré sur le port 9876");
   // Ici on réalisera les inscriptions de beans.
  } catch (IOException ioe) {
   System.err
     .println("Echec de l'obtention du serveur de beans. Connexion impossible "
       + ioe.getLocalizedMessage());
   ioe.printStackTrace();
  }
  // Ici on a le code de l'application, remplacé par un sleep de 10
  // secondes.
  try {
   Thread.sleep(10000);
  } catch (InterruptedException e) {
  }
  System.out.println("Sortie de l'application");
  try {
   starter.killServer();
  } catch (IOException ioe) {
   System.err.println("Erreur lors de l'arrêt sur serveur, était-il lancé? : "+ioe.getLocalizedMessage());
   ioe.printStackTrace();
  }
 }
}
La classe de démarrage du serveur est prévue pour lancer le registre RMI s'il n'a pas été lancé dans la ligne de commande. Pour une meilleure gestion de la sécurité Java, le mieux reste d'initier ce genre de paramètres en utilisant la ligne de commande.

Les MBeans, écriture et enregistrement

Maintenant que nous pouvons interagir avec notre serveur, nous allons écrire nos propres objets manageables et les enregistrer dans le serveur.
Cette opération nous permettra plus tard de gérer à distance nos objets enregistrés.

Le MBean est un objet répondant à un standard, le type du Bean géré par le serveur est déterminé par son nom.
Les beans qui nous intéressent ici sont les MXBeans, leur nom se terminera donc par MXBEan. Ils ont la particularité de fonctionner avec des données "Ouvertes" qui sont des containers génériques de données, on retrouve les types simples, les tableaux et les Maps (en gros). Ce point a été abordé dans la deuxième partie :
Java : Gérer la JVM avec Java Management Extensions (JMX) Technology - Partie 2 - Création d'un client
Pour reconnaître le type du Bean, c'est toujours la convention de nommage qui détermine le type du Bean, ainsi pas besoin d'héritage tout est dans le nom.
package fr.technorevue.jmx;

import java.util.List;
/**
 * Interface complete permettant de voir le fonctionnement de l'enregistrement JMX.
 * L'exemple ci-dessous affiche les méthodes permettant de gérer un serveur d'application.
 * @author Fabien TORDI
 */
public interface ServerManagerMXBean {
    //La constante de nommage du Bean dans le serveur.
 //Il s'appellera ServerManager et apparaîtra dans le domaine fr.technorevue.
 //Dans JConsole c'est affiché comme un dossier.
    public final static String SERVER_MANAGER_MXBEAN_NAME="fr.technorevue:name=ServerManager";
    /**
     * Démarre le serveur.
     */
    public void start();
    /**
     * Redémarre le serveur.
     */
    public void restart();
    /**
     * Arrête le serveur.
     */
    public void stop();
    
    /**
     * Donne le status du serveur.
     * @return Le status
     */
    public String getStatus();
    
    /**
     * Liste les applications du serveur.
     * @return
     */
    public List<String> getApplications();
    
    /**
     * Retourne la liste des applications du serveur, chargées ou non.
     * @param loaded si true retourne les applications chargées, si false les applications non chargées.
     * @return La liste des applications.
     */
    public List<String> listApplications(boolean loaded);

    /**
     * Charge l'application portant le nom passé en paramètres
     * @param application L'application à charger.
     * @return true si l'application est chargée, false sinon.
     */
    public boolean deploy(String application);
    
    
    /**
     * Décharge l'application portant le nom passé en paramètres
     * @param application L'application à charger.
     * @return true si l'application est déchargée, false sinon.
     */
    public boolean undeploy(String application);
    
    /**
     * Modifie le port d'écoute du serveur.
     * @param port Le port d'écoute du serveur.
     */
    public void setPort(int port);
    
    /**
     * Donne le port d'écoute du serveur.
     * @return port Le port d'écoute du serveur.
     */
    public int getPort();
}
Ce code est une interface, permettant d'enregistrer des attributs ainsi que des opérations exécutables par le biais de JMX. Les méthode commençant par get et par set se retrouvent dans les Attributes sans le get ni le set. Si un setter existe, l'attribut sera identifié comme inscriptible. Voici une implémentation sommaire de notre interface.
package fr.technorevue.jmx;

import java.util.ArrayList;
import java.util.List;

/**
 * Implémentation du server manager.
 * @author Fabien TORDI 
 */
public class ServerManager implements ServerManagerMXBean {
 private int port = 12345;
 /*
  * (non-Javadoc)
  * @see fr.technorevue.jmx.ServerManagerMXBean#start()
  */
 @Override
 public void start() {
  System.out.println("Démarrage");
  //Start code (ne nous concerne pas)
 }

 /*
  * (non-Javadoc)
  * @see fr.technorevue.jmx.ServerManagerMXBean#restart()
  */
 @Override
 public void restart() {
  System.out.println("Redémarrage");
  stop();
  start();
 }

 /*
  * (non-Javadoc)
  * @see fr.technorevue.jmx.ServerManagerMXBean#stop()
  */
 @Override
 public void stop() {
  System.out.println("On s'arrête");
  //Stop code...
 }

 /*
  * (non-Javadoc)
  * @see fr.technorevue.jmx.ServerManagerMXBean#getStatus()
  */
 @Override
 public String getStatus() {
  return "Fonctionne.";
 }
 
 /*
  * (non-Javadoc)
  * @see fr.technorevue.jmx.ServerManagerMXBean#listApplications()
  */
 @Override
 public List<String> getApplications() {
  System.out.println("Listing de toutes les applications .");
  List<String> apps = new ArrayList<String>();
  apps.add("App1");
  apps.add("App2");
  apps.add("App3");
  return apps;
 }

 /*
  * (non-Javadoc)
  * @see fr.technorevue.jmx.ServerManagerMXBean#listApplications(boolean)
  */
 @Override
 public List<String> listApplications(boolean loaded) {
  System.out.println("Listing de toutes applications chargées ? "+loaded);
  List<String> apps = new ArrayList<String>();
  if(loaded){
   apps.add("App1");
  }else{
   apps.add("App2");
   apps.add("App3");
  }
  return apps;
 }

 /*
  * (non-Javadoc)
  * @see fr.technorevue.jmx.ServerManagerMXBean#deploy(java.lang.String)
  */
 @Override
 public boolean deploy(String application) {
  System.out.println("Déploiement de l'application "+application);
  return true;
 }

 @Override
 public boolean undeploy(String application) {
  System.out.println("Arrêt de l'application "+application);
  return true;
 }

 @Override
 public void setPort(int port) {
  System.out.println("On redéfini le port d'écoute du serveur sur : "+port);
  this.port = port;
 }
 
 @Override
 public int getPort(){
  return this.port;
 }
}
On enregistre notre MBean dans notre serveur, le code suivant est la mise à jour du point d'entrée de notre application :
package fr.technorevue.jmx;
import java.io.IOException;

import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;

public class Application {
 public static void main(String[] args) {
  JMXServerStarter starter = new JMXServerStarter(9876); // On démarre sur
                // le port 9876.
  try {
   MBeanServer serveur = starter.getServeur();
   System.out.println("Serveur démarré sur le port 9876");
// Ici on enregistre notre Bean dans le serveur, avec un ObjectName pour le définir.
   serveur.registerMBean(new ServerManager(), new ObjectName(ServerManagerMXBean.SERVER_MANAGER_MXBEAN_NAME));
                         //Après toutes les erreurs que le serveur est susceptible de renvoyer lors de l'enregistrement...
  } catch (IOException ioe) {
   System.err
     .println("Echec de l'obtention du serveur de beans. Connexion impossible "
       + ioe.getLocalizedMessage());
   ioe.printStackTrace();
  } catch (InstanceAlreadyExistsException e) {
   System.err.println("Un bean existe déjà avec ce nom");
   e.printStackTrace();
  } catch (MBeanRegistrationException e) {
   System.err.println("Impossible d'enregistrer ce bean\n"+e.getLocalizedMessage());
   e.printStackTrace();
  } catch (NotCompliantMBeanException e) {
   System.err.println("MBean non correct\n"+e.getLocalizedMessage());
   e.printStackTrace();
  } catch (MalformedObjectNameException e) {
   System.err.println("Le nom du MBean est mauvais\n"+e.getLocalizedMessage());
   e.printStackTrace();
  } catch (NullPointerException e) {
   System.err.println("Tentative d'enregistrement d'un MBean null\n"+e.getLocalizedMessage());
   e.printStackTrace();
  }
  // Ici on a le code de l'application, remplacé par un sleep de 10
  // secondes.
  try {
   Thread.sleep(120000);
  } catch (InterruptedException e) {
  }
  System.out.println("Sortie de l'application");
  try {
   starter.killServer();
  } catch (IOException ioe) {
   System.err.println("Erreur lors de l'arrêt sur serveur, était-il lancé? : "+ioe.getLocalizedMessage());
   ioe.printStackTrace();
  }
 }
}
Voici un exemple de ce qui est visible dans JConsole lors de la connexion au serveur JMX de l'application, notre Bean apparaît, les getters et les setters sont regroupés dans les attributs et les opérations moins standard se retrouvent dans Opérations :


Exemple du résultat de l'appel à l'opération listApplications : 

Les notifications

Pour les notifications, il n'y a pas de méthode magique comme pour la decouverte des méthodes d'un Bean, l'objet qui veut envoyer des notifications, il doit soit implémenter NotificationEmitter soit étendre de NotificationBroadcasterSupport. Dans tous les cas, il doit créer un objet Notification et appeler NotificationBroadcasterSupport.sendNotification
Je n'ai pas creusé les notifications aussi bien que le reste alors je vais vous livrer un exemple tiré du site officiel de Java, adapté au code d'exemple. Dans notre code, on va envoyer une notification à chaque fois que quelqu'un modifie le port de l'application. On peut aisément imaginer un listener de notification, dès qu'il reçoit une information comme quoi le port a changé, il redémarre le serveur pour qu'il puisse prendre ne compte la modification. Nous n'irons pas jusque là. Tout d'abord le lien vers ma source : Lesson: Notifications (The Java™ Tutorials > Java Management Extensions (JMX))

D'ailleurs pour cette suite d'article je me suis beaucoup appuyé sur la documentation officielle pour comprendre le fonctionnement de JMX. Voici les changements apportés au bean pour lui permettre d'envoyer des notifications :
public class ServerManager extends NotificationBroadcasterSupport implements ServerManagerMXBean {
 private int port = 12345;
 private long sequenceNumber = 0; //Le sequence Number permet de gérer l'ordre d'envoi des notifications.
 ...
        //Ici la méthode passe synchronized, on ne sait jamais.
 @Override
 public synchronized void setPort(int port) {
  if(port != this.port){
   System.out.println("On redéfini le port d'écoute du serveur sur : "+port);
   Notification n = new AttributeChangeNotification(this,
     sequenceNumber++, System.currentTimeMillis(),
                    "Le port à changé", "Port", "int",
                    this.port, port); //La notification...
   this.port = port;
sendNotification(n);
  }
 }

 // On met à jour les informations de notification.
 @Override
 public MBeanNotificationInfo[] getNotificationInfo() {
  String[] types = new String[] { AttributeChangeNotification.ATTRIBUTE_CHANGE };

  String name = AttributeChangeNotification.class.getName();
  String description = "Un attribut de ce MBean à changé";
  MBeanNotificationInfo info = new MBeanNotificationInfo(types, name,
    description);
  return new MBeanNotificationInfo[] { info };
 }
}
Voilà les notifications sont prêtes pour notre Bean, il ne reste plus qu'à voir si ça fonctionne, on relance la JConsole comme toujours. Un champ Notifications supplémentaire apparaît sous notre Bean. En cliquant dessus on peut s'abonner à celui-ci par le bouton Subscribe c'est ce qui a été fait ci-dessous :

Conclusion

Tout au long de ces articles nous avons vu la puissance de JMX, un outil permettant de faire de la gestion/supervision de machine virtuelle Java simplement.
Nous avons vu comment surveiller l'utilisation des ressources JVM, en local ou a distance grâce à JConsole, je vous conseille également JVisualVM qui est plus développée et qui est livrée avec Java 7.
Nous avons également vu comment écrire un client permettant d'interroger les informations de la JVM exposées par JMX et par ce biais nous avons appris à lire toutes les informations mises à dispositions par JMX.
Dans cet article nous avons vu comment exposer nous même des informations en passant JMX et comment écrire des méthodes de gestion distantes d'une application. Le redémarrage d'un serveur d'application est un exemple parmi toutes les possibilités. La plupart du temps ce sont des fonctionnalités d'audits qui sont activées à distance.
Ce que nous n'avons pas vu et qui fera peut être l'objet d'un quatrième article et tout ce qui concerne la sécurité, la gestion de l'authentification et le problème de timeout des connexions qui n'est pas réglable. J'ai trouvé un excellent exemple sur le sujet que je n'ai pas encore partagé... L'occasion de tout traiter dans le prochain volet.