【日志技术】8 - 1 JUL
§8-1 JUL
8-1.1 日志概述
8-1.1.1 日志技术
日志文件是用于记录系统操作事件的文件集合,分为事件日志和消息日志。具有处理历史数据、诊断问题追踪以及理解系统活动等重要作用。
在计算机中,日志文件是记录操作系统或其他软件运行中发生的事件,或在通信软件的不同用户之间的消息的文件。
日志标准:目前广泛使用的日志记录标准为 RFC 5424 中所定义的 syslog。该标准使专用的标准化子系统能够生成、过滤、记录和分析日志消息。
调试日志:调试日志常用于软件开发阶段,通常用于记录程序运行状况、查看参数传递、变量值以及程序的控制流程。将调试日志输出到控制台,便于开发者观看、管理,解决开发过程中遇到的问题。通常在项目上线前关闭调试日志。
系统日志:系统日志是应用的重要组成部分,记录系统中硬件、软件和系统问题的信息,同时还可以监视系统中发生的事件。可以通过查看日志,快速定位和检查错误发生的原因。系统日志包括系统日志、应用程序日志和安全日志。许多操作系统、软件框架和应用程序包括日志系统。
开发的项目在实际生产中是要脱离开发环境运行的,但是项目一旦脱离开发环境,出现故障,应当如何快速定位和排除就成了重要问题。断点调试几乎不可行,因此,有必要通过日志技术,详细记载程序运行时所执行的操作,为故障排除提供有用信息。
相较于使用输出语句打印日志,使用日志技术的优点有:
- 可以将系统执行的信息,方便地记录到指定位置(控制台、文件、数据库);
- 可以随时以开关的形式控制日志启停,无需修改源代码;
- 在多线程环境中,日志技术的性能较好;
8-1.1.2 日志框架
当今,软件系统已经发展得十分复杂,在不同领域中所涉及的知识、内容、问题很多,使用别人已经设计好的框架,就可以简化在某一方面的工作,开发者得以专注地完成业务逻辑设计。
日志框架可以用于:
- 控制日志输出的内容和格式;
- 控制日志输出位置;
- 日志优化(异步日志、日志文件压缩和归档);
- 日志系统维护;
- 面向接口开发——日志的门面;
日志框架基于一套日志规范实现,日志规范提供了一套日志实现框架设计的标准。日志框架需要实现这些接口,常见的日志接口有:
基于这些日志规范接口,就可以设计出一套日志实现框架。常见的日志实现框架有:
- JUL(
java.util.logging
); - Apache Log4j 2;
- Logback(基于 SLF4j 实现);
- 其他实现;
由于对 Commons Logging 的接口不满意,诞生了 SLF4j。因为对 Log4j 的性能不满意,诞生了 Logback。这里我们使用 Logback 日志框架。
8-1.2 JUL 入门
JUL 是 Java 原生的日志框架,不需要导入额外的第三方库使用。相对其他日志框架使用方便,学习简单,能在小型应用中灵活使用。
JUL 的架构:
- Logger:日志记录器,应用程序获取
Logger
对象,调用其 API 发布日志消息。Logger 通常是应用程序访问日志系统的入口程序。 - Appender:也称为 Handler,处理器。每一个
Logger
都关联着一组Handler
,Logger
会将日志交给关联的Handler
处理,由Handler
负责记录日志。Handler
是一个抽象类,其具体实现决定了日志记录的位置可以是控制台、文件、网络上的其他日志服务或操作系统日志等。 - Layout:布局,也被称为 Formatter,它负责对日志事件中的数据进行转换和格式化。
Layout
决定了数据在一条日志记录中的最终形式。 - Filter:过滤器。根据规则定制哪些信息被记录,哪些信息被放过。
示例:使用 JUL 记录日志
package com.logging;
import java.util.logging.Level;
import java.util.logging.Logger;
public class JULDemo {
// 1. 获取日志对象:日志对象是程序访问日志系统的入口,有且只有一个
// 每一个日志记录器都需要有一个唯一的标识,通常为当前类的全限定类名
public static final Logger LOGGER = Logger.getLogger("com.logging.JULDemo");
public static void main(String[] args) {
// 使用 JUL 记录日志:适用于小型应用程序
// 2. 输出日志记录
LOGGER.info("hello jul");
// 通用方法记录日志
LOGGER.log(Level.INFO, "hello jul"); // 同 logger.info(String);
// 使用占位符输出变量
String name = "zhangsan";
int age = 23;
// Level 是一个类,其枚举常量定义了不同的日志级别
LOGGER.log(Level.INFO, "用户信息:{0}, {1}", new Object[]{name, age});
}
}
getLogger(String)
需要传入一个唯一标识该日志记录器的全限定名。这样,日志记录器所记录的日志就会携带该全限定名信息,有利于在查看日志时,定位产生日志的类与包。
此外,同一个全限定名所返回的日志记录器实例都是唯一的(单例模式),这种设计保证了资源的有效管理,并允许为每个命名的日志记录器共享和维护相同的日志级别、输出目的地等配置信息。
8-1.3 JUL 中的日志级别
追踪 java.util.logging.Level
类,可以看到它定义了以下日志级别(降序排列):
- SEVERE:严重(最高级别),表示严重故障;
- WARNING:警告,表示潜在问题;
- INFO:信息;
- CONFIG:配置,用于静态配置消息;
- FINE:用于 DEBUG 级别日志输出,提供详细追踪信息;
- FINER:用于颗粒度更细的 DEBUG 级别日志输出,提供较详细追踪信息;
- FINEST:用于最低颗粒度的 DEBUG 级别日志输出(最低级别),提供非常详细的追踪信息。
此外,Level
中还定义了 OFF
用于关闭日志记录,ALL
用于记录全部日志。默认情况下,JUL 的日志记录级别为 INFO
。一般而言,DEBUG 级别的日志记录从三者选其一即可,一般选 FINE
。
示例:调用不同级别的日志记录方法,记录不同级别的日志。
package com.logging;
import java.util.logging.Logger;
public class LevelDemo {
public static void main(String args[]) {
Logger logger = Logger.getLogger("com.logging.LevelDemo");
logger.severe("severe");
logger.warning("warning");
logger.info("info");
logger.config("config");
logger.fine("fine");
logger.finer("finer");
logger.finest("finest");
}
}
此外,JUL 还支持自定义日志级别,下列程序描述了使用 JUL 自定义日志级别的方法。
示例:自定义日志级别、日志输出。
// 自定义日志级别和日志输出
private static void logConfigTest() throws IOException {
// 1. 获取日志记录器对象
Logger logger = Logger.getLogger("com.logging.JULDemo");
// 2. 关闭系统默认配置
logger.setUseParentHandlers(false);
// 3. 自定义日志配置规则
// 3.1 自定义输出位置:以控制台为例,创建控制台处理器对象
ConsoleHandler consoleHandler = new ConsoleHandler();
// 3.2 自定义布局(格式化器):创建简单格式化器对象
SimpleFormatter simpleFormatter = new SimpleFormatter();
// 3.3 控制台处理器对象关联指定格式化器
consoleHandler.setFormatter(simpleFormatter);
// 3.4 日志记录器关联指定处理器
logger.addHandler(consoleHandler);
// 3.5 也可同时设置输出至文件:创建文件处理器对象
FileHandler fileHandler = new FileHandler("/logs/jul.log");
// 关联
fileHandler.setFormatter(simpleFormatter);
logger.addHandler(fileHandler);
// 4. 配置日志级别
logger.setLevel(Level.ALL);
consoleHandler.setLevel(Level.ALL);
fileHandler.setLevel(Level.ALL);
// 日志输出
logger.severe("severe");
logger.warning("warning");
logger.info("info");
logger.config("config");
logger.fine("fine");
logger.finer("finer");
logger.finest("finest");
}
8-1.4 Logger
对象的父子关系
调用 Logger.gerLogger(String)
方法时,方法要求我们传入全限定名。该全限定名既可以指向类,也可以指向软件包。
当一个程序中基于不同的全限定名所指向的实例,获得了不同的 Logger
对象,若这两个全限定名所指实例具有父子关系,则:
Logger
对象具有和所指定的包/类相同的层级关系:位于父包中的全限定名实例所获得的Logger
对象一定是位于子包的实例的Logger
对象的父级Logger
对象,可调用getParent()
方法获取父级Logger
对象;- 子
Logger
对象会继承父Logger
对象的配置:子Logger
可以调用setUseParentHandler(boolean)
方法启用或禁用父级Logger
的处理器设置; Logger
对象的顶级父对象为LogManager$RootLogger
:顶级父Logger
对象指向控制台输出,默认输出级别为info
,使用简单格式化器。该Logger
对象的名字为""
(getName() = ""
)。
示例:查看 Logger
对象的父子关系。
package com.logging;
import java.util.logging.Logger;
public class LoggerParentDemo {
public static void main(String args[]) {
// 获取当前类的日志记录器
Logger logger1 = Logger.getLogger("com.logging.JULDemo");
// 获取当前类所在包的日志记录器
Logger logger2 = Logger.getLogger("com.logging");
// 获取顶级日志记录器
Logger rootLogger = Logger.getLogger("com").getParent();
// 判断父子关系:
System.out.println(logger2 == logger1.getParent()); // true
// 查看顶级日志记录器信息
// Root Logger: java.util.logging.LogManager$RootLogger, name:
System.out.println("Root Logger: " + rootLogger + ", name: " + rootLogger.getName());
}
}
8-1.5 JUL 配置文件
若每次使用 JUL 记录日志时,都需要手动调用方法重新调整配置是相当繁琐的一件事。注意到 LogManager$RootManager
中具有默认的配置,这些配置从何而来?
8-1.5.1 默认配置文件
详细追踪并找到配置文件的流程见下文追踪 JUL 执行原理部分。这里通过追踪方法运行发现一个名为 logging.properties
的文件。根日志记录器使用该配置文件的配置信息以初始化。在本地搜索该文件,发现它位于 %JAVA_HOME%/conf
。复制一份到项目中,得到下文内容:
############################################################
# Default Logging Configuration File
#
# You can use a different file by specifying a filename
# with the java.util.logging.config.file system property.
# For example, java -Djava.util.logging.config.file=myfile
############################################################
############################################################
# Global properties
############################################################
# "handlers" specifies a comma-separated list of log Handler
# classes. These handlers will be installed during VM startup.
# Note that these classes must be on the system classpath.
# By default we only configure a ConsoleHandler, which will only
# show messages at the INFO and above levels.
handlers= java.util.logging.ConsoleHandler
# To also add the FileHandler, use the following line instead.
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler
# Default global logging level.
# This specifies which kinds of events are logged across
# all loggers. For any given facility this global level
# can be overridden by a facility-specific level
# Note that the ConsoleHandler also has a separate level
# setting to limit messages printed to the console.
.level= INFO
############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################
# default file output is in user's home directory.
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
# Default number of locks FileHandler can obtain synchronously.
# This specifies maximum number of attempts to obtain lock file by FileHandler
# implemented by incrementing the unique field %u as per FileHandler API documentation.
java.util.logging.FileHandler.maxLocks = 100
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
# Limit the messages that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# Example to customize the SimpleFormatter output format
# to print one-line log message like this:
# <level>: <log message> [<date/time>]
#
# java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
############################################################
# Facility-specific properties.
# Provides extra control for each logger.
############################################################
# For example, set the com.xyz.foo logger to only log SEVERE
# messages:
# com.xyz.foo.level = SEVERE
配置文件中的可配置内容来自于 JUL 中对应的类,必要时可追踪对应的类以查看自定义内容的范围和含义。此处省略。
8-1.5.2 导入自定义配置文件
示例:使用类加载器获取资源的流,并通过 LogManager
读取配置。
private static void loggingPropertiesTest() throws IOException {
// 使用类加载器获取配置文件的输入流
InputStream is = JULDemo.class.getClassLoader().getResourceAsStream("JavaSE\\files\\logs\\logging.properties");
// 获取 LogManager,并使用 LogManager 读取配置
LogManager.getLogManager().readConfiguration(is);
}
8-1.6 JUL 执行流程
接下来将简单地概括 JUL 的执行流程。
- 初始化
LogManager
:加载logging.properties
配置文件,添加Logger
到LogManager
中; - 从单例
LogManager
获取Logger
对象; - 设置级别
Level
,并指定日志记录LogRecord
; LogRecord
经过过滤器Filter
过滤,提供比日志级别更细粒度的控制;- 过滤放行的
LogRecord
由Handler
处理输出位置; Formatter
格式化LogRecord
,最终输出日志。
8-1.7 JUL 执行原理
这一套流程如何在代码中体现呢?下面开始逐步追踪代码运行。
8-1.7.1 获取日志记录器对象示例
以下列主程序代码为例:
package com.logging;
import java.util.logging.*;
public class LoggingDemo {
public static void main(String args[]) {
Logger logger = Logger.getLogger("com.logging.LoggingDemo");
logger.info("info");
}
}
调用 getLogger(String)
方法获取 Logger
对象,追踪该方法:
@CallerSensitive
public static Logger getLogger(String name) {
return Logger.getLogger(name, Reflection.getCallerClass());
}
继续追踪:
private static Logger getLogger(String name, Class<?> callerClass) {
return demandLogger(name, null, callerClass);
}
继续:
private static Logger demandLogger(String name, String resourceBundleName, Class<?> caller) {
// 首先获取 LoggerManager 对象
LogManager manager = LogManager.getLogManager();
if (!SystemLoggerHelper.disableCallerCheck) {
if (isSystem(caller.getModule())) {
return manager.demandSystemLogger(name, resourceBundleName, caller);
}
}
// 自定义 Logger 对象
return manager.demandLogger(name, resourceBundleName, caller);
}
方法有两个调用语句值得注意。第一个,每次调用 getLogger(String)
获取日志记录器时,首先都会先获取 LogManager
对象。前文已经提到,这是一个单例对象,通过静态方法获取它的对象。第二个,return
语句返回一个自定义的 Logger
对象。
8-1.7.2 初始化 LoggerManager
按照方法执行顺序,先来看看 LogManager
的初始化流程。
public static LogManager getLogManager() {
if (manager != null) {
manager.ensureLogManagerInitialized();
}
return manager;
}
程序必然会经历一次 LogManager
的初始化,追踪方法:
@SuppressWarnings("removal")
final void ensureLogManagerInitialized() {
final LogManager owner = this;
if (initializationDone || owner != manager) {
// 判断初始化是否完成,保证初始化不会二次进行,且不会在私有 manager 实例上运行
return;
}
// 可能其他线程在此之前已经调用 ensureLogManagerInitialized() 且正在执行
// 如果如此,则阻塞至日志管理器完成初始化为止,然后取得对象监视器锁
// 并注意此时 initializationDone 为 true,方法返回
// 否则,我们首先到达这里,获取监视器锁
// 发现 initializationDone 为 false,执行初始化
configurationLock.lock();
try {
// 若 initializedCalled 为 true,表明我们已在当前线程中执行 LogManager 初始化
// 已存在递归调用 ensureLogManagerInitialized()
final boolean isRecursiveInitialization = (initializedCalled == true);
assert initializedCalled || !initializationDone
: "Initialization can't be done if initialized has not been called!";
if (isRecursiveInitialization || initializationDone) {
// 若 isRecursiveInitialization 为 true,表明我们已在当前线程中执行 LogManager 初始化
// 已存在递归调用 ensureLogManagerInitialized()
// 此时不应继续,否则会导致无限递归
//
// 若 initializationDone 为 true,表明管理器已完成初始化,方法返回
return;
}
// 下方调用 addLogger 会调用 requiresDefaultLogger()
// 这又会调用 ensureLogManagerInitialized()
// 使用 initialzedCalled 以打断递归
initializedCalled = true;
try {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
assert rootLogger == null;
assert initializedCalled && !initializationDone;
// 读取原始配置前,创建 root logger
// 确保 root logger 的添加早于全局日志记录器,而不是反过来
final Logger root = owner.rootLogger = owner.new RootLogger();
// 读取配置
owner.readPrimordialConfiguration();
// 创建并保留用于名称空间根的日志记录器
owner.addLogger(root);
// 若未初始化级别,初始化日志级别
if (!root.isLevelInitialized()) {
root.setLevel(defaultLevel);
}
// 添加全局日志记录器
// 不要在此处调用 Logger.getGlobal(),因为这可能会触发
// 难以察觉的相互依赖问题(inter-dependency issues)
@SuppressWarnings("deprecation")
final Logger global = Logger.global;
// 确保全局记录器注册为全局管理器:向上兼容 JDK 1.7 前
owner.addLogger(global);
return null;
}
});
} finally {
initializationDone = true;
}
} finally {
configurationLock.unlock();
}
}
注意到,在进行实际的初始化工作时,方法先创建根日志记录器,然后读取配置文件。接下来看看配置文件的读取流程。
8-1.7.3 读取配置文件
初始化 LogManager
时,会调用 readPrimodialConfiguration()
方法读取配置信息。追踪该方法:
private void readPrimordialConfiguration() { // 必须在持有 configurationLock 时调用
if (!readPrimordialConfiguration) {
// 若 System.in/out/err 为 null
// 表明我们仍然处在启动阶段(boostrapping phase)
if (System.out == null) {
return;
}
readPrimordialConfiguration = true;
try {
readConfiguration();
// 平台日志记录器开始委托给 java.util.logging.Logger
jdk.internal.logger.BootstrapLogger.redirectTemporaryLoggers();
} catch (Exception ex) {
assert false : "Exception raised while reading logging configuration: " + ex;
}
}
}
方法内部真正读取配置文件内容的方法为 readConfiguration()
,追踪它:
public void readConfiguration() throws IOException, SecurityException {
checkPermission();
// 若指定了配置类,加载并读取它
String cname = System.getProperty("java.util.logging.config.class");
if (cname != null) {
try {
// 实例化命名类。
// 该类构造器用于初始化日志配置,通过使用一个合适的流调用 readConfiguration(InputStream)
try {
Class<?> clz = ClassLoader.getSystemClassLoader().loadClass(cname);
@SuppressWarnings("deprecation")
Object witness = clz.newInstance();
return;
} catch (ClassNotFoundException ex) {
Class<?> clz = Thread.currentThread().getContextClassLoader().loadClass(cname);
@SuppressWarnings("deprecation")
Object witness = clz.newInstance();
return;
}
} catch (Exception ex) {
System.err.println("Logging configuration class \"" + cname + "\" failed");
System.err.println("" + ex);
// keep going and useful config file.
}
}
// 否则加载默认配置
String fname = getConfigurationFileName();
try (final InputStream in = new FileInputStream(fname)) {
readConfiguration(in);
}
}
在默认情况,不存在用户定义的配置类,因此会转而读取并加载默认配置。方法首先会获取默认配置文件的名字:
String getConfigurationFileName() throws IOException {
String fname = System.getProperty("java.util.logging.config.file");
if (fname == null) {
fname = System.getProperty("java.home");
if (fname == null) {
throw new Error("Can't find java.home ??");
}
// 从 %JAVA_HOME%/conf 中读取 logging.properties 文件
fname = Paths.get(fname, "conf", "logging.properties")
.toAbsolutePath().normalize().toString();
}
return fname;
}
方法返回,然后调用 readConfiguration(InputStream)
读取配置文件。
8-1.7.4 添加日志记录器
在 ensureLogManagerInitialized()
方法中,创建完 RootLogger
以及读取配置文件后,就会调用 addLogger()
添加根日志记录器。最后,再添加全局记录器。下面来追踪 addLogger(Logger)
方法:
public boolean addLogger(Logger logger) {
final String name = logger.getName();
if (name == null) {
throw new NullPointerException();
}
drainLoggerRefQueueBounded();
LoggerContext cx = getUserContext();
if (cx.addLocalLogger(logger) || forceLoadHandlers(logger)) {
// 是否每个记录器都有自己的处理器?
// 注:这会有 200ms 的性能降低
loadLoggerHandlers(logger, name, name + ".handlers");
return true;
} else {
return false;
}
}
注意到第 7 行中获取了一个 LoggerContext
对象。这是 LogManager
的一个内部类,记录了日志记录器的上下文。
class LoggerContext {
// 线程安全的并发哈希表,记录日志记录器的名称及其对应引用
private final ConcurrentHashMap<String,LoggerWeakRef> namedLoggers =
new ConcurrentHashMap<>();
// 已命名记录器的树
private final LogNode root;
private LoggerContext() {
this.root = new LogNode(null, this);
}
// 方法...
}
LoggerContext
使用线程安全的并发哈希表记录每一个日志记录器及其对应的名称。键类型为 String
,即记录器名称,值类型为 LoggerWeakRef
,记录器弱引用。追踪该泛型类:
final class LoggerWeakRef extends WeakReference<Logger> {
private String name; // 当前记录器结点名称
private LogNode node; // 记录器结点对象
private WeakReference<Logger> parentRef; // 记录器结点的父结点
private boolean disposed = false;
// 方法...
}
每一个日志记录器对象由一个 LogNode
记录器结点包装。注意到 LoggerContext
中使用树存储记录器,树中的每一个结点就是一个记录器结点。追踪该类:
private static class LogNode {
HashMap<String, LogNode> children; // 当前结点的子结点
LoggerWeakRef loggerRef; // 当前结点所表示的记录器
LogNode parent; // 当前结点的父结点
final LoggerContext context; // 日志记录器上下文
LogNode(LogNode parent, LoggerContext context) {
this.parent = parent;
this.context = context;
}
// 方法...
}
这样下来,一个 LogContext
就记录了完整的 Logger
信息。回到 addLogger(Logger)
中,继续执行,先计算if
语句中的表达式。表达式调用了一个方法 addLocalLogger(Logger)
,追踪:
boolean addLocalLogger(Logger logger) {
// no need to add default loggers if it's not required
return addLocalLogger(logger, requiresDefaultLoggers());
}
方法调用了另外一个方法 addLocalLogger(Logger, boolean)
,方法的第二个参数执行一些判断,此处略过,进入方法内部:
synchronized boolean addLocalLogger(Logger logger, boolean addDefaultLoggersIfNeeded) {
if (addDefaultLoggersIfNeeded) {
ensureAllDefaultLoggers(logger);
}
final String name = logger.getName();
if (name == null) {
throw new NullPointerException();
}
LoggerWeakRef ref = namedLoggers.get(name);
if (ref != null) {
if (ref.refersTo(null)) {
ref.dispose();
} else {
return false;
}
}
final LogManager owner = getOwner();
logger.setLogManager(owner);
ref = owner.new LoggerWeakRef(logger);
Level level = owner.getLevelProperty(name + ".level", null);
if (level != null && !logger.isLevelInitialized()) {
doSetLevel(logger, level);
}
processParentHandlers(logger, name, VisitedLoggers.NEVER); // 处理父级处理器
LogNode node = getNode(name);
node.loggerRef = ref;
Logger parent = null;
LogNode nodep = node.parent;
while (nodep != null) {
LoggerWeakRef nodeRef = nodep.loggerRef;
if (nodeRef != null) {
parent = nodeRef.get();
if (parent != null) {
break;
}
}
nodep = nodep.parent;
}
if (parent != null) {
doSetParent(logger, parent);
}
node.walkAndSetParent(logger);
ref.setNode(node);
namedLoggers.put(name, ref); // 最终将记录器添加到 LoggerContext 所记录的并发哈希映射表中
return true;
}
方法会处理配置文件中的信息并将记录器添加到上下文。其中,添加到上下文的方法利用到了并发集合。在这之前有一个方法用于处理父级处理器:processParentHandlers(Logger, String, Predicate<Logger>)
@SuppressWarnings("removal")
private void processParentHandlers(final Logger logger, final String name,
Predicate<Logger> visited) {
final LogManager owner = getOwner();
AccessController.doPrivileged(new PrivilegedAction<Void>() {
@Override
public Void run() {
if (logger != owner.rootLogger) {
// 检查是否使用父级处理器
boolean useParent = owner.getBooleanProperty(name + ".useParentHandlers", true);
if (!useParent) {
logger.setUseParentHandlers(false);
}
}
return null;
}
});
int ix = 1;
for (;;) {
int ix2 = name.indexOf('.', ix);
if (ix2 < 0) {
break;
}
String pname = name.substring(0, ix2);
// 获取级别和处理器信息
if (owner.getProperty(pname + ".level") != null ||
owner.getProperty(pname + ".handlers") != null) {
// This pname has a level/handlers definition.
// Make sure it exists.
if (visited.test(demandLogger(pname, null, null))) {
break;
}
}
ix = ix2+1;
}
}
添加完成后,返回到 addLogger(Logger)
方法继续计算表达式,第二个方法 forceLoadHandlers(logger)
内部只是计算布尔表达式,判断是否强制加载处理器,此处略过。若条件表达式满足,进入分支,调用 loadLoggerHandlers()
方法。方法会检查所添加的记录器是否定义了自身的处理器。若有,则配置自身的处理器,否则跳过,使用父级的处理器。
8-1.7.5 初始化完成,返回日志记录器对象
LogManager
初始化完成后,方法会返回所得到的 LogManager
对象。方法逐步返回至 demandLogger(String, String, Class<?>)
,来到最后的返回语句:
return manager.demandLogger(name, resourceBundleName, caller);
不断向下追踪:
Logger demandLogger(String name, String resourceBundleName, Class<?> caller) {
final Module module = caller == null ? null : caller.getModule();
return demandLogger(name, resourceBundleName, module);
}
继续:
Logger demandLogger(String name, String resourceBundleName, Module module) {
// 从 LogManager 中获取日志记录器(日志记录器的名称就是其唯一标识符,为所在类/包的全限定名)
Logger result = getLogger(name);
if (result == null) {
// 仅为新的记录器分配一次
Logger newLogger = new Logger(name, resourceBundleName,
module, this, false);
do {
if (addLogger(newLogger)) {
// 我们成功添加了新建的记录器,直接返回,无需重新获取
return newLogger;
}
// 我们没能成功添加所新建的记录器,因为在本线程的 null 检查执行完毕后,
// 以及 addLogger() 方法调用前,其他线程可能添加了同名的记录器。
// 我们需要重新获取记录器,因为 addLogger() 返回的是 boolean 而不是 Logger
// 然而,若创建了其他记录器的其他线程不持有指向所创建的记录器的强引用,
// 那么该纪录器会在 addLogger() 后,调用 getLogger() 前被垃圾回收
// 若已被 GC,则循环多几遍反复尝试
result = getLogger(name);
} while (result == null);
}
return result;
}
最终获得记录其对象,方法层层返回,结束。
8-1.7.6 记录日志
获取了 Logger
后,调用 logger.info("info")
记录日志。下面来追踪该方法的执行:
public void info(String msg) {
log(Level.INFO, msg);
}
可见,调用类似方法最后都会在底层调用重载的 log(Level, String)
方法记录日志。继续追踪:
public void log(Level level, String msg) {
// 检查日志级别是否满足要求
// 低于所配置的级别则拒绝操作,方法返回
if (!isLoggable(level)) {
return;
}
// 若日志级别允许被记录,则创建日志记录对象
LogRecord lr = new LogRecord(level, msg);
doLog(lr);
}
若需要记录日志,最终会调用 doLog(LogRecord)
:
private void doLog(LogRecord lr) {
lr.setLoggerName(name);
final LoggerBundle lb = getEffectiveLoggerBundle();
final ResourceBundle bundle = lb.userBundle;
final String ebname = lb.resourceBundleName;
if (ebname != null && bundle != null) {
lr.setResourceBundleName(ebname);
lr.setResourceBundle(bundle);
}
log(lr);
}
方法进行一系列的判断和其他操作后,最终调用 log(LogRecord)
记录日志:
public void log(LogRecord record) {
if (!isLoggable(record.getLevel())) {
return;
}
Filter theFilter = config.filter;
if (theFilter != null && !theFilter.isLoggable(record)) {
return;
}
// Post the LogRecord to all our Handlers, and then to
// our parents' handlers, all the way up the tree.
Logger logger = this;
while (logger != null) {
final Handler[] loggerHandlers = isSystemLogger
? logger.accessCheckedHandlers()
: logger.getHandlers();
for (Handler handler : loggerHandlers) {
handler.publish(record); // 真正发布日志的方法
}
final boolean useParentHdls = isSystemLogger
? logger.config.useParentHandlers
: logger.getUseParentHandlers();
if (!useParentHdls) {
break;
}
logger = isSystemLogger ? logger.parent : logger.getParent();
}
}
这个日志发布的方法声明于接口 Handler
中。使用默认配置情况下,JUL 会调用 ConsoleHandler
发布日志:
@Override
public void publish(LogRecord record) {
super.publish(record);
flush();
}
可见,该方法先调用父类的日志发布方法后刷新流,将日志成功输出。下面追踪父类的方法(来自于 StreamHandler
):
@Override
public void publish(LogRecord record) {
if (tryUseLock()) {
try {
publish0(record);
} finally {
unlock();
}
} else {
synchronized (this) {
publish0(record);
}
}
}
继续追踪 publish0(LogRecord)
:
private void publish0(LogRecord record) {
if (!isLoggable(record)) {
return;
}
String msg;
try {
msg = getFormatter().format(record); // 使用 Formatter 格式化
} catch (Exception ex) {
// We don't want to throw an exception here, but we
// report the exception to any registered ErrorManager.
reportError(null, ex, ErrorManager.FORMAT_FAILURE);
return;
}
try {
if (!doneHeader) {
writer.write(getFormatter().getHead(this));
doneHeader = true;
}
writer.write(msg); // 字符流写出
} catch (Exception ex) {
// We don't want to throw an exception here, but we
// report the exception to any registered ErrorManager.
reportError(null, ex, ErrorManager.WRITE_FAILURE);
}
}