【问题标题】:Printing thread name using java.util.logging使用 java.util.logging 打印线程名称
【发布时间】:2011-10-16 20:56:39
【问题描述】:

java.util.logging.Logger生成的日志语句中是否可以打印线程名?

另一种方法是执行以下操作:

logger.info(thread.getName() + " some useful info");

但它是重复的,日志框架应该处理它。

【问题讨论】:

  • 我相信,使用 log4j 或 slf4j 会比答案中建议的解决方案更干净。 :)

标签: java logging


【解决方案1】:

很尴尬,但看起来java.util.logging 不能这样做......

默认的java.util.logging.SimpleFormatter 根本无法记录线程名称。 java.util.logging.FileHandler 支持的模板占位符很少,它们都不是线程名称。

java.util.logging.XMLFormatter 是最接近的,但只记录线程 ID:

<record>
  <date>2011-07-31T13:15:32</date>
  <millis>1312110932680</millis>
  <sequence>0</sequence>
  <logger></logger>
  <level>INFO</level>
  <class>java.util.logging.LogManager$RootLogger</class>
  <method>log</method>
  <thread>10</thread>
  <message>Test</message>
</record>

如果您认为我们正在接近 - 我们不是。 LogRecord 类只保存线程 ID,而不是它的名称 - 不是很有用。

【讨论】:

  • SimpleFormatter确实不使用线程ID。但是it's easy to define 是一个自定义格式化程序并通过线程 ID 获取线程名称。不需要尴尬:-)
【解决方案2】:

带有自定义Formatter

幸运的是,LogRecord 包含产生日志消息的线程的 ID。我们可以在编写自定义Formatter 时获取此LogRecord。一旦我们有了它,我们只需要通过它的 ID 来获取线程名称。

a couple of ways可以得到那个ID对应的Thread对象,这里是我的:

static Optional<Thread> getThread(long threadId) {
    return Thread.getAllStackTraces().keySet().stream()
            .filter(t -> t.getId() == threadId)
            .findFirst();
}

以下是最小的Formatter,只打印线程名称和日志消息:

private static Formatter getMinimalFormatter() {
    return new Formatter() {

        @Override
        public String format(LogRecord record) {

            int threadId = record.getThreadID();
            String threadName = getThread(threadId)
                    .map(Thread::getName)
                    .orElseGet(() -> "Thread with ID " + threadId);

            return threadName + ": " + record.getMessage() + "\n";
        }
    };
}

要使用您的自定义格式化程序,还有different options,一种方法是修改默认的ConsoleHandler

public static void main(final String... args) {

    getDefaultConsoleHandler().ifPresentOrElse(
            consoleHandler -> consoleHandler.setFormatter(getMinimalFormatter()),
            () -> System.err.println("Could not get default ConsoleHandler"));

    Logger log = Logger.getLogger(MyClass.class.getName());
    log.info("Hello from the main thread");
    SwingUtilities.invokeLater(() -> log.info("Hello from the event dispatch thread"));
}

static Optional<Handler> getDefaultConsoleHandler() {
    // All the loggers inherit configuration from the root logger. See:
    // https://docs.oracle.com/javase/8/docs/technotes/guides/logging/overview.html#a1.3
    var rootLogger = Logger.getLogger("")
    // The root logger's first handler is the default ConsoleHandler
    return first(Arrays.asList(rootLogger.getHandlers()));
}

static <T> Optional<T> first(List<T> list) {
    return list.isEmpty() ?
            Optional.empty() :
            Optional.ofNullable(list.get(0));
}

然后,您的最小 Formatter 应该会生成以下包含线程名称的日志消息:

main:来自主线程的问候

AWT-EventQueue-0:来自事件调度线程的问候


这是一个Formatter,它显示了如何记录线程名称和日志消息以外的内容:

private static Formatter getCustomFormatter() {
    return new Formatter() {

        @Override
        public String format(LogRecord record) {

            var dateTime = ZonedDateTime.ofInstant(record.getInstant(), ZoneId.systemDefault());

            int threadId = record.getThreadID();
            String threadName = getThread(threadId)
                    .map(Thread::getName)
                    .orElse("Thread with ID " + threadId);

            // See also: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Formatter.html
            var formatString = "%1$tF %1$tT %2$-7s [%3$s] %4$s.%5$s: %6$s %n%7$s";

            return String.format(
                    formatString,
                    dateTime,
                    record.getLevel().getName(),
                    threadName,
                    record.getSourceClassName(),
                    record.getSourceMethodName(),
                    record.getMessage(),
                    stackTraceToString(record)
            );
        }
    };
}

private static String stackTraceToString(LogRecord record) {
    final String throwableAsString;
    if (record.getThrown() != null) {
        var stringWriter = new StringWriter();
        var printWriter = new PrintWriter(stringWriter);
        printWriter.println();
        record.getThrown().printStackTrace(printWriter);
        printWriter.close();
        throwableAsString = stringWriter.toString();
    } else {
        throwableAsString = "";
    }
    return throwableAsString;
}

Formatter 产生如下日志消息:

2019-04-27 13:21:01 INFO [AWT-EventQueue-0] package.ClassName.method:日志消息

【讨论】:

    【解决方案3】:

    一些应用程序服务器隐式记录线程 ID(我知道 WebSphere)。您可以创建自己的LogFormatter。传递给格式化程序的记录包含线程 ID,请参阅here。我曾多次为 Tomcat 实现过这种方法,但它也适用于 Java SE 环境。

    顺便说一句:线程名称不适用于 LogRecord。

    【讨论】:

      【解决方案4】:

      java.util.logging 有许多奇怪的特性。您可以添加外观 API 来调整其行为

      public class Log
      
          Logger logger;
      
          static public Log of(Class clazz)
              return new Log( Logger.getLogger( clazz.getName() ));
      
          public void error(Throwable thrown, String msg, Object... params)
          {
              log(ERROR, thrown, msg, params);
          }
      
          void log(Level level, Throwable thrown, String msg, Object... params)
          {
              if( !logger.isLoggable(level) ) return;
      
              // bolt on thread name somewhere
              LogRecord record = new LogRecord(...);
              record.setXxx(...);
              ...
              logger.log(record);
          }
      
      ----
      
      static final Log log = Log.of(Foo.class);
      ....
      log.error(...);
      

      人们使用 java 的日志记录主要是因为他们不想拥有 3rd 方依赖项。这也是为什么他们不能依赖现有的日志记录门面,如 apache 或 slf4j。

      【讨论】:

        【解决方案5】:

        我有类似的问题。正如这里所回答的How to align log messages using java.util.logging,您可以扩展java.util.logging.Formatter,但获取LogRecord#getThreadID(),您可以通过调用Thread.currentThread().getName() 来获取线程名称,如下所示:

        public class MyLogFormatter extends Formatter
        {
        
            private static final MessageFormat messageFormat = new MessageFormat("[{3,date,hh:mm:ss} {2} {0} {5}]{4} \n");
        
            public MyLogFormatter()
            {
                super();
            }
        
            @Override
            public String format(LogRecord record)
            {
                Object[] arguments = new Object[6];
                arguments[0] = record.getLoggerName();
                arguments[1] = record.getLevel();
                arguments[2] = Thread.currentThread().getName();
                arguments[3] = new Date(record.getMillis());
                arguments[4] = record.getMessage();
                arguments[5] = record.getSourceMethodName();
                return messageFormat.format(arguments);
            }
        
        }
        

        【讨论】:

        • 这不会放入调用格式化程序的线程的线程名称,而不是创建日志条目的线程的线程名称吗?我想它们在某些情况下可能是相同的,但不能保证。
        【解决方案6】:

        在前面的答案中提到,LogRecord 只有 ThreadID 信息,你必须遍历线程列表才能获取线程名称。 令人惊讶的是,在某些情况下,在 Logger 记录消息时线程可能并不活跃。

        我的建议是编写一个 Wrapper,它允许您将线程名称与消息本身一起发送。

        package com.sun.experiments.java.logging;
        
            import java.util.logging.Level;
            
            public class ThreadLogger {
            
                public static void main(String[] args) {
                    Logger log = Logger.getLogger(ThreadLogger.class.getName());//Invokes the static method of the below Logger class
                    log.log(Level.INFO, "Logging main message");
                    new Thread(()-> {Logger.getLogger(ThreadLogger.class.getName());log.log(Level.INFO, "Logging thread message");}).start();
                }
            
                public static class Logger{
                    private final java.util.logging.Logger log;
                    private Logger() {  log = null;}//Shouldn't use this. The log is initialized to null and thus it will generate Null Pointer exception when accessing methods using it.
                    private Logger(String name) {
                        log = java.util.logging.Logger.getLogger(name);
                    }   
                    private static Logger getLogger(String name) {
                        return new Logger(name);
                    }
                    public void log(Level level,String message)
                    {
                        message = "["+Thread.currentThread().getName()+"]: "+message;
                        log.log(level,message);
                    }
                    public void log(Level level,String message,Throwable e)
                    {
                        message = "["+Thread.currentThread().getName()+"]: "+message;
                        log.log(level,message,e);
                    }
                }
            }
        

        输出将是:

        Jul 16, 2020 12:06:24 PM com.sun.experiments.java.logging.ThreadLogger$Logger log
        INFO: [main]:     Logging main message
        Jul 16, 2020 12:06:24 PM com.sun.experiments.java.logging.ThreadLogger$Logger log
        INFO: [Thread-1]: Logging thread message
        

        我没有扩展整个 Logger 类,而是包装它,因为我不想覆盖所有方法。我只需要几个方法。

        【讨论】:

        【解决方案7】:

        上面的几个答案表明 LogRecord.getThreadId() 返回一个有意义的线程 ID,而我们所缺少的只是一种将其与线程名称相关联的方法。

        不幸的是,LogRecord.getThreadId() 返回的 int 值与引发日志消息的线程的长 id 不对应。

        所以我们不能只使用 ManagementFactory.getThreadMXBean() 来解析线程名称。它会产生随机的线程名称。

        如果您确定您的日志记录工具始终在与调用者相同的线程中格式化,那么您可以按照上面的建议创建一个自定义格式化程序,并调用 Thread.currentThread().getName()。

        似乎 Logging 门面或第三方库是唯一完全安全的选择。

        【讨论】:

        • LogRecord.getThreadID() 确实返回一个有意义的线程 ID。您可以使用 Thread.getAllStackTraces 等将 ID 与 Thread 对象相关联。
        【解决方案8】:

        补充@l245c4l 答案:不要使用 SimpleFormatter() 使用:

        //fileHandler.setFormatter(new SimpleFormatter());
        
        class MyFormatter extends Formatter {
            private final MessageFormat messageFormat = new MessageFormat("{0,date}, {0,time} {1} {2}: {3} [T:{4}] {5}\n");
        
            public String format(LogRecord record)
            {
                Object[] arguments = new Object[6];
                arguments[0] = new Date( record.getMillis() );
                arguments[1] = record.getSourceClassName();
                arguments[2] = record.getSourceMethodName();
                arguments[3] = record.getLevel();
                arguments[4] = Long.toString( Thread.currentThread().getId() );
                arguments[5] = record.getMessage();
        
                return messageFormat.format(arguments);
            }
        }
        
        fileHandler.setFormatter( new MyFormatter() ); 
        
        Logger myLogger = Logger.getLogger("<LOGGER_NAME>");
        myLogger.addHandler(fileHandler);
        

        其中T:{4} 是线程ID(参数4)。

        【讨论】:

        • Thread.currentThread() 不一定与日志消息起源的线程相同。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-08-26
        相关资源
        最近更新 更多