La consignation

Chapitres traités   

Tous les programmeurs Java connaissent bien le processus d'insertion des appels à System.out.println() dans un code qui pose des problèmes, pour se faire une idée du comportement du programme. Bien sûr, lorsque vous aurez découvert la cause du problème, les instructions d'affichage s'effacerons ; malgré tout, elles réapparaîtrons au prochain problème. L'API de consignation vise à surmonter cela, et comme son nom l'indique, elle propose de mettre en place plusieurs journaux (envoyés sur la console, dans un fichier, etc.) où l'ensemble des problèmes sont consignés suivant une certaine organisation.

 

Cette étude fait suite à la gestion d'exception qu'il faut bien maîtriser afin de bien comprendre le rôle de la consignation. Retour à la gestion d'exception.
§

Choix du chapitre Avantages de la consignation

Ses principaux avantages sont les suivants :

  1. Il est facile de supprimer tous les enregistrements du journal ou seulement ceux apparaissant au-delà d'un certain niveau, il est aussi simple de les retrouver.
  2. Les journaux supprimés sont très légers, la pénalité est donc mince à les conserver dans le code de consignation de votre application.
  3. Les enregistrements de journaux peuvent être dirigés vers différents gestionnaires, afin d'être affichés sur la console, stockés dans un fichier, etc.
  4. Le système de consignation et de gestion sont en mesure de filtrer les enregistrements. Selon les critères de leur créateurs, les filtres rejettent les entrées de journal inintéressantes.
  5. Les enregistrements de journaux peuvent être mis en forme de diverses manières, par exemple en texte brut ou en XML.
  6. Les applications peuvent faire appel à divers systèmes de consignation, utilisant des noms hiérarchiques comme org.manu, identiques aux noms de paquetages.
  7. Par défaut, la configuration de la consignation se contrôle grâce à un fichier de configuration. Les applications sont en mesure de remplacer ce mécanisme si nous le souhaitons.

 

Choix du chapitre Consignation de base

Commençons par le cas le plus simple. Le système de consignation gère un enregistreur par défaut, nommé java.util.logging.Logger.global, que vous pouvez utiliser à la place de System.out. Sa méthode info() permet de consigner un message informatif :

Logger.global.info("ATTENTION : Ce fichier -"+fichier.getName()+"- n'est pas une image");

Par défaut, l'enregistrement s'affiche comme suit :

12 août 2008 13:40:16 jwsphotos.PanneauImage setImage
INFO: ATTENTION : Ce fichier ( index.html ) n'est pas une image

Vous remarquez que l'heure et les noms de la classe et de la méthode d'appel apparaissent automatiquement.
§

Toutefois, si vous appelez la méthode setLevel() de l'objet Logger.global, avec la valeur Level.OFF passée en argument, à un endroit qui convient (par exemple au début de la méthode main()), l'ensemble de la consignation est supprimée.

Logger.global.setLevel(Level.OFF);

Exemple de mise en oeuvre

A titre d'exemple, je vous propose de mettre en place une application qui est une visionneuse des photos présentes sur la machine locale. Il est possible que la récupération de la photo se passe mal. Effectivement, l'utilisateur peut éventuellement choisir un fichier qui n'est pas une photo. Un journal est alors mis en oeuvre pour recenser les différents cas de disfonctionnement à l'administrateur de la machine locale.

Codage correspondant
package jwsphotos;

import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.util.logging.*;
import javax.imageio.ImageIO;
import javax.swing.*;

public class AfficherPhotos extends JFrame implements ActionListener {
   private JButton soumettre = new JButton("Récupérer la photo à afficher");
   private PanneauImage panneau = new PanneauImage();

   public AfficherPhotos() {
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setSize(500, 400);
      setTitle("Afficher photos");
      soumettre.addActionListener(this);
      add(soumettre, BorderLayout.NORTH);
      add(panneau);
      setVisible(true);
   }

   public static void main(String[] args) {
//      Logger.global.setLevel(Level.OFF);
      new AfficherPhotos();
   }

   public void actionPerformed(ActionEvent e) {
      JFileChooser fichier = new JFileChooser();
      fichier.setDialogTitle("Sélectionner la photo");
      fichier.showOpenDialog(this);
      panneau.setImage(fichier.getSelectedFile());
      panneau.repaint();
   }
}

class PanneauImage extends JPanel {
   private BufferedImage image;
   private double ratio;

   public void setImage(File fichier) {
      try {
         image = ImageIO.read(fichier);
         ratio = (double)image.getWidth()/image.getHeight();
      }
      catch (Exception ex) {
         Logger.global.info("ATTENTION : Ce fichier ( "+fichier.getName()+" ) n'est pas une image");
      }
   }

   @Override
   protected void paintComponent(Graphics surface) {
      if (image!=null) surface.drawImage(image, 0, 0, this.getWidth(), (int)(this.getWidth()/ratio), null);
   }
}

La consignation de base est maintenant considérée comme désuette. il est donc préférable de prévoir une consignation avancée que nous allons aborder maintenant.

 

Choix du chapitre Consignation avancée

Après cette introduction plus que sommaire, voyons à quoi ressemble la consignation pour les professionnels. Dans une application professionnelle, il est inutile d'enregistrer toutes les entrées d'un enregistreur global. Il vaut mieux définir quelques critères.

Création d'un enregistrement

Lorsque vous appelez pour la première fois un enregistreur ayant un nom donné, celui-ci se crée au moyen de la méthode statique getLogger() de la classe Logger :

Logger enregistrement = Logger.getLogger("org.manu.visionneuse");

Les appels ultérieurs à ce nom permettrons de recenser les problèmes éventuels sur ce même enregistrement.
§

Structure hiérarchique

A l'instar des noms de paquetage, les noms d'enregistreurs affichent une structure hiérarchique. Ils sont en fait plus hiérarchiques que les paquetages. En effet, il n'existe aucune relation sémantique entre un paquetage et son parent ; or, les enfants et parents des enregistreurs (logger) partagent certaines propriétés.

Par exemple, si vous définissez le niveau de consignation sur l'enregistreur "org.manu", ses enfants en hériteront.
§

Niveaux de consignation

Il existe sept niveaux de consignation qui correspondent à des constantes prédéfinies de la classe Level :

  1. SEVERE : consignation sur des erreurs sérieuses. Méthode équivalente severe().
  2. WARNING : consignation sur des messages d'avertissement. Méthode équivalente warning().
  3. INFO : consignation à titre informatif. Méthode équivalente info().
  4. CONFIG : consignation sur des configurations statiques. Méthode équivalente config().
  5. FINE : consignation pour une mise au point du programme. Méthode équivalente fine().
  6. FINER : même consignation que la précédente avec un peu plus de détail que la précédente. Méthode équivalente finer().
  7. FINEST : même consignation que la précédente avec énormément de détail. Méthode équivalente finest().
Par défaut, les trois niveaux supérieur sont consignés, mais il est facile de modifier ce comportement en proposant son niveau d'exigence au travers de la méthode setLevel() de la classe Logger. Si vous désirez que désormais, la consignation concerne tous les niveaux à partir de FINE :

enregistrement.setLevel(Level.FINE);

Vous pouvez également utiliser Level.ALL pour consigner tous les niveaux ou Level.OFF pour annuler toute consignation.
§

La classe Logger dispose également de méthodes de consignation pour tous les niveaux, comme :

enregistrement.warning(message);
enregistrement.fine(message);

Mais, vous pouvez aussi utiliser la méthode log() de la classe Logger, en indiquant le niveau de consignation désiré :

enregistrement.log(Level.FINE, message);

Astuce : La configuration par défaut permet de consigner tous les enregistrements ayant le niveau INFO ou supérieur. Vous devez donc utiliser les niveaux CONFIG, FINE, FINER et FINEST pour déboguer les messages insignifiants pour l'utilisateur du programme et pourtant utiles au diagnostic.

Attention : Si vous réglez le niveau de consignation sur une valeur plus précise que INFO, modifiez également la configuration du gestionnaire de l'enregistreur. Ce gestionnaire par défaut supprime les messages inférieurs au niveau INFO. Consultez le chapitre suivant pour en savoir plus.

Consignation en phase de mise au point de programme

La consignation par défaut précise le nom de la classe et de la méthode qui contient l'appel de consignation, comme cela est suggéré par la pile d'appel. Malgré tout, si la machine virtuelle optimise l'exécution, aucune information précise ne sera disponible.

  1. Vous pouvez alors utiliser la méthode logp() de la classe Logger. La signature de la méthode est la suivante :

    void logp(Level niveau, String classe, String méthode, String message)

  2. Il existe certaines méthodes bien utiles pour retracer le flux d'exécution pour connaître le moment où nous rentrons et où nous sortons d'une méthode. Ainsi nous permettrons une bonne mise au point du programme durant la phase de déboguage :

    void entering(String classe, String méthode)
    void
    entering(String classe, String méthode, Object[ ] paramètres)
    void exiting(String classe, String méthode)
    void
    exiting(String classe, String méthode, Object résultat)

    Ces appels génèrent des enregistrements de journaux du niveau FINER, qui commencent par les chaînes ENTRY et RETURN.
    §

  3. Codage correspondant pour consigner la méthode setImage() de la classe PanneauImage
    public void setImage(File fichier) {
        consignation.entering("PanneauImage", "setImage", new Object[] {fichier});
        ...
        consignation.exiting("PanneauImage", "setImage");
    }  
    Résultat correspondant lorsque nous choisissons une bonne photo à visualiser

    16 août 2008 07:39:23 PanneauImage setImage
    PLUS FIN: ENTRY C:\Photos\Papillon.jpg
    16 août 2008 07:39:24 PanneauImage setImage
    PLUS FIN: RETURN

    A l'avenir, les méthodes de consignation seront réécrites pour supporter les listes de paramètres variables "varargs". Il sera alors possible de réaliser des appels tels que :

    consignation.entering("PanneauImage", "setImage", fichier);


  4. Les journaux sont le plus souvent utiliser pour enregistrer les exceptions inattendues. Deux méthodes incluent une description de l'exception dans l'enregistrement :

    void throwing(String classe, String méthode, Throwable exception)
    void
    log(Level niveau, String message, Throwable exception)

    Les usages courants sont les suivants :
  5. if (...) {
      IOException exception = IOException(...);
      enregtistrement.throwing("PanneauImage", "setImage", exception);
      throw exception;
    }
    

    L'appel de la méthode throwing() génèrent un enregistrement du niveau FINER et un message commencent par la chaîne THROW.
    §

    et :

        try {
           ...
        }
        catch (Exception ex) {
           consignation.log(Level.WARNING,"Ce fichier ( "+fichier.getName()+" ) n'est pas une image", ex);
        }
    
    Codage correspondant pour consigner la méthode setImage() de la classe PanneauImage
    public void setImage(File fichier) {
        consignation.entering("PanneauImage", "setImage", new Object[] {fichier});
        try {
           image = ImageIO.read(fichier);
           ratio = (double)image.getWidth()/image.getHeight();
        }
        catch (Exception ex) {
           consignation.log(Level.WARNING,"Ce fichier ( "+fichier.getName()+" ) n'est pas une image", ex);
        }
        consignation.exiting("PanneauImage", "setImage");
    }  
    Résultat correspondant lorsque nous choisissons un autre fichier qu'un fichier image
    16 août 2008 08:01:37 PanneauImage setImage
    PLUS FIN: ENTRY H:\COURS linux\l'accès aux services.pdf
    16 août 2008 08:01:37 jwsphotos.AfficherPhotos$PanneauImage setImage ATTENTION: Ce fichier ( l'accès aux services.pdf ) n'est pas une image
    java.lang.NullPointerExceptionat jwsphotos.AfficherPhotos$PanneauImage.setImage(AfficherPhotos.java:51)
    at jwsphotos.AfficherPhotos.actionPerformed(AfficherPhotos.java:39)
    at javax.swing.AbstractButton.fireActionPerformed(AbstractButton.java:1995)
    at javax.swing.AbstractButton$Handler.actionPerformed(AbstractButton.java:2318)
    at javax.swing.DefaultButtonModel.fireActionPerformed(DefaultButtonModel.java:387)
    at javax.swing.DefaultButtonModel.setPressed(DefaultButtonModel.java:242)
    at javax.swing.plaf.basic.BasicButtonListener.mouseReleased(BasicButtonListener.java:236)
    at java.awt.Component.processMouseEvent(Component.java:6041)
    at javax.swing.JComponent.processMouseEvent(JComponent.java:3265)
    at java.awt.Component.processEvent(Component.java:5806)
    at java.awt.Container.processEvent(Container.java:2058)
    at java.awt.Component.dispatchEventImpl(Component.java:4413)
    at java.awt.Container.dispatchEventImpl(Container.java:2116)
    at java.awt.Component.dispatchEvent(Component.java:4243)
    at java.awt.LightweightDispatcher.retargetMouseEvent(Container.java:4322)
    at java.awt.LightweightDispatcher.processMouseEvent(Container.java:3986)
    at java.awt.LightweightDispatcher.dispatchEvent(Container.java:3916)
    at java.awt.Container.dispatchEventImpl(Container.java:2102)
    at java.awt.Window.dispatchEventImpl(Window.java:2440)
    at java.awt.Component.dispatchEvent(Component.java:4243)
    at java.awt.EventQueue.dispatchEvent(EventQueue.java:599)
    at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:273)
    at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:183)
    at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:173)
    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:168)
    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:160)
    at java.awt.EventDispatchThread.run(EventDispatchThread.java:121)

    16 août 2008 08:01:38 PanneauImage setImage
    PLUS FIN: RETURN
    Personnellement, je préfère autant utiliser directement la méthode warning() à la place de la méthode log() pour éviter le traçage de l'exception :

    Codage correspondant pour consigner la méthode setImage() de la classe PanneauImage
    public void setImage(File fichier) {
        consignation.entering("PanneauImage", "setImage", fichier);
        try {
           image = ImageIO.read(fichier);
           ratio = (double)image.getWidth()/image.getHeight();
        }
        catch (Exception ex) {
           consignation.warning("Ce fichier ( "+fichier.getName()+" ) n'est pas une image");
        }
        consignation.exiting("PanneauImage", "setImage");
    }  
    Résultat correspondant lorsque nous choisissons un autre fichier qu'un fichier image
    16 août 2008 08:01:37 PanneauImage setImage
    PLUS FIN: ENTRY H:\COURS linux\l'accès aux services.pdf
    16 août 2008 08:01:37 jwsphotos.AfficherPhotos$PanneauImage setImage ATTENTION: Ce fichier ( l'accès aux services.pdf ) n'est pas une image
    16 août 2008 08:01:38 PanneauImage setImage
    PLUS FIN: RETURN
    Ou alors, nous pouvons aussi prendre une des méthodes log() qui prend en compte un type Object en paramètre plutôt que le type Throwable.

    Codage correspondant pour consigner la méthode setImage() de la classe PanneauImage
    public void setImage(File fichier) {
        consignation.entering("PanneauImage", "setImage", fichier);
        try {
           image = ImageIO.read(fichier);
           ratio = (double)image.getWidth()/image.getHeight();
        }
        catch (Exception ex) {
           consignation.log(Level.WARNING, "Ce fichier ({0}) n'est pas une image", fichier.getName());
        }
        consignation.exiting("PanneauImage", "setImage");
    }  
    Résultat correspondant lorsque nous choisissons un autre fichier qu'un fichier image
    16 août 2008 08:01:37 PanneauImage setImage
    PLUS FIN: ENTRY H:\COURS linux\l'accès aux services.pdf
    16 août 2008 08:01:37 jwsphotos.AfficherPhotos$PanneauImage setImage ATTENTION: Ce fichier ( l'accès aux services.pdf ) n'est pas une image
    16 août 2008 08:01:38 PanneauImage setImage
    PLUS FIN: RETURN
  6. Attention, pour valider toutes ces fonctionnalités, je me suis servi de mon propre gestionnaire de consignation. Voir plus loin dans le chapitre correspondant.

La classe java.util.logging.Logger
Logger getLogger(String nom)
Logger getLogger(String nom, String ressources)
Récupèrent l'enregistreur ayant un nom donné. S'il n'existe pas il est créé.
- nom : Le nom hiérarchique de l'enregistreur comme org.manu.jwsphotos.
- ressources : Le nom du groupe de ressources permettant de rechercher les messages localisés.
void severe(String message)
void warning(String message)
void info(String message)
void config(String message)
void fine(String message)
void finer(String message)
void finest(String message)
Consignent un enregistrement avec le niveau indiqué par le nom de la méthode avec le message donné.
void entering(String classe, String méthode)
void entering(String classe, String méthode, Object paramètre)
void
entering(String classe, String méthode, Object[ ] paramètres)
void exiting(String classe, String méthode)
void
exiting(String classe, String méthode, Object résultat)
Consignent un enregistrement décrivant l'entrée ou la sortie d'une méthode avec le ou les paramètres donnés ou la valeur de retour.
void throwing(String classe, String méthode, Throwable exception)
Consigne un enregistrement décrivant le lancement de l'objet exception donné..
void log(Level niveau, String message)
void log(Level niveau, String message, Object objet)
void log(Level niveau, String message, Object[] objets)
void log(Level niveau, String message, Throwable exception)
Consignent un enregistrement avec le niveau donné et le message, en incluant en option des objets ou un Throwable. Pour inclure des objets, le message doit contenir des emplacements de mise en forme tels {0}, {1}, etc.
void logp(Level niveau, String classe, String méthode, String message)
void logp(Level niveau, String classe, String méthode, String message, Object objet)
void logp(Level niveau, String classe, String méthode, String message, Object[] objets)
void logp(Level niveau, String classe, String méthode, String message, Throwable exception)
Consignent un enregistrement avec le niveau donné, des informations précises sur l'appelant et un message. Incluent, en option, des objets ou un Throwable.
void logrb(Level niveau, String classe, String méthode, String ressources, String message)
void logrb(Level niveau, String classe, String méthode, String ressources, String message, Object objet)
void logrb(Level niveau, String classe, String méthode, String ressources, String message, Object[] objets)
void logrb(Level niveau, String classe, String méthode, String ressources, String message, Throwable exception)
Consignent un enregistrement avec le niveau donné, des informations précises sur l'appelant, le nom du groupe de ressources et le message. Consignent, en option, des objets ou un Throwable.
Level getLevel()
void setLevel(Level niveau)
Récupère ou définit le niveau de cet enregistreur.
Logger getParent()
void setParent(Logger enregistrement)
Récupère ou définit l'enregistrement parent de cet enregistreur.
Handler[] getHandlers()
Récupère tous les gestionnaires de cet enregistreur.
void addHandler(Handler gestionnaire)
void removeHandler(Handler gestionnaire)
Ajoute ou supprime un gestionnaire pour cet enregistreur.
boolean getUseParentHandlers()
void setUseParentHandlers(boolean utiliser)
Récupère ou définit la propriété "use parent handler" (utilise le gestionnaire parent). Si cette propriété est validée, l'enregistreur transmet tous les enregistrements consignés également aux gestionnaires de son parent.
Filter getFilter()
void setFilter(Filter filtre)
Récupère ou définit le filtre de cet enregistreur.

 

Choix du chapitre Modifier la configuration du gestionnaire de journaux

Diverses propriétés du système de consignation peuvent être modifiées par un changement apporté au fichier de configuration. Le fichier par défaut est situé à :

{JRE}/lib/logging.properties

Prendre un autre fichier de configuration (personnalisé)

Pour utiliser un autre fichier, faites passer la propriété java.util.logging.config.file sur l'emplacement du fichier en lançant votre application avec :

java -Djava.util.config.file=FichierDeConfiguration ClassePrincipale

Attention : appeler System.setProperty("java.util.logging.config.file", fichier) dans la méthode main() de votre classe principale n'est d'aucun effet car le gestionnaire de journaux est initialisé au démarrage de la machine virtuelle, avant l'exécution de la méthode main().

Modification dans le fichier de configuration

  1. Pour changer le niveau de consignation par défaut, modifiez la ligne suivante dans le fichier de configuration :

    .level=INFO

  2. Vous pouvez spécifier les niveaux de consignation de vos propres enregistreurs en ajoutant des lignes telles que :

    jwsphotos.level=FINER

    Il s'agit ici d'ajouter le suffixe .level au nom de l'enregistreur.
    §

    Les paramètres de configuration du gestionnaire de journaux ne sont pas des propriétés système. Lancer un programme par -Djwsphotos.level=FINER n'a aucune influence sur l'enregistreur.

  3. Comme nous le verrons plus loin dans cette étude, ce ne sont pas réellement les enregistreurs qui envoient un message à la console, c'est plutôt le rôle des gestionnaires. Ceux-ci possèdent aussi des niveaux. Pour afficher les messages de type FINER sur la console, vous devez également définir :

    java.util.logging.ConsoleHandler.level=FINER

  4. Exemple à suivre sur le fichier de configuration :


 

Choix du chapitre Les gestionnaires

Par défaut, les enregistreurs envoient leurs enregistrements à un ConsoleHandler qui les affiche dans le flux System.err. Plus précisément, l'enregistreur envoie l'enregistrement au gestionnaire parent, et le dernier ancêtre (avec pour nom " ") possède un ConsoleHandler.

De même les gestionnaires disposent d'un niveau de consignation. Pour qu'un enregistrement soit consigné, son niveau de consignation doit être supérieur au seuil de l'enregistrement et à celui du gestionnaire.

Le fichier de configuration du gestionnaire de consignation définit le niveau de consignation du gestionnaire de console par défaut comme suit :

java.util.logging.ConsoleHandler.level = INFO

Mise en place de son propre gestionnaire

Vous pouvez aussi contourner le fichier de configuration et installer votre propre gestionnaire :

Logger consignation = Logger.getLogger("jwsphotos");
consignation.setLevel(Level.FINER);
consignation.setUseParentHandlers(false);
Handler console = new ConsoleHandler();
console.setLevel(Level.FINER);
consignation.addHandler(console);

Par défaut, un enregistreur enregistre dans ses propres gestionnaires et dans ceux du parent. Ici, c'est un enfant du principal enregistreur (avec le nom " "), qui envoie à la console tous les enregistrements ayant le niveau FINER ou supérieur. mais ces enregistrements ne doivent pas apparaître deux fois. Nous définissons donc la propriété useParentHandlers sur false.

Exemple de mise en oeuvre

A titre d'exemple, je vous propose de reprendre la visionneuse des photos.

Codage correspondant
package jwsphotos;

import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.util.logging.*;
import javax.imageio.ImageIO;
import javax.swing.*;

public class AfficherPhotos extends JFrame implements ActionListener {
   private JButton soumettre = new JButton("Récupérer la photo à afficher");
   private PanneauImage panneau = new PanneauImage();
   private Logger consignation = Logger.getLogger("jwsphotos");
   private Handler console = new ConsoleHandler();

   public AfficherPhotos() {
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setSize(500, 400);
      setTitle("Afficher photos");
      soumettre.addActionListener(this);
      add(soumettre, BorderLayout.NORTH);
      add(panneau);
      setVisible(true);
      consignation.setLevel(Level.FINER);
      consignation.setUseParentHandlers(false);
      console.setLevel(Level.FINER);
      consignation.addHandler(console);
   }

   public static void main(String[] args) {
      new AfficherPhotos();
   }

   public void actionPerformed(ActionEvent e) {
      JFileChooser fichier = new JFileChooser();
      fichier.setDialogTitle("Sélectionner la photo");
      fichier.showOpenDialog(this);
      panneau.setImage(fichier.getSelectedFile());
      panneau.repaint();
   }

   class PanneauImage extends JPanel {
      private BufferedImage image;
      private double ratio;

      public void setImage(File fichier) {
         consignation.entering("PanneauImage", "setImage", new Object[] {fichier});
         try {
            image = ImageIO.read(fichier);
            ratio = (double)image.getWidth()/image.getHeight();
         }
         catch (Exception ex) {
            consignation.warning("Ce fichier ( "+fichier.getName()+" ) n'est pas une image");
         }
         consignation.exiting("PanneauImage", "setImage");
      }

      @Override
      protected void paintComponent(Graphics surface) {
         if (image!=null) surface.drawImage(image, 0, 0, this.getWidth(), (int)(this.getWidth()/ratio), null);
      }
   }
}

Choix du type d'enregistrement de journal

Pour envoyer des enregistrements de journal partout ailleurs, ajoutez un autre gestionnaire. L'API de consignation fournit deux gestionnaires qui se montreront fort utiles à cet effet :

  1. Un FileHandler : collecte les enregistrements dans un fichier au lieu de la console standard.
  2. Un SocketHandler : envoie les enregistrements vers un hôte à un port spécifique.

Gestionnaire de fichiers

Vous pouvez envoyer les enregistrements vers un gestionnaire de fichiers par défaut, comme ceci :

Logger consignation = Logger.getLogger("jwsphotos");
consignation.setLevel(Level.FINER);
consignation.setUseParentHandlers(false);
FileHandler journal = new FileHandler();
journal.setLevel(Level.FINER);
consignation.addHandler(journal);

Les enregistrements sont envoyés vers un fichier javan.log dans le répertoire de base de l'utilisateur, où n est remplacé par un nombre pour rendre le fichier unique.

Lorsqu'un système ne connaît pas le répertoire de base de l'utilisateur (par exemple sous Windows 95/98/Me), le fichier est stocké dans un emplacement par défaut comme C:\Windows.

Par défaut, les enregistrements sont mis en forme suivant une structure XML.
§

A titre d'exemple, je vous propose de reprendre la visionneuse des photos en enregistrant cette fois-ci les consignations dans un fichier journal :

Codage correspondant
package jwsphotos;

import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.util.logging.*;
import javax.imageio.ImageIO;
import javax.swing.*;

public class AfficherPhotos extends JFrame implements ActionListener {
   private JButton soumettre = new JButton("Récupérer la photo à afficher");
   private PanneauImage panneau = new PanneauImage();
   private Logger consignation = Logger.getLogger("jwsphotos");

   public AfficherPhotos() {
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setSize(500, 400);
      setTitle("Afficher photos");
      soumettre.addActionListener(this);
      add(soumettre, BorderLayout.NORTH);
      add(panneau);
      setVisible(true);
      consignation.setLevel(Level.FINER);
      consignation.setUseParentHandlers(false);
      FileHandler journal = new FileHandler();
 //     journal.setLevel(Level.FINER);
      consignation.addHandler(journal);
   }


...
}

Modifier le comportement par défaut

  1. Vous pouvez modifier le comportement par défaut du gestionnaire de fichiers en changeant ses paramètres de configuration comme le montre le tableau ci-dessous, ou en utilisant un autre constructeur que le constructeur par défaut.

    Propriété de configuration Description Paramètre par défaut
    java.util.logging.FileHandler.level Le niveau du gestionnaire. Level.ALL
    java.util.logging.FileHandler.append Contrôle si le gestionnaire doit continuer un fichier existant ou ouvrir un nouveau fichier pour chaque programme. false
    java.util.logging.FileHandler.limit Le nombre maximal approximatif d'octets à écrire dans un fichier avant d'en ouvrir un autre (0 = pas de limite). 0 (pas de limite) dans la classe FileHandler, 50 000 dans la configuration par défaut du gestionnaire de journaux.
    java.util.logging.FileHandler.pattern Le modèle de nom pour le fichier journal. Voir le tableau suivant pour connaître ses variables. %h/java%u.log
    java.util.logging.FileHandler.count Le nombre de journaux dans une suite. 1 (pas de rotation)
    java.util.logging.FileHandler.filter La classe du filtre à utiliser. Pas de filtrage
    java.util.logging.FileHandler.encoding Le codage de caractères à utiliser. Le codage de la plate-forme.
    java.util.logging.FileHandler.formatter Le formatteur des enregistrements. java.util.logging.XMLFormatter
  2. Il est souvent préférable de changer également le nom par défaut du fichier journal. Vous pourrez utiliser un modèle différent, par exemple %h/monapplication.log. Voir le tableau ci-dessous pour connaître les différentes variables des modèles à proposer :

    Variable Description
    %h La valeur de la propriété système user.home
    %t Le répertoire temporaire du système
    %u Un numéro unique pour éviter les conflits
    %g Le numéro pour les journaux par rotation (le suffixe .%g indique que la rotation est spécifiée, le modèle ne contient pas %g)
    %% Le caractère %
  3. Lorsque plusieurs applications (ou plusieurs copies d'une même application) utilisent le même fichier journal, il est recommandé d'activer la balise "append". Vous pouvez également insérer %u dans le modèle du nom de fichier, pour que chaque application crée sa propre copie de journal.
  4. Il est également conseillé d'activer la notaion des fichiers. Les fichiers journaux sont conservés suivant un modèle séquentiel tel que monappli.log.0, monappli.log.1, monappli.log.2, etc. Dès qu'un fichier dépasse le plafons déterminé, le plus vieux est supprimé, les autres osnt renommés et un nouveau fichier est créé avec le numéro 0.

    Astuce : De nombreux programmeurs utilisent la consignation pour aider le personnel du support technique. En cas de dérèglement d'un programme au cours d'une session, l'utilisateur peut renvoyer les fichiers journaux pour analyse. Dans ce cas, il convient d'activer la balise "append", d'utiliser la rotation des fichiers, voire de faire les deux.

    Exemple de mise en oeuvre

    A titre d'exemple, je vous propose simplement de proposer le nom du fichier journal jwsphotos.0.journal, avec un format de texte classique sans les balises XML :

    Codage correspondant
    ...
    
    public class AfficherPhotos extends JFrame implements ActionListener {
       private JButton soumettre = new JButton("Récupérer la photo à afficher");
       private PanneauImage panneau = new PanneauImage();
       private Logger consignation = Logger.getLogger("jwsphotos");
    
       public AfficherPhotos() {
          setDefaultCloseOperation(EXIT_ON_CLOSE);
          setSize(500, 400);
          setTitle("Afficher photos");
          soumettre.addActionListener(this);
          add(soumettre, BorderLayout.NORTH);
          add(panneau);
          setVisible(true);
          consignation.setLevel(Level.FINER);
          consignation.setUseParentHandlers(false);
          FileHandler journal = new FileHandler("%h/jwsphotos.%u.journal");
          journal.setFormatter(new SimpleFormatter());
          consignation.addHandler(journal);
       }
    ...
    }  
La classe java.util.logging.Handler
abstract void publish(LogRecord enregistrement)
Envoie l'enregistrement à la destination prévue.
abstract void flush()
Efface toute donnée mise en tampon.
abstract void close()
Efface toute donnée mise en tampon et libère les ressources associées.
Level getLevel()
void setLevel(Level niveau)
Récupère ou définit le niveau de ce gestionnaire.
Filter getFilter()
void setFilter(Filter filtre)
Récupère ou définit le filtre de ce gestionnaire.
Formatter getFormatter()
void setFormatter(Formatter formateur)
Récupère ou définit le formateur de ce gestionnaire.
La classe java.util.logging.ConsoleHandler qui hérite de Handler
ConsoleHandler()
Construisent un nouveau gestionnaire de console (erreur standard).
La classe java.util.logging.FileHandler qui hérite de Handler
FileHandler(String motif)
FileHandler(String motif, boolean ajout)
FileHandler(String motif, int limite, int nombre)
FileHandler(String motif, int limite, int nombre, boolean ajout)
Construisent un gestionnaire de fichiers.

- motif : Le motif pour connaître le nom du fichier journal.
- limite : Le nombre maximal approximatif d'octets avant qu'un nouveau fichier journal ne soit ouvert.
- nombre : Le nombre de fichiers dans une rotation.
- ajout : valider lorsqu'un gestionnaire de fichiers nouvellement construit doit continuer un fichier journal existant.

 

Choix du chapitre Les filtres

Par défaut, les enregistrements sont filtrés en fonction de leurs niveaux de consignation. Chaque enregistreur et chaque gestionnaire peuvent avoir un filtre optionnel qui réalisera un filtrage complémentaire.

Pour définir un filtre, vous devez implémenter l'interface Filter et redéfinir la méthode isLoggable() :
interface Filter {
   boolean isLoggable(LogRecord enregistrement);
}

Dans cette méthode isLoggable(), analysez l'enregistrement du journal à l'aide des critères souhaités et renvoyez true pour ceux que vous voulez inclure dans le journal.

Par exemple, un filtre particulier peut n'être intéressé que par les messages générés par les méthode entering() et exiting(). Le filtre doit alors appeler la méthode getMessage() de la classe LogRecord et vérifier s'il commence par ENTRY ou RETURN.

L'interface java.util.logging.Filter
boolean isLoggable(LogRecord enregistrement)
Renvoie true si l'enregistrement du journal donné doit être consigné.
La classe java.util.logging.LogRecord
Level getLevel()
Récupère le niveau de consignation de cet enregistrement.
String getLoggerName()
Récupère le nom de l'enregistreur qui consigne cet enregistrement.
ResourceBundle getResourceBundle()
String getResourceBundleName()
Récupèrent le groupe de ressources ou son nom pour l'utiliser pour localiser le message ou null si aucun n'est fourni.
String getMessage()
Récupère le message "brut" avant la localisation ou la mise en forme.
Object[] getParameters()
Récupère les objets paramètre ou null si aucun n'est fourni.
Throwable getThrown()
Récupère l'objet lancé ou null si aucun n'est fourni.
String getSourceClassName()
String getSourceMethodName()
Récupèrent l'emplacement du code qui a consigné cet enregistrement. Ces informations peuvent être fournies par le code de consignation ou automatiquement tirées de la pile d'exécution. Cela peut être inexact si le code de consignation a fourni la mauvaise valeur ous i le code d'exécution était optimisé et que l'emplacement exact ne puisse pas être extrait.
long getMillis()
Récupère l'heure de la création, en millièmes de seconde, depuis 1970.
long getSequenceNumber()
Récupère le numéro unique (dans la suite) de cet enregistrement.
long getThreadID()
Récupère l'ID unique pour le thread dans lequel cet enregistrement a été créé. Ces ID sont attribués par la classe LogRecord et n'ont pas de relation avec les autres ID de thread.

Pour installer un filtre dans un enregistreur ou un gestionnaire, appelez simplement la méthode setFilter(). Sachez que vous pouvez installer plusieurs filtres à la fois.


Codage de l'application précédente en ne prenant en compte que l'entrée et la sortie de méthode
package jwsphotos;

import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.util.logging.*;
import javax.imageio.ImageIO;
import javax.swing.*;

public class AfficherPhotos extends JFrame implements ActionListener {
   private JButton soumettre = new JButton("Récupérer la photo à afficher");
   private PanneauImage panneau = new PanneauImage();
   private Logger consignation = Logger.getLogger("jwsphotos");

   public AfficherPhotos() {
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setSize(500, 400);
      setTitle("Afficher photos");
      soumettre.addActionListener(this);
      add(soumettre, BorderLayout.NORTH);
      add(panneau);
      setVisible(true);
      consignation.setLevel(Level.FINER);
      consignation.setUseParentHandlers(false);
      Handler journal = new ConsoleHandler();
      journal.setLevel(Level.FINER);
      consignation.setFilter(panneau);
      consignation.addHandler(journal);     
   }

   public static void main(String[] args) {
      new AfficherPhotos();
   }

   public void actionPerformed(ActionEvent e) {
      JFileChooser fichier = new JFileChooser();
      fichier.setDialogTitle("Sélectionner la photo");
      fichier.showOpenDialog(this);
      panneau.setImage(fichier.getSelectedFile());
      panneau.repaint();
   }

   class PanneauImage extends JPanel implements Filter {
      private BufferedImage image;
      private double ratio;

      public void setImage(File fichier) {
         consignation.entering("PanneauImage", "setImage", fichier);
         try {
            image = ImageIO.read(fichier);
            ratio = (double)image.getWidth()/image.getHeight();
         }
         catch (Exception ex) {
            consignation.log(Level.WARNING, "Ce fichier ({0}) n'est pas une image", fichier.getName());
         }
         consignation.exiting("PanneauImage", "setImage");
      }

      @Override
      protected void paintComponent(Graphics surface) {
         if (image!=null) surface.drawImage(image, 0, 0, this.getWidth(), (int)(this.getWidth()/ratio), null);
      }

      public boolean isLoggable(LogRecord enregistrement) {
         String message = enregistrement.getMessage();
         return message.startsWith("ENTRY") || message.startsWith("RETURN");
      }
   }
}

 

Choix du chapitre Les formatteurs

Les classes ConsoleHandler et FileHandler délivrent des enregistrements des journaux respectivement aux formats texte et XML. Mais vous pouvez aussi définir vos propres formats.

  1. Pour cela prolongez la classe Formatter et surcharger la méthode format() :

    String format(LogRecord enregistrement)

  2. Vous pouvez mettre en forme les informations contenues dans l'enregistrement selon vos préférences et renvoyer la chaîne de résultat. Dans votre méthode format(), vous pouvez aussi appeler la méthode formatMessage(). Celle-ci met en forme le message qui fait partie de l'enregistrement, en remplaçant les paramètres et en appliquant la localisation :

    String format(LogRecord enregistrement)

  3. De nombreux formats de fichiers (comme XML) exignet un bloc de début et un bloc de fin pour entourer les enregistrement mis en forme. Dans ce cas surcharger les méthodes getHead() et getTail() :

    String getHead(Handler h)
    String getTail(Handler h)

  4. Enfin, applez la méthode setFormatter(), issue de la classe Handler, pour installer le formatter dans le gestionnaire.
  5. A noter qu'il existe des classes prédéfinies SimpleFormatter et XMLFormatter qui mettent en place des formats tout prêts, respectivement le format texte et le format XML. Ces classes sont d'ailleurs utilisées par les classes ConsoleHandler et FileHandler. Ainsi, si vous désirez obtenir un format standard sous forme de texte simple sans balises XML dans un fichier de consignation, il suffit d'exécuter la ligne suivante :

    journal
    .setFormatter(new SimpleFormatter())


La classe java.util.logging.Formatter
abstract String format(LogRecord enregistrement)
Renvoie la chaîne née de la mise en forme de l'enregistrement du journal.
String getHead(Handler h)
String getTail(Handler h)
Renvoient les chaînes qui doivent apparaître au début et à la fin du document contenant les enregistrements du journal. La superclasse Formatter définit ces méthodes de sorte qu'elles renvoient la chaîne vide ; surchargez-les ci-nécessaire.
String formatMessage(LogRecord enregistrement)
Renvoie la partie du message qui a été localisée et mise en forme.

Exemple de mise en oeuvre

Je vous invite à reprendre l'application précédente dans laquelle je propose de fabriquer un nouveau formatteur de messages de consignation. Voici par exemple ce que nous pouvons obtenir sur la console lorsque le choix du fichier à visualiser n'est pas une image :

<22 août 2008 - 15:48:34> Entrée dans la méthode PanneauImage.setImage( )
<22 août 2008 - 15:48:34> ATTENTION : Ce fichier (index.html) nest pas une image
<22 août 2008 - 15:48:34> Sortie de la méthode PanneauImage.setImage( )

Codage correspondant
package jwsphotos;

import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.text.*;
import java.util.Date;
import java.util.logging.*;
import javax.imageio.ImageIO;
import javax.swing.*;

public class AfficherPhotos extends JFrame implements ActionListener {
   private JButton soumettre = new JButton("Récupérer la photo à afficher");
   private PanneauImage panneau = new PanneauImage();
   private Logger consignation = Logger.getLogger("jwsphotos");

   public AfficherPhotos() {
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setSize(500, 400);
      setTitle("Afficher photos");
      soumettre.addActionListener(this);
      add(soumettre, BorderLayout.NORTH);
      add(panneau);
      setVisible(true);
      consignation.setLevel(Level.FINER);
      consignation.setUseParentHandlers(false);
      Handler journal = new ConsoleHandler();
      journal.setLevel(Level.FINER);
      journal.setFormatter(new Format());
      consignation.addHandler(journal);     
   }

   public static void main(String[] args) {
      new AfficherPhotos();
   }

   public void actionPerformed(ActionEvent e) {
      JFileChooser fichier = new JFileChooser();
      fichier.setDialogTitle("Sélectionner la photo");
      fichier.showOpenDialog(this);
      panneau.setImage(fichier.getSelectedFile());
      panneau.repaint();
   }

   class PanneauImage extends JPanel {
      private BufferedImage image;
      private double ratio;

      public void setImage(File fichier) {
         consignation.entering("PanneauImage", "setImage", fichier);
         try {
            image = ImageIO.read(fichier);
            ratio = (double)image.getWidth()/image.getHeight();
         }
         catch (Exception ex) {
            consignation.log(Level.WARNING, "Ce fichier ({0}) n'est pas une image", fichier.getName());
         }
         consignation.exiting("PanneauImage", "setImage");
      }

      @Override
      protected void paintComponent(Graphics surface) {
         if (image!=null) surface.drawImage(image, 0, 0, this.getWidth(), (int)(this.getWidth()/ratio), null);
      }
   }

   class Format extends Formatter {
      @Override
      public String format(LogRecord enregistrement) {
         String date = MessageFormat.format("<{0, date, long} - {0, time, medium}> ", new Date(enregistrement.getMillis()));
         if (enregistrement.getLevel() == Level.WARNING) {
            return date+"ATTENTION : "+formatMessage(enregistrement)+'\n';
         }
         else {
            String type = "";
            if (enregistrement.getMessage().startsWith("ENTRY")) type = "Entrée dans la méthode ";
            else if (enregistrement.getMessage().startsWith("RETURN")) type = "Sortie de la méthode ";
            String classe = enregistrement.getSourceClassName();
            String méthode = enregistrement.getSourceMethodName();
            return date+type+classe+'.'+méthode+"( )\n";
         }
      }
   }
}