루트 패키지 당 파일로 Java 로깅

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

그러나 다른 모든 클래스의 경우 각 클래스에는 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);
}

답변

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

변수 이름이 잘못되었습니다. 정규화 된 이름이 아니라 정식 이름입니다. 그들은 같을 수도 있고 같지 않을 수도 있습니다.


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다음 전달 된 클래스를 기반으로 경로 / 파일을 생성하는 것이 훨씬 좋습니다 . 그것은 훨씬 덜 취약하고 결정적 일 것입니다.

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