Java Logging ด้วยไฟล์ต่อแพ็กเกจรูท

Oct 22 2020

ฉันมีชุดคลาสในหลายแพ็กเกจและฉันต้องการให้มีการบันทึกจากภายในแพ็กเกจ "com.name.root.xxx" เดียวและแพ็กเกจลูกทั้งหมดเพื่อบันทึกลงในไฟล์อื่น เช่น:

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

เป็นต้น

ฉันได้สร้างคลาสตัวช่วยที่ติดตามว่าเส้นทางไฟล์บันทึกใดได้รับการตั้งค่าด้วยตัวจัดการไฟล์แล้วและแพ็คเกจใดที่มีการตั้งค่าผู้บันทึกไว้แล้ว และให้ตามนั้น

ฉันสนใจที่จะตรวจสอบประสิทธิภาพและหากมีวิธีที่ดีกว่า (ง่ายกว่า / สะอาดกว่า / เข้าใจได้มากกว่านี้) ให้ทำดังนี้:

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

ตัวอย่างการใช้งาน:

สำหรับคลาสยูทิลิตี้ (เช่นคลาสการเข้าถึงฐานข้อมูลแบบคงที่) ฉันแค่ส่งตัวบันทึกไปยังแต่ละวิธีที่ใช้เนื่องจากสามารถใช้งานได้จากหลายแพ็คเกจและควรบันทึกราวกับว่าเป็นส่วนหนึ่งของคลาสการโทร:

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

แต่สำหรับคลาสอื่น ๆ ทั้งหมดแต่ละคลาสจะมีอินสแตนซ์ Logger สุดท้ายแบบคงที่ซึ่งสร้างอินสแตนซ์พร้อมกับคลาสโดยเรียกเมธอด 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);
}

คำตอบ

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

คลาสนั้นควรจะเป็นfinalและมีตัวprivateสร้างเพื่อให้ชัดเจนว่าเป็นยูทิลิตี้แบบคงที่


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

พยายามที่จะใช้ระดับต่ำสุดที่พบบ่อยหรือติดต่อเมื่อประกาศตัวแปรในกรณีนี้และMapSet


ฉันไม่แน่ใจในชื่อของคุณโดยปกติฉันคาดว่าstatic finalตัวแปรจะคงที่ดังนั้นจึงตั้งชื่อด้วย UPPER_SNAKE_CASE


public class LogProvider

LoggerProviderชั้นไม่ได้ให้บันทึกก็ไม่ให้ตัดไม้ดังนั้นจึงควรจะตั้งชื่อ

โดยทั่วไปมักเป็นชื่อLoggerFactoryของคลาสดังกล่าว


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

ฉันจะทิ้ง "กำหนดค่า" ออกจากชื่อเนื่องจากไม่มีวิธีรับตัวบันทึกที่ "ไม่ได้กำหนดค่า" และไม่สำคัญกับผู้ใช้ API ว่ามีการกำหนดค่าหรือไม่ คุณอาจหลีกหนีไปได้โดยมีเพียงgetวิธีการเช่นนี้:

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

String fqClassName = callingClass.getCanonicalName();

ชื่อของตัวแปรไม่ถูกต้องเป็นชื่อ Canonical ไม่ใช่ชื่อแบบเต็ม อาจจะเหมือนกันหรือไม่ก็ได้


String packageName = fqClassName;

อันนี้ก็ไม่ถูกต้องเช่นกันไม่ใช่ชื่อแพ็คเกจ (เรียกดูผ่านClass.getPackageName()) แต่เป็นชื่อ Canonical


if (fqClassName.startsWith(rootPackageName))

com.name.rootbutdifferentpackage.sub.Classที่ตรวจสอบไม่ถูกต้องมันก็จะนำไปใช้


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

คุณควรใช้คนตัดไม้เพื่อบันทึกสิ่งนั้น ... แต่จริงๆแล้วคุณมักต้องการล้มเหลวเนื่องจากในกรณีนี้คุณไม่สามารถให้ความสามารถในการบันทึกได้อีกต่อไปซึ่งอาจเป็นอันตรายต่อการทำงานของแอปพลิเคชัน

ใช่การไม่บันทึกข้อมูลเป็นปัญหาร้ายแรงในสภาพแวดล้อมขององค์กรในแอปพลิเคชันเดสก์ท็อปไม่ใช่เลย แต่ถ้าคุณมีดาต้าเซ็นเตอร์ที่มีอินสแตนซ์ 10k ทำงานอยู่และ 5k ไม่บันทึกแสดงว่าคุณมีปัญหา


เท่าที่ฉันเห็นคลาสของคุณไม่ปลอดภัยต่อเธรดซึ่งอาจนำไปสู่ความล้มเหลวหรือFileHandlerสร้างขึ้นสองครั้งสำหรับไฟล์เดียวกันอาจทำให้ข้อมูลบันทึกเสียหาย

คุณต้องมีแนวคิดว่าจะซิงโครไนซ์การเข้าถึงข้อมูลที่จัดเก็บไว้เมื่อใด Collections.synchronized*วิธีที่ง่ายที่สุดอาจจะมีการใช้รายการตรงกันโดยใช้กระดาษห่อที่สร้างขึ้นโดย อย่างไรก็ตามนั่นจะกำจัดเฉพาะข้อยกเว้นที่เป็นไปได้เท่านั้น แต่อาจทำให้สถานะที่จัดเก็บเสียหายได้ คุณจะต้องซิงโครไนซ์กับบางสิ่งบางอย่างล็อคอ็อบเจ็กต์เพื่อให้แน่ใจว่าคุณจะไม่สร้างไฟล์เดียวกันซ้ำสอง

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

สำหรับคลาสยูทิลิตี้ (เช่นคลาสการเข้าถึงฐานข้อมูลแบบคงที่) ฉันแค่ส่งตัวบันทึกไปยังแต่ละวิธีที่ใช้เนื่องจากสามารถใช้งานได้จากหลายแพ็คเกจและควรบันทึกราวกับว่าเป็นส่วนหนึ่งของคลาสการโทร:

ที่มีกลิ่น แต่ฉันเข้าใจว่าทำไมคุณถึงทำมัน

ไม่มีวิธีง่ายๆในการทำสิ่งที่คุณต้องการที่นี่เราสามารถหาวิธีแก้ปัญหาเพื่อแยกวิเคราะห์ StackTrace ปัจจุบันเพื่อดึงคลาสการโทรและแพ็คเกจ แต่นั่นอาจเป็นวิธีแก้ปัญหาที่เปราะบางโดยไม่ต้องคิดมาก


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

อย่างที่กล่าวไว้ก่อนหน้านี้ฉันคาดว่าตัวแปรนั้นจะเป็น UPPER_SNAKE_CASE

ฉันพลาดสิ่งนี้มาก่อน แต่ทำไมคุณถึงเดินผ่านทั้งเส้นทาง? มันจะดีกว่ามากถ้าคุณไม่ผ่านเส้นทางเลย แต่ให้ตั้งค่าหนึ่งในLogProviderแล้วสร้างเส้นทาง / ไฟล์สำหรับสิ่งนี้ตามการส่งผ่านในคลาส นั่นจะมีความเปราะบางและกำหนดได้น้อยกว่ามาก

ZoranJankov Oct 29 2020 at 03:46

ฉันไม่แน่ใจว่าคุณพยายามทำอะไรที่นี่ แต่คุณควรจัดเก็บทรัพยากรทั้งหมดของคุณคือไฟล์บันทึกของคุณในโฟลเดอร์แหล่งทรัพยากรและในรหัสคุณสามารถเข้าถึงทรัพยากรเหล่านั้นได้ด้วย:

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