Registrazione Java con un file per pacchetto root

Oct 22 2020

Dispongo di un insieme di classi su più pacchetti e desidero che qualsiasi registrazione da un singolo pacchetto "com.name.root.xxx" e tutti i suoi pacchetti figlio, venga registrata in un file diverso. per esempio:

//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

eccetera.

Ho creato una classe helper che tiene traccia di quali percorsi dei file di registro sono già stati impostati con i gestori di file e anche di quali pacchetti sono già stati impostati i logger; e fornisce di conseguenza.

Sono interessato alla revisione per l'efficienza e se esiste un modo migliore (più semplice / più pulito / più comprensibile) per farlo:

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;
        }
    }
}

Utilizzo di esempio:

Per le classi di utilità (ad esempio le classi di accesso al database statico), sto solo passando il logger in ogni metodo che lo utilizza, perché può essere utilizzato da più pacchetti e dovrebbe registrarsi come se fosse una parte della classe chiamante:

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
    }
}

ma per tutte le altre classi, ogni classe ha la propria istanza Logger finale statica che viene istanziata insieme alla classe, chiamando il metodo getConfiguredLogger:

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);
}

Risposte

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

Quella classe dovrebbe molto probabilmente essere finale avere un privatecostruttore, per chiarire che si tratta di un'utilità statica.


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

Cerca sempre di utilizzare la classe o l'interfaccia comune più bassa quando dichiari le variabili, in questo caso Mape Set.


Non sono sicuro dei tuoi nomi, normalmente mi aspetto che le static finalvariabili siano costanti, quindi denominate con UPPER_SNAKE_CASE.


public class LogProvider

La classe non fornisce log, fornisce logger, quindi dovrebbe essere nominata LoggerProvider.

Più comunemente è il nome LoggerFactorydi tali classi, però.


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

Eliminare "Configurato" dal nome, poiché non è possibile ottenere un logger "non configurato" e inoltre non importa all'utente dell'API se è configurato o meno. Potresti anche farla franca solo con un getmetodo, come questo:

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

String fqClassName = callingClass.getCanonicalName();

Il nome della variabile non è corretto, è il nome canonico, non il nome completo. Potrebbero o non potrebbero essere la stessa cosa.


String packageName = fqClassName;

Anche questo non è corretto, non è il nome del pacchetto (recuperato tramite Class.getPackageName()) ma è il nome canonico.


if (fqClassName.startsWith(rootPackageName))

Quel controllo non è corretto, si applicherebbe anche a com.name.rootbutdifferentpackage.sub.Class.


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

Dovresti usare un logger per registrarlo ... ma in realtà, molto probabilmente vorrai fallire, perché in questo caso non puoi più fornire funzionalità di registrazione, il che potrebbe compromettere il funzionamento dell'applicazione.

Sì, non registrare le informazioni è un problema serio negli ambienti aziendali, per niente nell'applicazione desktop, ma se hai un datacenter con 10.000 istanze in esecuzione e 5k di queste non registrano, hai un problema.


Per quanto posso vedere, la tua classe non è thread-safe, il che potrebbe portare a un errore o che a FileHandlerviene creato due volte per lo stesso file, probabilmente corrompendo i dati di registro.

È necessario un concetto di quando sincronizzare l'accesso alle informazioni memorizzate. Il modo più semplice potrebbe essere quello di utilizzare un elenco sincronizzato utilizzando un wrapper creato da Collections.synchronized*. Tuttavia, ciò eliminerà solo le possibili eccezioni, potrebbe comunque danneggiare lo stato memorizzato. Avrai bisogno di sincronizzarti su qualcosa, un oggetto di blocco per assicurarti di non creare mai lo stesso file due volte.

// 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;

Per le classi di utilità (ad esempio le classi di accesso al database statico), sto solo passando il logger in ogni metodo che lo utilizza, perché può essere utilizzato da più pacchetti e dovrebbe registrarsi come se fosse una parte della classe chiamante:

Che odora. Ma capisco perché lo stai facendo.

Non c'è un modo semplice per fare quello che vuoi qui, si potrebbe trovare una soluzione per analizzare lo StackTrace corrente per recuperare la classe e il pacchetto chiamante, ma potrebbe essere una soluzione fragile senza pensarci molto.


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

Come detto prima, mi aspetto che la variabile UPPER_SNAKE_CASE.

Mi mancava prima, ma perché stai seguendo l'intero percorso? Sarebbe molto meglio se non si passasse affatto un percorso, ma invece impostarne uno LogProvidere quindi creare un percorso / file per questo in base alla classe passata. Sarebbe molto meno fragile e deterministico.

ZoranJankov Oct 29 2020 at 03:46

Non sono completamente sicuro di cosa stai cercando di ottenere qui, ma dovresti archiviare tutte le tue risorse, vale a dire i tuoi file di registro, in una cartella di origine delle risorse e nel codice puoi accedere a tali risorse con:

// 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");