【发布时间】:2011-10-16 20:56:39
【问题描述】:
java.util.logging.Logger生成的日志语句中是否可以打印线程名?
另一种方法是执行以下操作:
logger.info(thread.getName() + " some useful info");
但它是重复的,日志框架应该处理它。
【问题讨论】:
-
我相信,使用 log4j 或 slf4j 会比答案中建议的解决方案更干净。 :)
java.util.logging.Logger生成的日志语句中是否可以打印线程名?
另一种方法是执行以下操作:
logger.info(thread.getName() + " some useful info");
但它是重复的,日志框架应该处理它。
【问题讨论】:
很尴尬,但看起来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 获取线程名称。不需要尴尬:-)
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:日志消息
【讨论】:
一些应用程序服务器隐式记录线程 ID(我知道 WebSphere)。您可以创建自己的LogFormatter。传递给格式化程序的记录包含线程 ID,请参阅here。我曾多次为 Tomcat 实现过这种方法,但它也适用于 Java SE 环境。
顺便说一句:线程名称不适用于 LogRecord。
【讨论】:
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。
【讨论】:
我有类似的问题。正如这里所回答的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);
}
}
【讨论】:
在前面的答案中提到,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 类,而是包装它,因为我不想覆盖所有方法。我只需要几个方法。
【讨论】:
上面的几个答案表明 LogRecord.getThreadId() 返回一个有意义的线程 ID,而我们所缺少的只是一种将其与线程名称相关联的方法。
不幸的是,LogRecord.getThreadId() 返回的 int 值与引发日志消息的线程的长 id 不对应。
所以我们不能只使用 ManagementFactory.getThreadMXBean() 来解析线程名称。它会产生随机的线程名称。
如果您确定您的日志记录工具始终在与调用者相同的线程中格式化,那么您可以按照上面的建议创建一个自定义格式化程序,并调用 Thread.currentThread().getName()。
似乎 Logging 门面或第三方库是唯一完全安全的选择。
【讨论】:
LogRecord.getThreadID() 确实返回一个有意义的线程 ID。您可以使用 Thread.getAllStackTraces 等将 ID 与 Thread 对象相关联。
补充@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() 不一定与日志消息起源的线程相同。