Java-Protokollierung mit einer Datei pro Root-Paket

Oct 22 2020

Ich habe eine Reihe von Klassen über mehrere Pakete hinweg, und ich möchte, dass jede Protokollierung innerhalb eines einzelnen "com.name.root.xxx" -Pakets und aller seiner untergeordneten Pakete in einer anderen Datei protokolliert wird. z.B:

//Package Name                    Log To     \\
//-------------------------------------------\\
com.name.root.router         C:\com\router.log
com.name.root.router.utils   C:\com\router.log
com.name.root.init           C:\com\init.log
com.name.root.database       C:\com\database.log
com.name.root.web            C:\com\web.log
com.name.root.web.rest       C:\com\web.log
com.name.root.web.http       C:\com\web.log

usw.

Ich habe eine Hilfsklasse erstellt, die verfolgt, welche Protokolldateipfade bereits mit Dateihandlern eingerichtet wurden und welche Pakete bereits ihre Protokollierer eingerichtet haben. und sorgt entsprechend.

Ich bin an einer Überprüfung auf Effizienz interessiert, und wenn es einen besseren (einfacheren / saubereren / verständlicheren) Weg gibt, dies zu tun:

package com.name.root.util.log;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;

public class LogProvider
{
    private static final String rootPackageName = "com.name.root";
    private static final int rootPackageNameLength = rootPackageName.length();
    private static final HashMap<String,FileHandler> fileHandlersByPath = new HashMap<>();
    private static final HashSet<String> alreadyProvidedPackages = new HashSet<>();

    public static Logger getConfiguredLogger(Class<?> callingClass, String logPathIfNotAlreadySet)
    {
        return getConfiguredLogger(callingClass, logPathIfNotAlreadySet, Level.FINEST); // default log level
    }

    public static Logger getConfiguredLogger(Class<?> callingClass, String logPathIfNotAlreadySet, Level maxLogLevelIfNotAlreadySet)
    {
        String fqClassName = callingClass.getCanonicalName(); // e.g. com.name.root.router.utils
        String packageName = fqClassName; // default
        if (fqClassName.startsWith(rootPackageName))
        {
            // we want to just get as far as the main package after "com.name.root" - e.g. "com.name.root.router"
            packageName = fqClassName.substring(0,fqClassName.indexOf(".", rootPackageNameLength+1));
        }
        
        return getConfiguredLogger(packageName, logPathIfNotAlreadySet, maxLogLevelIfNotAlreadySet);
    }

    private static Logger getConfiguredLogger(String packageName, String logPathIfNotAlreadySet, Level maxLogLevelIfNotAlreadySet)
    {
        Logger logger = Logger.getLogger(packageName); // get the logger for the package
        if (alreadyProvidedPackages.contains(packageName))
        {
            return logger; // we've already configured this logger
        }
        else
        {
            alreadyProvidedPackages.add(packageName);

            logger.setLevel(maxLogLevelIfNotAlreadySet);
            
            String logPath = (logPathIfNotAlreadySet == null || logPathIfNotAlreadySet.isBlank() ? "C:\\com\\output.log" : logPathIfNotAlreadySet);
            
            try
            {
                // reuse an existing file handler if possible, so we don't get multiple output files if two packages want to log to the same file
                FileHandler fh = null;
                
                if (fileHandlersByPath.containsKey(logPath))
                {
                    fh = (fileHandlersByPath.get(logPath));
                }
                else
                {
                    fh = new FileHandler(logPath, false);
                    fh.setFormatter(new customSingleLineLogFormatter()); // The formatter itself is out of scope for review
                    fileHandlersByPath.put(logPath, fh);
                }
                
                logger.addHandler(fh);
            }           
            catch (SecurityException | IOException e)
            {
                e.printStackTrace();
            }
            
            return logger;
        }
    }
}

Anwendungsbeispiel:

Für Dienstprogrammklassen (z. B. statische Datenbankzugriffsklassen) übergebe ich den Logger nur an jede Methode, die ihn verwendet, da er aus mehreren Paketen verwendet werden kann und protokollieren sollte, als ob er Teil der aufrufenden Klasse wäre:

package com.name.root.util.database
public class StringUtils
{
    // just an example
    public static long parseStringToEpoch(String s, Logger logger)
    {
        logger.entering("parseStringToEpoch"); // should turn up in the calling class's log file
    }
}

Für alle anderen Klassen verfügt jede Klasse über eine eigene statische endgültige Logger-Instanz, die zusammen mit der Klasse instanziiert wird und die Methode getConfiguredLogger aufruft:

package com.name.root.router.base
public abstract class BaseRouter
{
    private static final Logger logger = LogProvider.getConfiguredLogger(BaseRouter.class, "C:\\com\\Router.log");`
// etc, including static methods that log
}

package com.name.root.router.impl
public class ChildRouter
{
    private static final Logger logger = LogProvider.getConfiguredLogger(ChildRouter.class, "C:\\com\\Router.log");`
// etc, including main and static methods that log

   // sample usage of utilities methods
   private static final long testEpoch = StringUtils.parseStringToEpoch("1234567",logger);
}

Antworten

1 Bobby Oct 29 2020 at 19:16
public class LogProvider

Diese Klasse sollte höchstwahrscheinlich finaleinen privateKonstruktor haben und haben , um zu verdeutlichen, dass es sich um ein statisches Dienstprogramm handelt.


    private static final HashMap<String,FileHandler> fileHandlersByPath = new HashMap<>();
    private static final HashSet<String> alreadyProvidedPackages = new HashSet<>();

Versuchen Sie immer, die niedrigste gemeinsame Klasse oder Schnittstelle zu verwenden, wenn Sie Variablen deklarieren, in diesem Fall Mapund Set.


Ich bin mir Ihrer Namen nicht sicher. Normalerweise würde ich erwarten static final, dass Variablen Konstanten sind, die daher mit UPPER_SNAKE_CASE benannt werden.


public class LogProvider

Die Klasse stellt keine Protokolle bereit, sie stellt Protokollierer bereit, daher sollte sie benannt werden LoggerProvider.

Häufiger ist jedoch der Name LoggerFactoryfür solche Klassen.


public static Logger getConfiguredLogger(Class<?> callingClass, String logPathIfNotAlreadySet)

Ich würde "Konfiguriert" aus dem Namen streichen, da es keine Möglichkeit gibt, einen "nicht konfigurierten" Logger zu erhalten, und es für den Benutzer der API auch nicht wichtig ist, ob er konfiguriert ist oder nicht. Sie könnten sogar davonkommen, nur eine getMethode wie diese zu haben:

Logger LOGGER = LoggerFactory.get(SomeClass.class);

String fqClassName = callingClass.getCanonicalName();

Der Name der Variablen ist falsch, es ist der kanonische Name, nicht der vollständig qualifizierte Name. Sie könnten gleich sein oder auch nicht.


String packageName = fqClassName;

Dieser ist auch falsch, es ist nicht der Paketname (abgerufen durch Class.getPackageName()), sondern der kanonische Name.


if (fqClassName.startsWith(rootPackageName))

Diese Prüfung ist falsch, sie würde auch für gelten com.name.rootbutdifferentpackage.sub.Class.


            catch (SecurityException | IOException e)
            {
                e.printStackTrace();
            }

Sie sollten einen Protokollierer verwenden, um dies zu protokollieren ... aber tatsächlich möchten Sie höchstwahrscheinlich fehlschlagen, da Sie in diesem Fall keine Protokollierungsfunktionen mehr bereitstellen können, die den Betrieb der Anwendung gefährden könnten.

Ja, Informationen nicht zu protokollieren ist ein ernstes Problem in Unternehmensumgebungen, in Desktopanwendungen überhaupt nicht. Wenn Sie jedoch ein Rechenzentrum mit 10.000 Instanzen haben und 5.000 nicht protokollieren, liegt ein Problem vor.


Soweit ich sehen kann, ist Ihre Klasse nicht threadsicher, was zu einem Fehler führen kann oder dazu, dass a FileHandlerzweimal für dieselbe Datei erstellt wird , wodurch möglicherweise die Protokolldaten beschädigt werden.

Sie benötigen ein Konzept, wann der Zugriff auf die gespeicherten Informationen synchronisiert werden soll. Am einfachsten ist es, eine synchronisierte Liste mit einem von erstellten Wrapper zu verwenden Collections.synchronized*. Dadurch werden jedoch nur mögliche Ausnahmen beseitigt, und der gespeicherte Status kann weiterhin beschädigt werden. Sie müssen etwas synchronisieren, ein Sperrobjekt, um sicherzustellen, dass Sie niemals dieselbe Datei zweimal erstellen.

// Requires to be synchronized to allow adding and checking at the same time.
private static Map<String,FileHandler> fileHandlersByPath = Collections.synchronizedMap(new HashMap<>());

private static Object insertionLockObject = new Object();

// ...

// Assuming that this method is thread-safe and only delivers the same
// instance once for the same parameter.
Logger logger = Logger.getLogger(packageName);

// First "cheap" check to see if it is set.
if (!alreadyProvidedPackages.contains(packageName)) {
    // If it is not, we must acquire the lock to insert it.
    synchronized(insertionLockObject) {
        // Second check, because another thread could have acquired
        // the lock before us, and already did all the set up.
        if (!alreadyProvidedPackages.contains(packageName)) {
            // Code goes here.
        }
    }
}

return logger;

Für Dienstprogrammklassen (z. B. statische Datenbankzugriffsklassen) übergebe ich den Logger nur an jede Methode, die ihn verwendet, da er aus mehreren Paketen verwendet werden kann und protokollieren sollte, als ob er Teil der aufrufenden Klasse wäre:

Das riecht. Aber ich verstehe, warum du es tust.

Es gibt hier keine einfache Möglichkeit, das zu tun, was Sie wollen. Man könnte eine Lösung finden, um die aktuelle StackTrace zu analysieren, um die aufrufende Klasse und das aufrufende Paket abzurufen, aber das könnte eine fragile Lösung sein, ohne wirklich darüber nachzudenken.


private static final Logger logger = LogProvider.getConfiguredLogger(BaseRouter.class, "C:\\com\\Router.log");

Wie bereits erwähnt, würde ich diese Variable für UPPER_SNAKE_CASE erwarten.

Ich habe das schon einmal verpasst, aber warum gehst du den ganzen Weg? Es wäre viel besser, wenn Sie keinen Pfad übergeben würden, sondern einen Pfad festlegen LogProviderund dann einen Pfad / eine Datei dafür basierend auf der übergebenen Klasse erstellen würden. Das wäre viel weniger fragil und deterministisch.

ZoranJankov Oct 29 2020 at 03:46

Ich bin nicht ganz sicher, was Sie hier erreichen möchten, aber Sie sollten alle Ihre Ressourcen, nämlich Ihre Protokolldateien, in einem Ressourcenquellenordner speichern und in Code, auf den Sie zugreifen können, mit:

// the stream holding the file content
InputStream is = getClass().getClassLoader().getResourceAsStream("file.txt");

// for static access, uses the class name directly
InputStream is = JavaClassName.class.getClassLoader().getResourceAsStream("file.txt");