Kök paket başına bir dosya ile Java Günlüğü

Oct 22 2020

Birden çok pakette bir dizi sınıfım var ve tek bir "com.name.root.xxx" paketi ve tüm alt paketleri içindeki tüm günlüklerin farklı bir dosyada günlüğe kaydedilmesini istiyorum. Örneğin:

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

vb.

Dosya işleyicileri ile hangi günlük dosyası yollarının önceden ayarlandığını ve ayrıca hangi paketlerin günlüklerinin önceden kurulmuş olduğunu izleyen bir yardımcı sınıf oluşturdum; ve buna göre sağlar.

Verimlilik için incelemeyle ilgileniyorum ve bunu yapmanın daha iyi (daha basit / daha temiz / daha anlaşılır) bir yolu varsa:

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

Örnek Kullanım:

Yardımcı program sınıfları için (örneğin, statik veritabanı erişim sınıfları), günlükçüyü onu kullanan her yönteme geçiriyorum, çünkü birden çok paketten kullanılabilir ve çağıran sınıfın bir parçasıymış gibi günlüğe kaydetmelidir:

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

ancak diğer tüm sınıflar için, her sınıfın, getConfiguredLogger yöntemini çağıran, sınıfla birlikte başlatılan kendi statik son Logger örneği vardır:

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

Yanıtlar

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

Statik bir yardımcı program olduğunu netleştirmek için bu sınıf büyük olasılıkla finalbir privatekurucu olmalı ve bir kurucuya sahip olmalıdır .


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

Değişkenleri bildirirken her zaman en düşük ortak sınıfı veya arayüzü kullanmayı deneyin, bu durumda Mapve Set.


İsimlerinizden emin değilim, normalde static finaldeğişkenlerin sabitler olmasını beklerdim , bu nedenle UPPER_SNAKE_CASE ile adlandırılır.


public class LogProvider

Sınıf günlükler sağlamaz, günlükler sağlar, bu nedenle adlandırılmalıdır LoggerProvider.

Daha yaygın olarak, bu LoggerFactorytür sınıfların adıdır .


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

"Yapılandırılmamış" bir günlükleyici almanın bir yolu olmadığından ve yapılandırılmış olsun ya da olmasın API kullanıcısı için önemli olmadığından "Yapılandırıldı" ifadesini adından çıkarırdım. Bunun getgibi sadece bir yönteme sahip olmakla bile kurtulabilirsiniz :

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

String fqClassName = callingClass.getCanonicalName();

Değişkenin adı yanlış, kurallı ad, tam nitelikli ad değil. Aynı olabilirler de olmayabilir de.


String packageName = fqClassName;

Bu da yanlıştır, bu paket adı (üzerinden alınan Class.getPackageName()) değil, kanonik addır.


if (fqClassName.startsWith(rootPackageName))

Bu çek yanlıştır, için de geçerlidir com.name.rootbutdifferentpackage.sub.Class.


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

Bunu günlüğe kaydetmek için bir kaydedici kullanmalısınız ... ama aslında, büyük olasılıkla başarısız olmak istersiniz, çünkü bu durumda artık günlükleme yetenekleri sağlayamazsınız, bu da uygulamanın çalışmasını tehlikeye atabilir.

Evet, kurumsal ortamlarda, masaüstü uygulamasında bilgilerin günlüğe kaydedilmemesi ciddi bir sorundur, ancak 10k örneği çalışan bir veri merkeziniz varsa ve bunların 5k tanesi günlüğe kaydetmiyorsa, bir sorununuz var demektir.


Görebildiğim kadarıyla, sınıfınız iş parçacığı açısından güvenli değil, bu da başarısızlığa yol açabilir veya a FileHandleraynı dosya için iki kez oluşturulabilir ve muhtemelen günlük verilerini bozabilir.

Depolanan bilgilere erişimi ne zaman senkronize edeceğinize dair bir konsepte ihtiyacınız var. En kolay yol, tarafından oluşturulan bir sarmalayıcı kullanarak eşitlenmiş bir liste kullanmak olabilir Collections.synchronized*. Ancak, bu yalnızca olası istisnalardan kurtulacaktır, yine de depolanan durumu bozabilir. Aynı dosyayı asla iki kez oluşturmadığınızdan emin olmak için bir kilit nesnesi üzerinde senkronize etmeniz gerekecek.

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

Yardımcı program sınıfları için (örneğin, statik veritabanı erişim sınıfları), günlükçüyü onu kullanan her yönteme geçiriyorum, çünkü birden çok paketten kullanılabilir ve çağıran sınıfın bir parçasıymış gibi günlüğe kaydetmelidir:

Bu kokuyor. Ama neden yaptığını anlıyorum.

Burada istediğinizi yapmanın kolay bir yolu yoktur, çağıran sınıf ve paketi almak için mevcut StackTrace'i ayrıştırmak için bir çözüm bulabiliriz, ancak bu gerçekten fazla düşünmeden kırılgan bir çözüm olabilir.


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

Daha önce de söylediğim gibi, bu değişkenin UPPER_SNAKE_CASE olmasını beklerdim.

Bunu daha önce kaçırdım ama neden bütün yolu geçiyorsun? Bir yolu hiç geçmeseniz çok daha iyi olurdu, bunun yerine bir tane ayarlayın LogProviderve sonra bunun için sınıfta geçirilene göre bir yol / dosya oluşturun. Bu çok daha az kırılgan ve belirleyici olacaktır.

ZoranJankov Oct 29 2020 at 03:46

Burada neyi başarmaya çalıştığınızdan tam olarak emin değilim, ancak tüm kaynaklarınızı, yani günlük dosyalarınızı bir kaynak kaynak klasöründe depolamalısınız ve kodda bu kaynaklara aşağıdakilerle erişebilirsiniz:

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