루트 패키지 당 파일로 Java 로깅
여러 패키지에 걸쳐 클래스 집합이 있고 단일 "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
}
}
그러나 다른 모든 클래스의 경우 각 클래스에는 getConfiguredLogger 메서드를 호출하여 클래스와 함께 인스턴스화되는 자체 정적 최종 Logger 인스턴스가 있습니다.
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<>();
변수를 선언 할 때 항상 가장 낮은 공통 클래스 또는 인터페이스를 사용하십시오 (이 경우 Map및 Set.
나는 당신의 이름을 확신하지 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();
변수 이름이 잘못되었습니다. 정규화 된 이름이 아니라 정식 이름입니다. 그들은 같을 수도 있고 같지 않을 수도 있습니다.
String packageName = fqClassName;
이것은 또한 올바르지 않습니다. 패키지 이름 (을 통해 검색 됨 Class.getPackageName())이 아니라 정식 이름입니다.
if (fqClassName.startsWith(rootPackageName))
이 검사는 올바르지 않습니다 com.name.rootbutdifferentpackage.sub.Class..
catch (SecurityException | IOException e)
{
e.printStackTrace();
}
로거를 사용하여 기록해야하지만 실제로는 실패 할 가능성이 큽니다.이 경우 더 이상 로깅 기능을 제공 할 수 없어 응용 프로그램의 작동이 위험해질 수 있기 때문입니다.
예, 정보를 기록하지 않는 것은 엔터프라이즈 환경에서 심각한 문제입니다. 데스크톱 애플리케이션에서는 전혀 그렇지 않지만 10k 인스턴스가 실행되는 데이터 센터가 있고 그중 5k가 기록되지 않으면 문제가 있습니다.
내가 볼 수있는 한, 클래스는 스레드로부터 안전하지 않아 실패로 이어 지거나 FileHandler동일한 파일에 대해 a 가 두 번 생성되어 로그 데이터가 손상 될 수 있습니다.
저장된 정보에 대한 액세스를 동기화 할시기에 대한 개념이 필요합니다. 가장 쉬운 방법은에서 만든 래퍼를 사용하여 동기화 된 목록을 사용하는 것 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");