web-dev-qa-db-fra.com

Pourquoi appeler LoggerFactory.getLogger (...) à chaque fois n'est pas recommandé?

J'ai lu des tonnes de publications et de documents (sur ce site et ailleurs) indiquant que le modèle recommandé pour la journalisation SFL4J est:

public class MyClass {
    final static Logger logger = LoggerFactory.getLogger(MyClass.class);

    public void myMethod() {
        //do some stuff
        logger.debug("blah blah blah");
    }
}

Mon patron préfère que nous utilisions simplement un wrapper pour intercepter les appels de journal et éviter le code de plaque de chaudière pour déclarer l'enregistreur sur chaque classe:

public class MyLoggerWrapper {
    public static void debug(Class clazz, String msg){
        LoggerFactory.getLogger(clazz).debug(msg));
    }
}

et en l'utilisant simplement comme ceci:

public class MyClass {

    public void myMethod() {
        //do some stuff
        MyLoggerWrapper.debug(this.getClass(), "blah blah blah");
    }
}

Je présume que l'instanciation d'un enregistreur chaque fois que nous nous connectons coûte un peu cher, mais je n'ai trouvé aucun document appuyant cette hypothèse. De plus, il dit que le framework (LogBack ou Log4J, nous décidons encore) va sûrement "mettre en cache" les enregistreurs et aussi que dans tous les cas les serveurs fonctionnent bien en dessous de leur capacité donc ce n'est pas un problème.

Avez-vous de l'aide pour signaler des problèmes potentiels avec cette approche?

42
danirod

Voici un problème évident avec cette approche: les messages String seront construits à chaque appel à debug(), il n'y a aucun moyen évident d'utiliser une clause de garde avec votre wrapper.

L'idiome standard avec log4j/commons-logging/slf4j est d'utiliser une clause de garde telle que:

if (log.isDebugEnabled()) log.debug("blah blah blah");

Dans le but que si le niveau DEBUG n'est pas activé pour l'enregistreur, le compilateur peut éviter de concaténer ensemble des chaînes plus longues que vous pouvez lui envoyer:

if (log.isDebugEnabled()) log.debug("the result of method foo is " + bar 
     + ", and the length is " + blah.length());

Voir "Quel est le moyen le plus rapide de (pas) se connecter?" Dans le SLF4J ou log4j FAQ.

Je recommanderais contre le "wrapper" que votre patron suggère. Une bibliothèque comme slf4j ou commons-logging est déjà une façade autour de l'implémentation de journalisation sous-jacente utilisée. De plus, chaque invocation de l'enregistreur devient beaucoup plus longue - comparez ce qui précède avec

 MyLoggerWrapper.debug(Foo.class, "some message");

C'est le type de "wrapping" et d'obscurcissement triviaux et sans importance qui ne sert à rien d'autre que d'ajouter des couches d'indirection et de vilainement ficeler votre code. Je pense que votre patron peut trouver des problèmes plus importants à obséder.

29
matt b

Les objets de l'enregistreur sont sûrement réutilisés, donc aucune instanciation supplémentaire ne se produira de toute façon. Le plus gros problème que je vois est que vos informations de numéro de fichier/ligne seront inutiles, car l'enregistreur enregistrera toujours fidèlement que chaque message a été émis à partir de la classe LoggerWrapper, ligne 12 :

OTOH --- (SLF4J est déjà une façade wrapper pour masquer le cadre de journalisation spécifique utilisé, vous permettant de basculer librement entre les différentes implémentations de journalisation. Par conséquent, je ne vois absolument aucun intérêt à le cacher derrière un autre emballage.

12
Péter Török

Les appels répétés à LoggerFactory.getLogger(clazz) ne devraient pas entraîner à chaque fois un nouvel objet Logger. Mais cela ne signifie pas que les appels sont gratuits. Alors que le comportement réel dépend du système de journalisation derrière la façade, il est très probable que chaque getLogger entraîne une recherche dans une structure de données simultanée ou synchronisée1 pour rechercher une instance préexistante.

Si votre application effectue de nombreux appels à votre méthode MyLoggerWrapper.debug, Tout cela peut représenter un impact significatif sur les performances. Et dans une application multithread, il peut s'agir d'un goulot d'étranglement de concurrence.

D'autres problèmes mentionnés par d'autres réponses sont également importants:

  • Votre application ne peut plus utiliser logger.isDebugEnabled() pour minimiser les frais généraux lorsque le débogage est désactivé.
  • La classe MyLoggerWrapper masque les noms de classe et les numéros de ligne des appels de débogage de votre application.
  • Le code utilisant MyLoggerWrapper sera probablement plus détaillé si vous effectuez plusieurs appels de consignateur. Et la verbosité sera dans la zone où elle affecte le plus la lisibilité; c'est-à-dire dans les méthodes qui font des choses qui nécessitent une journalisation.

Enfin, ce n'est tout simplement "pas la façon dont cela se fait".


1 - Apparemment, c'est un Hashtable dans Logback et Log4j, et cela signifie que le potentiel d'un goulot d'étranglement de concurrence existe certainement. Notez que ce n'est pas une critique de ces cadres de journalisation. Au contraire, la méthode getLogger n'a pas été conçue/optimisée pour être utilisée de cette façon.

10
Stephen C

Pour ajouter aux raisons déjà mentionnées, la suggestion de votre patron est mauvaise car:

  • Cela vous oblige à taper à plusieurs reprises quelque chose qui n'a rien à voir avec la journalisation, chaque fois que vous souhaitez enregistrer quelque chose: this.getClass()
  • Crée une interface non uniforme entre les contextes statiques et non statiques (car il n'y a pas de this dans un contexte statique)
  • Les paramètres inutiles supplémentaires créent une marge d'erreur, permettent aux instructions de la même classe d'aller à différents enregistreurs (pensez au copier-coller imprudent)
  • Bien qu'il enregistre 74 caractères de déclaration de l'enregistreur, il ajoute 27 caractères supplémentaires à chaque appel de journalisation. Cela signifie que si une classe utilise l'enregistreur plus de 2 fois, vous augmentez le code standard en termes de nombre de caractères.
9
oksayt

Lorsque vous utilisez quelque chose comme: MyLoggerWrapper.debug(this.getClass(), "blah") Vous obtiendrez des noms de classe incorrects lorsque vous utilisez des frameworks AOP ou des outils d'injection de code. Les noms de classe ne sont pas comme l'origine, mais un nom de classe généré. Et un autre inconvénient de l'utilisation de l'encapsuleur: pour chaque instruction log, vous devez inclure du code supplémentaire "MyClass.class".

La 'mise en cache' des enregistreurs dépend des frameworks utilisés. Mais même dans ce cas, il doit toujours rechercher l'enregistreur souhaité pour chaque instruction de journal que vous faites. Donc, ayant 3 instructions dans une méthode, elle doit la rechercher 3 fois. En l'utilisant comme variable static, elle ne doit la rechercher qu'une seule fois!

Et dit avant: vous perdez la possibilité d'utiliser if( log.isXXXEnabled() ){} pour un ensemble d'instructions.

Qu'est-ce que votre patron a contre la "méthode acceptée et recommandée par défaut de la communauté"? L'introduction de l'emballage n'apporte pas plus d'efficacité. À la place, vous devez utiliser le nom de classe pour chaque instruction de journal. Après un certain temps, vous voulez "améliorer" cela, alors vous ajoutez une autre variable, ou un autre wrapper, ce qui rend plus difficile pour vous.

7
SPee

Voici une possibilité pour faciliter la connexion Java 8 - définissez une interface pour le faire pour vous. Par exemple:

package logtesting;

import Java.util.Arrays;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public interface Loggable { 
    enum LogLevel {
        TRACE, DEBUG, INFO, WARN, ERROR
    }

    LogLevel TRACE = LogLevel.TRACE;
    LogLevel DEBUG = LogLevel.DEBUG;
    LogLevel INFO = LogLevel.INFO;
    LogLevel WARN = LogLevel.WARN;
    LogLevel ERROR = LogLevel.ERROR;

    default void log(Object...args){
        log(DEBUG, args);
    }

    default void log(final LogLevel level, final Object...args){
        Logger logger = LoggerFactory.getLogger(this.getClass());
        switch(level){
        case ERROR:
            if (logger.isErrorEnabled()){
                logger.error(concat(args));
            }
            break;
        case WARN:
            if (logger.isWarnEnabled()){
                logger.warn(concat(args));
            }
            break;          
        case INFO:
            if (logger.isInfoEnabled()){
                logger.info(concat(args));
            }
        case TRACE:
            if (logger.isTraceEnabled()){
                logger.trace(concat(args));
            }
            break;
        default:
            if (logger.isDebugEnabled()){
                logger.debug(concat(args));
            }
            break;
        }
    }

    default String concat(final Object...args){ 
        return Arrays.stream(args).map(o->o.toString()).collect(Collectors.joining());
    }
}

Ensuite, tout ce que vous avez à faire est de vous assurer que vos classes déclarent implémentez Logged, et à partir de n'importe laquelle d'entre elles, vous pouvez faire des choses comme:

log(INFO, "This is the first part ","of my string ","and this ","is the last");

La fonction log () s'occupe de concaténer vos chaînes, mais seulement après avoir testé pour activé. Il se connecte pour déboguer par défaut, et si vous souhaitez vous connecter pour déboguer, vous pouvez omettre l'argument LogLevel. Ceci est un exemple très simple. Vous pouvez faire un certain nombre de choses pour améliorer cela, telles que l'implémentation des méthodes individuelles, c'est-à-dire error (), trace (), warn (), etc. Vous pouvez également simplement implémenter "logger" comme une fonction qui renvoie un enregistreur:

public interface Loggable {
    default Logger logger(){
        return LoggerFactory.getLogger(this.getClass());
    }
}

Et puis il devient assez trivial d'utiliser votre enregistreur:

logger().debug("This is my message");

Vous pouvez même le rendre entièrement fonctionnel en générant des méthodes déléguées pour toutes les méthodes Logger, de sorte que chaque classe d'implémentation soit une instance de Logger.

package logtesting;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;

public interface Loggable extends Logger {
    default Logger logger(){
        return LoggerFactory.getLogger(this.getClass());
    }

    default String getName() {
        return logger().getName();
    }

    default boolean isTraceEnabled() {
        return logger().isTraceEnabled();
    }

    default void trace(String msg) {
        logger().trace(msg);
    }

    default void trace(String format, Object arg) {
        logger().trace(format, arg);
    }

    default void trace(String format, Object arg1, Object arg2) {
        logger().trace(format, arg1, arg2);
    }

    default void trace(String format, Object... arguments) {
        logger().trace(format, arguments);
    }

    default void trace(String msg, Throwable t) {
        logger().trace(msg, t);
    }

    default boolean isTraceEnabled(Marker marker) {
        return logger().isTraceEnabled(marker);
    }

    default void trace(Marker marker, String msg) {
        logger().trace(marker, msg);
    }

    default void trace(Marker marker, String format, Object arg) {
        logger().trace(marker, format, arg);
    }

    default void trace(Marker marker, String format, Object arg1, Object arg2) {
        logger().trace(marker, format, arg1, arg2);
    }

    default void trace(Marker marker, String format, Object... argArray) {
        logger().trace(marker, format, argArray);
    }

    default void trace(Marker marker, String msg, Throwable t) {
        logger().trace(marker, msg, t);
    }

    default boolean isDebugEnabled() {
        return logger().isDebugEnabled();
    }

    default void debug(String msg) {
        logger().debug(msg);
    }

    default void debug(String format, Object arg) {
        logger().debug(format, arg);
    }

    default void debug(String format, Object arg1, Object arg2) {
        logger().debug(format, arg1, arg2);
    }

    default void debug(String format, Object... arguments) {
        logger().debug(format, arguments);
    }

    default void debug(String msg, Throwable t) {
        logger().debug(msg, t);
    }

    default boolean isDebugEnabled(Marker marker) {
        return logger().isDebugEnabled(marker);
    }

    default void debug(Marker marker, String msg) {
        logger().debug(marker, msg);
    }

    default void debug(Marker marker, String format, Object arg) {
        logger().debug(marker, format, arg);
    }

    default void debug(Marker marker, String format, Object arg1, Object arg2) {
        logger().debug(marker, format, arg1, arg2);
    }

    default void debug(Marker marker, String format, Object... arguments) {
        logger().debug(marker, format, arguments);
    }

    default void debug(Marker marker, String msg, Throwable t) {
        logger().debug(marker, msg, t);
    }

    default boolean isInfoEnabled() {
        return logger().isInfoEnabled();
    }

    default void info(String msg) {
        logger().info(msg);
    }

    default void info(String format, Object arg) {
        logger().info(format, arg);
    }

    default void info(String format, Object arg1, Object arg2) {
        logger().info(format, arg1, arg2);
    }

    default void info(String format, Object... arguments) {
        logger().info(format, arguments);
    }

    default void info(String msg, Throwable t) {
        logger().info(msg, t);
    }

    default boolean isInfoEnabled(Marker marker) {
        return logger().isInfoEnabled(marker);
    }

    default void info(Marker marker, String msg) {
        logger().info(marker, msg);
    }

    default void info(Marker marker, String format, Object arg) {
        logger().info(marker, format, arg);
    }

    default void info(Marker marker, String format, Object arg1, Object arg2) {
        logger().info(marker, format, arg1, arg2);
    }

    default void info(Marker marker, String format, Object... arguments) {
        logger().info(marker, format, arguments);
    }

    default void info(Marker marker, String msg, Throwable t) {
        logger().info(marker, msg, t);
    }

    default boolean isWarnEnabled() {
        return logger().isWarnEnabled();
    }

    default void warn(String msg) {
        logger().warn(msg);
    }

    default void warn(String format, Object arg) {
        logger().warn(format, arg);
    }

    default void warn(String format, Object... arguments) {
        logger().warn(format, arguments);
    }

    default void warn(String format, Object arg1, Object arg2) {
        logger().warn(format, arg1, arg2);
    }

    default void warn(String msg, Throwable t) {
        logger().warn(msg, t);
    }

    default boolean isWarnEnabled(Marker marker) {
        return logger().isWarnEnabled(marker);
    }

    default void warn(Marker marker, String msg) {
        logger().warn(marker, msg);
    }

    default void warn(Marker marker, String format, Object arg) {
        logger().warn(marker, format, arg);
    }

    default void warn(Marker marker, String format, Object arg1, Object arg2) {
        logger().warn(marker, format, arg1, arg2);
    }

    default void warn(Marker marker, String format, Object... arguments) {
        logger().warn(marker, format, arguments);
    }

    default void warn(Marker marker, String msg, Throwable t) {
        logger().warn(marker, msg, t);
    }

    default boolean isErrorEnabled() {
        return logger().isErrorEnabled();
    }

    default void error(String msg) {
        logger().error(msg);
    }

    default void error(String format, Object arg) {
        logger().error(format, arg);
    }

    default void error(String format, Object arg1, Object arg2) {
        logger().error(format, arg1, arg2);
    }

    default void error(String format, Object... arguments) {
        logger().error(format, arguments);
    }

    default void error(String msg, Throwable t) {
        logger().error(msg, t);
    }

    default boolean isErrorEnabled(Marker marker) {
        return logger().isErrorEnabled(marker);
    }

    default void error(Marker marker, String msg) {
        logger().error(marker, msg);
    }

    default void error(Marker marker, String format, Object arg) {
        logger().error(marker, format, arg);
    }

    default void error(Marker marker, String format, Object arg1, Object arg2) {
        logger().error(marker, format, arg1, arg2);
    }

    default void error(Marker marker, String format, Object... arguments) {
        logger().error(marker, format, arguments);
    }

    default void error(Marker marker, String msg, Throwable t) {
        logger().error(marker, msg, t);
    }
}

Bien sûr, comme mentionné précédemment, cela signifie que chaque fois que vous vous connectez, vous devrez passer par le processus de recherche de l'enregistreur dans votre LoggerFactory - à moins que vous ne remplaciez la méthode logger () ... auquel cas vous pourriez aussi bien le faire la manière "recommandée".

5
Steve K

Je dois juste dire que le modèle recommandé est le plus facile à lire et à mettre en œuvre. Je ne vois aucune raison de m'en éloigner. Surtout aucun avantage.

Cependant, mon point principal concerne les gardes mentionnés précédemment. Je ne recommanderais pas de protéger explicitement vos journaux car cela est déjà fait en interne par log4j et est une duplication d'efforts.

Téléchargez la source de log4j et jetez un œil aux classes Logger et Category pour voir par vous-même.

4
Alasdair

Comme indiqué ici par l'équipe SLF4J, vous pouvez utiliser MethodLookup () introduit dans JDK 1.7.

final static Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

De cette façon, vous pouvez vous référer à la classe sans avoir à utiliser le mot-clé "this".

4
Mariano LEANCE

Version 2015 de la réponse: veuillez libérer votre esprit avec lombok @ slf4j .

4
fayndee

Non, sauf qu'il gâche la pile d'appels. Cela perturbe les méthodes qui vous permettent de voir le nom de la méthode et la classe du code faisant le journal.

Vous pouvez envisager de jeter un œil au conteneur Web Jetty qui contient leur propre abstraction qui s'appuie sur slf4j. Très agréable.

Il y a deux raisons pour lesquelles l'approche de votre patron n'atteint pas ce qu'il pense.

La plus petite raison est que le surcoût lié à l'ajout d'un enregistreur statique est négligeable. Après tout, la configuration de l'enregistreur fait partie de cette séquence assez longue:

  • Localisez la classe, c'est-à-dire parcourez tous les .jars et répertoires. Java. Très gros frais généraux dus aux appels du système de fichiers. Peut créer des objets d'aide, par exemple avec File.
  • Chargez le bytecode, c'est-à-dire copiez-le dans une structure de données à l'intérieur de la JVM. Code natif.
  • Validez le bytecode. Code natif.
  • Liez le bytecode, c'est-à-dire parcourez tous les noms de classe dans le bytecode et remplacez-les par des pointeurs vers la classe référencée. Code natif.
  • Exécutez des initialiseurs statiques. Déclenchés à partir du code natif, les initialiseurs sont Java code bien sûr. Le Logger est créé ici.
  • Après un certain temps, peut-être JIT-compilera la classe. Code natif. énorme frais généraux (par rapport aux autres opérations de toute façon).

De plus, votre patron ne sauvegarde rien.
Le premier appel à LoggerFactor.getLogger Créera l'enregistreur et le placera dans un HashMap global nom-à-enregistreur. Cela se produira même pour les appels isXxxEnabled, car pour cela, vous devez d'abord construire l'objet Logger ...
L'objet Class portera un pointeur supplémentaire pour la variable statique. Ceci est compensé par la surcharge de passage du paramètre clazz - une instruction supplémentaire et une référence de taille de pointeur supplémentaire dans le bytecode, de sorte que vous perdez déjà au moins un octet dans la taille de la classe.

Le code passe également par une indirection supplémentaire, LoggerFactory.getLogger(Class) utilise Class#getName Et délègue à LoggerFactory.getLogger(String).

Maintenant, si votre patron n'est pas après la performance mais après avoir la capacité de simplement copier la déclaration statique, il peut utiliser une fonction qui inspecte la pile des appels et récupère le nom de la classe. La fonction doit être annotée @CallerSensitive, Et c'est toujours quelque chose qui doit être testé chaque fois qu'une nouvelle JVM est utilisée - pas sympa si vous ne contrôlez pas la JVM sur laquelle l'utilisateur exécute le code.

L'approche la moins problématique serait d'avoir un IDE qui vérifie l'instanciation de l'enregistreur. Cela signifie probablement trouver ou écrire un plugin.

1
toolforger

Je l'ai peut-être manqué dans l'un des commentaires précédents, mais je n'ai vu aucune mention que l'enregistreur est statique, l'appel à LoggerFactory est effectué UNE FOIS (par instanciation de la classe) - donc le la préoccupation initiale concernant plusieurs appels pour créer l'enregistreur est tout simplement fausse.

Les autres commentaires concernant tous les problèmes liés à l'ajout de classes d'enveloppement sont également très importants.

0
Jay