Java Logging ด้วยไฟล์ต่อแพ็กเกจรูท
ฉันมีชุดคลาสในหลายแพ็กเกจและฉันต้องการให้มีการบันทึกจากภายในแพ็กเกจ "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);
}
คำตอบ
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แล้วสร้างเส้นทาง / ไฟล์สำหรับสิ่งนี้ตามการส่งผ่านในคลาส นั่นจะมีความเปราะบางและกำหนดได้น้อยกว่ามาก
ฉันไม่แน่ใจว่าคุณพยายามทำอะไรที่นี่ แต่คุณควรจัดเก็บทรัพยากรทั้งหมดของคุณคือไฟล์บันทึกของคุณในโฟลเดอร์แหล่งทรัพยากรและในรหัสคุณสามารถเข้าถึงทรัพยากรเหล่านั้นได้ด้วย:
// 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");