dimanche 6 mai 2012

JMX à travers un firewall

Comme je l'ai déjà dit ici ( en anglais ) je suis un adepte de la technologie JMX. Mais je viens d'être confronté à un problème : la connection (en JMX) à une JVM cachée derrière un firewall. Après avoir un peu cherché, j'ai fini par trouver comment le solutionner  ; par contre, je n'ai pas trouvé de documentation en français. C'est pourquoi j'écris cet article : si vous aussi, vous avez des problèmes pour établir une connexion JMX à un serveur JAVA et cela au travers d'un firewall alors il pourra vous intéresser.

Description de la problématique

J'essaie de me connecter à un serveur écoutant sur le port JMX 1100 ; serveur séparé de mon poste client ( sur lequel je lance par exemple une jconsole ) par un firewall.
Pour forcer le port JMX j'ai rajouté les paramètres suivant à la ligne de commande de démarrage de mon serveur :
-Dcom.sun.management.jmxremote.port=1100
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
Du côté de mon firewall nous avons fait le nécessaire pour ouvrir le port TCP 1100. Et pourtant ma jconsole n'arrive pas à se connecter au serveur distant. Après analyse j'ai fini par comprendre qu'en fait d'autres ports sont mis en jeux.

Les explications

En fait le port 'jmx' n'est que celui recevant les connexions ; ensuite un autre est utilisé sur lequel sont exportés les objets (RMI) accédé via JMX. Cet autre port est en général alloué dynamiquement ; mais il est possible de le contrôler.

La solution

Elle consiste à créer une instance de la classe JMXConnectorServerFactory, et de l'associer au MBean server de l'application. Si vous ne voulez pas, ou si vous ne pouvez pas, créer un tel serveur dans le code de votre application ( par exemple si vous n'avez pas la main sur toutes les sections du code ; voir pas la main du tout ) vous pouvez le créer dans un agent enregistré au démarrage de la VM : ainsi vous allez modifier le fonctionnement de votre application sans toucher à son code.

Exemple d'agent

public class JmxDefaultAgent {
    
    
    public static MBeanServer getMBeanServer() {
        ArrayList list =MBeanServerFactory.findMBeanServer(null) ;
        return((list.size()==0)?ManagementFactory.getPlatformMBeanServer():list.get(0));
    }
    
    public static void premain(String agentArgs) throws IOException {
        int port = Integer.parseInt(System.getProperty("com.me.jmxport", "1100"));
        
        String hostname = System.getProperty("com.me.jmxhost", null);
        if (hostname==null) hostname = InetAddress.getLocalHost().getHostName();

        System.out.println("Create RMI registry for JMX on port " + port);
        LocateRegistry.createRegistry(port);

        
        MBeanServer mbs = getMBeanServer();

        // Environment map
        HashMap env = new HashMap();
        JMXServiceURL url = new JMXServiceURL("service:jmx:rmi://"+hostname+":" + port + "/jndi/rmi://" + hostname
                + ":" + port + "/jmxrmi");

        // Now create the server from the JMXServiceURL
        //
        JMXConnectorServer cs = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs);

        // Start the RMI connector server.
        //
        System.out.println("Start the RMI connector server on port " + port);
        cs.start();
    }
}

Si vous avez la main sur le code

Il n'est pas nécessaire d'utiliser un agent ; il vous suffit simplement de démarrer le serveur JMXConnectorServer lors du chargement de votre application.

Dernier problème

Le code précédent fonctionne parfaitement ; son seul problème est que le server JMXConnectorServer démarré dans l'agent va empêcher l'arrêt correct de la JVM : un tel serveur doit être stoppé via sa méthode stop. Autrement dit, ce serveur n'est pas démarré sous la forme d'un thread daemon qui à l'arrêt de la JVM nettoierait ses ressources et s'arrêterait automatiquement.

Si vous n'avez pas la main sur le code du serveur pour y rajouter l'appel à la méthode stop ; une méthode consiste à créer un thread dans l'agent vu précédemment, thread dont le rôle sera de détecter la mort imminente de l'application. Cette détection consiste à repérer qu'il n'existe plus de thread actif au sein de la JVM, en dehors des thread daemon et de ceux liés au serveur JMXConnectorServer ; et dans ce cas, on peut appeler la méthode stop du JMXConnectorServer de manière sure.

Exemple de thread détectant l'arrêt de l'application

public class Cleaner extends Thread {
    public Cleaner(JMXConnectorServer connectorServer) {
        super("ThreadCleaner");
        this.connectorServer = connectorServer;
    }

    @Override
    public void run() {
        boolean alive = true ;
        try {
            while( alive == true ) {
                Thread[] activeThreads = new Thread[Thread.activeCount()+10];
                int total = Thread.enumerate(activeThreads);
                try {
                    alive = false ;
                    for (int i=0;i<total;i++) {
                        Thread currentThread = activeThreads[i];
                        if (currentThread.getName().contains("RMI")) continue;
                        if (currentThread.equals(this)) continue;
                        if (currentThread.isDaemon()) continue;
                        else {
                            alive = true ;
                            currentThread.join();
                            break;
                        }
                    }
                    if (alive == false) {
                        System.out.println("no more thread remaining - stopping jmxconnectorserver");
                    }
                } catch(InterruptedException e) {
                    System.out.println("received interruption - stopping jmxconnectorserver");
                    alive = false ;
                }
            }
        } finally {
            try {
                connectorServer.stop();
            } catch (IOException e) {

            }
        }
    }

    private JMXConnectorServer connectorServer ;
}

Lien utiles

Aucun commentaire:

Enregistrer un commentaire