Publié par

Il y a 7 années -

Temps de lecture 7 minutes

Legacy code – gestion des exceptions avec JPDA

Il y a quelques jours un ami a sollicité mon aide pour résoudre un problème peu commun. Il avait comme tâche de lever des alertes sur les exceptions levées par une application dont il ne peut modifier le code. L’application utilise mal l’API de log et ne loggue pas (toutes) les exceptions. De plus, elle est peu documentée et le peu qui existe est obsolète.

A chaque exception qui survient pendant l’exécution de l’application, il doit envoyer un mail d’alerte.

L’application est un programme JAVA standalone lancé via un script shell. Ci après les solutions auxquelles j’ai pensé :

  1. Utiliser JPDA (Java Platform Debugger Architecture).
  2. Utiliser l’api instrumentation de Java.
  3. Utiliser de l’AOP.

Je vous propose dans cette série de trois articles de développer chacune de ces solutions.

Dans cette première partie, je vais explorer JPDA. Je commencerais par une brève introduction, avant de présenter la résolution du problème.

JPDA

Java Platform Debugger Architecture (JPDA) est un ensemble d’API créé dans le but de permettre de déboguer des programmes JAVA. Elle définit le cadre dans lequel un développeur débogue une JVM en utilisant un outil de débogage.  Le débogueur crée des points d’arrêt. La JVM débogguée arrête son exécution lors de la rencontre d’un point d’arrêt et notifie le déboggeur. JPDA peut être considérée comme une sorte de communication inter-processus. En effet, elle permet à deux JMV (déboguée et débogueur) d’échanger des informations. Plus, il permet à la JVM source (celle du débogueur) de commander et contrôler l’exécution de la JVM cible (déboguée).

Cette architecture repose sur deux API et un protocole :

  • Java Virtual Machine Tools Interface (JVMTI) : une API native que la JVM doit implémenter pour fournir ledit « mode débug ». Elle est utilisée aussi par les éditeurs voulant créer des outils de débogage, profiling ou monitoring. Introduite depuis JavaSE 5.0, elle remplace Java Virtual Machine Debug Interface (JVMDI) et Java Virtual Machine Profiler Interface (JVMPI).
  • Le protocole Java Debug Wire Protocol (JDWP) : un protocole binaire qui formalise les échanges entre le programme débogueur et le programme débogué.
  • Java Debug Interface (JDI) : API de haut niveau écrite en Java, à utiliser par l’outil de débogage. Les IDE comme Eclipse et IntelliJ IDEA utilisent cette API.

Pour notre problème, c’est l’API JDI qui nous intéresse. En effet, nous allons l’utiliser pour créer notre débogueur.

Voici le code que nous allons déboguer :

package fr.xebia.jpda;
public class LegacyCode {
    public static void main(String[] args) {
        try {
   throw new RuntimeException("Test of a caughted exception");  
 } catch (Exception ex) {
  ex.printStackTrace();
  throw new IllegalStateException("Test of an uncaughted exception",ex);
 }
   } 
}

Le seul but de ce bout de code est de simuler un programme qui lance de temps en temps des exceptions qu’il catche et traite, et d’autres qu’il ne catche pas.

La solution consiste à attacher cette JVM à un débogueur que nous allons créer. Le débogueur crée un point d’arrêt sur toutes les exceptions lancées (comme vous le faites sur votre IDE préféré).

Le débogueur est alors notifié sur les exceptions. Pour chaque exception, le débogueur demandera à la JVM cible d’écrire la stacktrace dans un fichier qui pourra être envoyé par mail.  

Connexion à la JVM

Les échanges d’information entre les JVM se basent sur le protocole JDWP. Ce protocole présente deux types de transport:

  1. le transport par socket: permet de déboguer une JVM à distance. La JVM cible peut être sur la même machine que le débogueur comme elle peut être sur une autre machine. Lors du lancement de la JVM cible il faut spécifier le port du débogage.
  2. le transport par mémoire partagée: permet de déboguer une JVM locale en connaissant son ID.

Pour pouvoir établir une connexion entre les deux JVM, ,il faut que la machine virtuelle à déboguer soit lancée en mode Debug.

Pour ça, il faut rajouter une des lignes suivantes au script de démarrage de la JVM cible:

  • -Xdebug -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n : mise en place d’un connecteur de type socket qui écoute sur le port 8000
  • -agentlib:jdwp:transport=dt_socket,address=8000,server=y,suspend=n à partir de JavaSe 5 (la première manière est toujours utilisée)
  • -Xdebug -Xrunjdwp:transport=dt_shmem,server=y,suspend=y : mise en place d’un connecteur de type mémoire partagée (il faut alors connaitre le PID du processus cible).
  • -Xagentlib:transport=dt_shmem,server=y,suspend=y.

La connexion à la JVM cible se fait comme suit:

public static VirtualMachine connect(String port) throws IOException, IllegalConnectorArgumentsException {
        AttachingConnector connector = null;
        VirtualMachineManager vmManager = Bootstrap.virtualMachineManager();
        for (Connector aconnector : vmManager.allConnectors()) {
            if ("com.sun.jdi.SocketAttach".equals(aconnector.name())) {
  connector = (AttachingConnector) aconnector;
  break;
     }
 }
 Map<String, Connector.Argument> args = connector.defaultArguments();
        Connector.Argument pidArgument = args.get("port");
        pidArgument.setValue(port);
        return connector.attach(args);
 }

Cette méthode prend en argument le numéro de port du débogue de la JVM cible (je suppose que les deux JVM tournent sur la même machine).

Elle retourne un Objet VirtualMachine qui référence la JVM cible, ou plutôt un miroir (Mirror) de la JVM cible.

Création du point d’arrêt

Une fois nous avons attaché la JVM cible, nous allons créer un point d’arrêt. Celui ci est lié à la levée des exceptions « catchées » et « non catchées ». Nous supposons que nous voulons être alertés pour toutes les exceptions.

public static void createExceptionBreakPoint(VirtualMachine vm) {
        EventRequestManager erm = vm.eventRequestManager();
        List<ReferenceType> referenceTypes = vm.classesByName("java.lang.Throwable");
        for (ReferenceType refType : referenceTypes){
  ExceptionRequest exceptionRequest = erm.createExceptionRequest(refType, true, true);
  exceptionRequest.setEnabled(true);
 }
}

Traitement des Exceptions

Maintenant que le point d’arrêt est créé et activé, nous pouvons commencer le traitement des notifications de débogage. Ceci se fait dans une boucle. Nous nous intéressons uniquement aux événements de type ExceptionEvent.

Notre idée principale est de faire en sorte que l’exception catchée soit loguée dans un fichier particulier. On peut penser à utiliser printStackTrace(PrintStream ps) ou printStackTrace(PrintWriter pw). Mais dans le programme débogueur nous n’avons qu’une image « miroir » de la vraie exception lancée dans la JVM cible. Nous pouvons alors faire une invocation distante de la méthode. Ces méthodes prennent en argument chacune un objet. Ceci dit, il nous faut disposer de cet objet dans la JVM cible. Heureusement, il nous est possible de créer des objets dans la JVM cible avec JDI.

Donc si nous considérons l’utilisation de printStackTrace(PrintStream ps), nous commençons par créer une chaine de caractère représentant le nom de fichier où nous voulons loguer l’exception, avant de créer un objet PrintStream. Enfin nous allons invoquer la méthode printStackTrace(PrintStream ps) sur l’exception catchée.

Une fois l’exception loguée, nous pouvant envoyer ce fichier comme pièce jointe d’un mail (l’envoi ne fait pas partie de l’exemple ci-dessous). Dans cet exemple nous nous contentons d’afficher son contenu.

public static void handleExceptionEvent(ExceptionEvent exceptionEvent) throws Exception {
        ObjectReference remoteException = exceptionEvent.exception();
        ThreadReference thread = exceptionEvent.thread();
        List<Value> paramList = new ArrayList<Value>(1);
        paramList.add(dumpFileName);
        //crer un printStream dans la JVM cible
        ObjectReference printStreamRef = printStreamClassType.newInstance(thread, printStreamConstructor, paramList,
                ObjectReference.INVOKE_SINGLE_THREADED);
        ReferenceType remoteType = remoteException.referenceType();
        Method printStackTrace = (Method) remoteType.methodsByName("printStackTrace").get(1);
        paramList.clear();
        paramList.add(printStreamRef);
        remoteException.invokeMethod(thread, printStackTrace, paramList, ObjectReference.INVOKE_SINGLE_THREADED);
        Scanner scanner = new Scanner(new File(dumpFileName.value()));
        while (scanner.hasNextLine()){
  System.out.println(scanner.nextLine());
 }
}

Conclusion

JPDA reste une API peu connue des développeurs Java et ceci malgré les possibilités qu’elle offre au programmeur. On peut lui reprocher l’overhead – dû surtout à la communication inter-processus – qu’elle induit dans l’application. Outre la complexité de son utilisation, beaucoup d’instructions doivent être écrites pour accomplir une « simple » tâche. Dans le prochain article nous verrons comment accomplir la même chose en utilisant l’API Instrumentation de Java. 

Publié par

Commentaire

3 réponses pour " Legacy code – gestion des exceptions avec JPDA "

  1. Publié par , Il y a 7 années

    Intéressant.
    Cela dit j’aurais plutôt utiliser l’API d’instrumentation en premier, elle me semble nettement plus adaptée que l’outillage JPDA.

    L’instrumentation permet de redéfinir les classes voulues un seules fois sans y revenir. Ce qui permet de ne pas impacter les performances de la JVM cible comme avec un break point en mode debug. Et en plus pas d’overhead / attentes au niveau de la communication inter-process (pour chaque levée d’exception).

  2. Publié par , Il y a 7 années

    Salut,

    Je m’étais aussi posé la question il y a quelques temps car la question ne se pose pas uniquement pour du legacy code malheureusement… dans les 2 cas la faute aux checked exceptions sans doute…

    Ça doit pas être trop cool à utiliser en production mais c’est vrai que je n’avais pas pensé à attacher un débugger de cette manière :)

    Je n’ai pas trouvé d’outil opensource pour faire cela simplement, par contre il semble que des produits payants proposent ce genre de fonctionnalités clé en main (Dynatrace, Appsight), ça serait cool de savoir comment ils fonctionnent.

  3. Publié par , Il y a 7 années

    Le fait de devoir démarrer la jvm en mode debug, chose qui est totalement interdite en prod, vide un peu cette méthode de son intérêt. L’AOP reste une meilleure alternative.
    D’ailleurs, je l’ai utilisé récemment sur un projet parce que je soupçonnais que le code applicatif modifiait le timeout de session fixé dans le descripteur de déploiement…
    Sinon Dynatrace et compagnie, se basent sur les agents qu’on démarre avec la jvm, ils ont donc un accès complet à tout ce qui se passe dans cette jvm, d’où la qualité des données qu’ils permettent d’exploiter…

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.