文章目录
- 日志原理简述
- JUL
- JUL定义的日志级别(Level类)
- 关于构造函数中defaultBundle的解释
- Level中的静态内部类KnownLevel的功能
- LogManager类的用法
- LogManager对象的初始化
- LogManager对象配置的装载
- LogManager中的LoggerContext
- LogManager中的LogNode
- LogManager中的RootLogger
- Logger类的用法
- LoggerBundle
- Logger记录日志
- Filter过滤日志
日志原理简述
日志功能的实现基本靠一下几个组件来完成:
- Loggers:Logger负责捕捉事件并将其发送给合适的Appender
- Appenders:也称为Handlers,负责从Logger中取出日志消息,并使用Layout来格式化消息,然后将消息发送出去,比如发送到控制台、文件或其他日志收集系统。
- Layouts:也称为Formatters,负责对日志事件进中的数据进行转换和格式化。
- Filters:过滤器,根据需要定制哪些信息会被记录,哪些信息会被放过。
总结一下就是:用户使用Logger来进行日志记录,Logger持有若干个Handler,日志的输出操作是由Handler完成的。在Handler在输出日志前,会经过Filter的过滤,判断代码是否可以继续执行,Filter返回false,日志方法return,Handler不会处理;Filter返回true,则继续向下执行,Handler会将日志内容输出到指定位置(日志文件、控制台等)。Handler在输出日志时会使用Layout,将输出内容进行排版。
JUL
JUL是JDK自带的一个日志实现,java.util.logging包下的各个类提供了原生的日志记录和输出支持。
JUL定义的日志级别(Level类)
要了解JUL的日志级别就要看一下java.util.logging.Level这个类,具体的日志级别都定义在了代码里。
// 用于关闭日志功能
public static final Level OFF = new Level("OFF",Integer.MAX_VALUE, defaultBundle);
// 用于表明程序严重的失败或错误
public static final Level SEVERE = new Level("SEVERE",1000, defaultBundle);
// 用于表明潜在的问题
public static final Level WARNING = new Level("WARNING", 900, defaultBundle);
// 信息级别,通过消息会被输入到控制台,所以信息级别一般记录对最终用户或系统管理员有用的信息
public static final Level INFO = new Level("INFO", 800, defaultBundle);
// 配置级别,用于表明系统静态配置的信息
public static final Level CONFIG = new Level("CONFIG", 700, defaultBundle);
// 用于提供跟踪信息的消息级别
public static final Level FINE = new Level("FINE", 500, defaultBundle);
// 用于提供跟踪信息的消息级别
public static final Level FINER = new Level("FINER", 400, defaultBundle);
// 用于提供跟踪信息的消息级别
public static final Level FINEST = new Level("FINEST", 300, defaultBundle);
// 记录所有级别的日志
public static final Level ALL = new Level("ALL", Integer.MIN_VALUE, defaultBundle);
JUL的Level提供了三个跟踪信息的级别,FINE、FINER、FINEST的确切含义在不同子系统之间变化,但通常用FINEST记录大量最详细的日志信息,用FINER记录记录相比于FINEST精简的日志信息,FINEST记录最精简,也是最重要的日志信息。
关于构造函数中defaultBundle的解释
// 默认资源包位置
private static final String defaultBundle = "sun.util.logging.resources.logging";
// 用于本地化级别名称的资源包名称
private final String resourceBundleName;
resourceBundleName是用来指定外部资源包的,如果不指定,会默认用defaultBundle指定的资源包,资源包是干嘛的呢,我在rt.jar包下找到了这个资源。
package sun.util.logging.resources;
import java.util.ListResourceBundle;
public final class logging extends ListResourceBundle {
public logging() {
}
protected final Object[][] getContents() {
return new Object[][]{{"ALL", "All"}, {"CONFIG", "Config"}, {"FINE", "Fine"}, {"FINER", "Finer"}, {"FINEST", "Finest"}, {"INFO", "Info"}, {"OFF", "Off"}, {"SEVERE", "Severe"}, {"WARNING", "Warning"}};
}
}
原来就是控制台输出日志时,定义本地化后的级别名称。或许你会有疑问,明明在控制台上看到的是警告、信息等中文的Level,这实际上是SimpleFormatter进行的处理。
public synchronized String format(LogRecord record) {
// 省略方法前半部分的代码
return String.format(format,
dat,
source,
record.getLoggerName(),
record.getLevel().getLocalizedLevelName(),
message,
throwable);
}
在进行格式化的时候,传入了record.getLevel().getLocalizedLevelName(),我们再看Level类的该方法,打断点可以看到具体本地化的过程。
final synchronized String getLocalizedLevelName() {
// 先拿缓存的locale值
final String cachedLocalizedName = getCachedLocalizedLevelName();
if (cachedLocalizedName != null) {
return cachedLocalizedName;
}
// 如果没有缓存的locale取电脑默认的Loacle
final Locale newLocale = Locale.getDefault();
try {
// 用locale去处理defaultBundle = "sun.util.logging.resources.logging"中的英文级别
localizedLevelName = computeLocalizedLevelName(newLocale);
} catch (Exception ex) {
localizedLevelName = name;
}
cachedLocale = newLocale;
return localizedLevelName;
}
除了上面这些内容,Level类还包含了一个静态内部类KnownLevel和两个transient修饰的属性。
// 用transient修饰,在序列化时会忽略这两个属性
// 用来缓存通过本地locale翻译的Level级别名称
private transient String localizedLevelName;
// 用来缓存本地Locale对象
private transient Locale cachedLocale;
Level中的静态内部类KnownLevel的功能
knownlevel类维护所有已知级别的全局列表。api允许同一名称/值的多个自定义级别实例创建(根据nameToLevels、intToLevels两个Map对象也可以看出同一个key对应多个value)。这个类提供了方便的方法来查找级别,例如:通过给定的名称、给定的值或给定的本地化名称。
static final class KnownLevel {
private static Map<String, List<KnownLevel>> nameToLevels = new HashMap<>();
private static Map<Integer, List<KnownLevel>> intToLevels = new HashMap<>();
// 用于存放标准的Level对象或客户自定义Level的对象
final Level levelObject;
// 表示日志配置中指定级别的级别对象。在match方法中用到
final Level mirroredLevel;
KnownLevel(Level l) {
this.levelObject = l;
if (l.getClass() == Level.class) {
this.mirroredLevel = l;
} else {
// this mirrored level object is hidden
this.mirroredLevel = new Level(l.name, l.value, l.resourceBundleName, false);
}
}
// 将新建的Level加到nameToLevels、intToLevels Map中进行缓存
static synchronized void add(Level l) {
// the mirroredLevel object is always added to the list
// before the custom Level instance
KnownLevel o = new KnownLevel(l);
List<KnownLevel> list = nameToLevels.get(l.name);
if (list == null) {
list = new ArrayList<>();
nameToLevels.put(l.name, list);
}
list.add(o);
list = intToLevels.get(l.value);
if (list == null) {
list = new ArrayList<>();
intToLevels.put(l.value, list);
}
list.add(o);
}
.....
// 省略了通过给定的名称、给定的值或给定的本地化名称三个方法的代码
......
// 判断一个给定的Level是否是被KnownLevel管理的Level
// 这个方法用在Level类的readResolve方法中,防止序列化反序列化得到没有被KnownLevel管理的Level
static synchronized KnownLevel matches(Level l) {
List<KnownLevel> list = nameToLevels.get(l.name);
if (list != null) {
for (KnownLevel level : list) {
Level other = level.mirroredLevel;
Class<? extends Level> type = level.levelObject.getClass();
if (l.value == other.value &&
(l.resourceBundleName == other.resourceBundleName ||
(l.resourceBundleName != null &&
l.resourceBundleName.equals(other.resourceBundleName)))) {
if (type == l.getClass()) {
return level;
}
}
}
}
return null;
}
}
LogManager类的用法
LogManager的作用是读取配置文件和管理Logger实例。LogManager类初始化时默认使用JVM系统属性java.util.logging.manager定义的类路径来加载配置类,配置LogManager,但如果没有找到,就new一个空对象。
LogManager对象的初始化
// The global LogManager object
private static final LogManager manager;
// 类加载时的逻辑,对全局的manager初始化
static {
manager = AccessController.doPrivileged(new PrivilegedAction<LogManager>() {
@Override
public LogManager run() {
LogManager mgr = null;
String cname = null;
try {
// DEBUG时这个值是空的,说明没有设置该属性时,该属性为空
cname = System.getProperty("java.util.logging.manager");
if (cname != null) {
try {
Class<?> clz = ClassLoader.getSystemClassLoader()
.loadClass(cname);
mgr = (LogManager) clz.newInstance();
} catch (ClassNotFoundException ex) {
Class<?> clz = Thread.currentThread()
.getContextClassLoader().loadClass(cname);
mgr = (LogManager) clz.newInstance();
}
}
} catch (Exception ex) {
System.err.println("Could not load Logmanager \"" + cname + "\"");
ex.printStackTrace();
}
if (mgr == null) {
mgr = new LogManager();
}
// 最终返回的是一个没有设置任何内容的LogManager对象
return mgr;
}
});
}
现在类初始化完成,全局的LogManager对象manager不为null了,但是这个什么属性都没设置的manager并没有什么用啊,所以LogManager类提供了一个ensureLogManagerInitialized方法,确保LogManager对象被初始化,在getLogManager方法返回LogManager对象之前被调用。
// 确保返回的LogManager是被初始化过的
public static LogManager getLogManager() {
if (manager != null) {
manager.ensureLogManagerInitialized();
}
return manager;
}
// ensureLogManagerInitialized方法主要做了两件事儿
// 1.读取基本的配置 2.创建rootLogger
final void ensureLogManagerInitialized() {
......
// 省略部分代码
initializedCalled = true;
try {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
assert rootLogger == null;
assert initializedCalled && !initializationDone;
// 读取基本配置
owner.readPrimordialConfiguration();
// 创建一个RootLogger
owner.rootLogger = owner.new RootLogger();
owner.addLogger(owner.rootLogger);
if (!owner.rootLogger.isLevelInitialized()) {
owner.rootLogger.setLevel(defaultLevel);
}
// 添加Logger.global
// 并且也特意说明了Logger.global不要通过Logger.getGlobal方法来获取
// 因为getGlobal方法内调用的是LogManager.getLogManager 这样就循环调用了
@SuppressWarnings("deprecation")
final Logger global = Logger.global;
// 确保global被注册到了全局LogManager对象中
// owner就是全局LogManager对象
owner.addLogger(global);
return null;
}
});
} finally {
initializationDone = true;
}
}
LogManager对象配置的装载
在初始化时,有一行readPrimordialConfiguration方法的调用,点进去后,发现最终读取配置文件是LogManager.readConfiguration方法实现的。
public void readConfiguration() throws IOException, SecurityException {
checkPermission();
// 如果指定配配置类,就拿配置类的属性
String cname = System.getProperty("java.util.logging.config.class");
if (cname != null) {
try {
// 实例化这个配置类,配置类的构造方法中,应该调用LogManager.readConfiguration来为 LogManager全局对象添加配置
try {
Class<?> clz = ClassLoader.getSystemClassLoader().loadClass(cname);
clz.newInstance();
return;
} catch (ClassNotFoundException ex) {
Class<?> clz = Thread.currentThread().getContextClassLoader().loadClass(cname);
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 = System.getProperty("java.util.logging.config.file");
if (fname == null) {
// JRE安装路径
fname = System.getProperty("java.home");
if (fname == null) {
throw new Error("Can't find java.home ??");
}
File f = new File(fname, "lib");
// 可以看出最终默认使用的是logging.propertie文件来配置全局LogManager对象
f = new File(f, "logging.properties");
fname = f.getCanonicalPath();
}
try (final InputStream in = new FileInputStream(fname)) {
final BufferedInputStream bin = new BufferedInputStream(in);
readConfiguration(bin);
}
}
LogManager中的LoggerContext
LoggerContext为每个context的Logger提供命名空间。默认的LogManager对象有一个系统上下文SystemLoggerContext和一个用户上下文LoggerContext。系统上下文用于维护所有系统Logger的命名空间,并由系统代码查询。如果系统Logger不存在于用户上下文中,它也将被添加到用户上下文中。用户代码查询用户上下文,并在用户上下文中添加所有Logger。
// LoggerContext for system loggers and user loggers
private final LoggerContext systemContext = new SystemLoggerContext();
private final LoggerContext userContext = new LoggerContext();
我们发现getSystemContext这个方法在demandSystemLogger方法中被引用到了,而后者正是Logger.demandLogger方法所调用的。
private static Logger demandLogger(String name, String resourceBundleName, Class<?> caller) {
LogManager manager = LogManager.getLogManager();
SecurityManager sm = System.getSecurityManager();
if (sm != null && !SystemLoggerHelper.disableCallerCheck) {
if (caller.getClassLoader() == null) {
// 获取系统Logger
return manager.demandSystemLogger(name, resourceBundleName);
}
}
// 获取用户Logger
return manager.demandLogger(name, resourceBundleName, caller);
}
不管是获取用户Logger还是系统Logger,都会调用LogManage的addLogger方法。这个方法在注释上就明确说明了,如果同名的Logger存在,则返回false,否则将注册一个新的Logger,并返回true。
public boolean addLogger(Logger logger) {
final String name = logger.getName();
if (name == null) {
throw new NullPointerException();
}
drainLoggerRefQueueBounded();
LoggerContext cx = getUserContext();
// 从这行可以看出,系统Logger也一定会被加入到用户LoggerContext中去
if (cx.addLocalLogger(logger)) {
// Do we have a per logger handler too?
// Note: this will add a 200ms penalty
loadLoggerHandlers(logger, name, name + ".handlers");
return true;
} else {
return false;
}
}
不管是获取系统Logger还是用户Logger,他们的方法中都有这一段代码,这也让我们明白了LoggerContext的作用,不就是提供了一个键值对,缓存了现有的所有Logger对象嘛。
if (addLogger(sysLogger)) {
// successfully added the new system logger
logger = sysLogger;
} else {
logger = getLogger(name);
}
public Logger getLogger(String name) {
return getUserContext().findLogger(name);
}
LogManager中的LogNode
LogNode维护了Logger对象之间的父子关系,在创建一个Logger对象时,LogManager的addLocalLogger方法会为新的Logger分配节点,并维护上父Logger。
LogManager中的RootLogger
RootLogger类的注释说:用一个Logger的子类作为rootLogger,让我们只在第一次需要全局handler时初始化。
LogManager提供了一个私有的方法对全局的Handlers进行初始化
private synchronized void initializeGlobalHandlers() {
if (initializedGlobalHandlers) {
return;
}
initializedGlobalHandlers = true;
if (deathImminent) {
// Aaargh...
// The VM is shutting down and our exit hook has been called.
// Avoid allocating global handlers.
return;
}
loadLoggerHandlers(rootLogger, null, "handlers");
}
说了半天全局Handlers,那么Handlers到底是什么,我们再看loadLoggerHandlers方法的实现,以及调用的parseClassNames方法,最终发现就是从Logmanager.props属性中获取"handlers"属性。也就是说,全局Handlers就是我们在配置文件中配置的handlers属性,去翻看配置文件,一定会发现有配置这个属性。
Logger类的用法
Logger类将需要记录的信息封装成一个对象,并且还封装了信息的级别,日志输出的实现,实际上是靠Handler实现的。
LoggerBundle
Logger提供了一个LoggerBundle类,这个类只有两个属性,一个是资源的名字resourceBundleName,另一个是资源对象userBundle。LoggerBundle用在日志记录时,会将这两个属性设置给LogRecord对象。LoggerBundle的构造器时private,也就是说不能被外部代码new出来,只能通过其提供的get()方法得到该对象。Logger类默认提供了两个LoggerBundle对象。
static final String SYSTEM_LOGGER_RB_NAME = "sun.util.logging.resources.logging";
// This instance will be shared by all loggers created by the system code
private static final LoggerBundle SYSTEM_BUNDLE =
new LoggerBundle(SYSTEM_LOGGER_RB_NAME, null);
// This instance indicates that no resource bundle has been specified yet,
// and it will be shared by all loggers which have no resource bundle.
private static final LoggerBundle NO_RESOURCE_BUNDLE =
new LoggerBundle(null, null);
根据SYSTEM_LOGGER_RB_NAME的值,我找到了指向的类,之前在介绍日志输出时也提到过这个类。
public final class logging extends ListResourceBundle {
public logging() {
}
protected final Object[][] getContents() {
return new Object[][]{{"ALL", "All"}, {"CONFIG", "Config"}, {"FINE", "Fine"}, {"FINER", "Finer"}, {"FINEST", "Finest"}, {"INFO", "Info"}, {"OFF", "Off"}, {"SEVERE", "Severe"}, {"WARNING", "Warning"}};
}
}
Logger记录日志
Logger提供了log(Level level, String msg)方法供用户调用来记录日志,先通过isLoggable方法,判断当前Logger对象的日志功能是否关闭或当前Logger对象的设定级别是否小于要记录的级别。比如说:Logger设定为INFO级别,当要记录WARNING和SERVER级别时,就要会返回false,不再向下执行。
public void log(Level level, String msg) {
if (!isLoggable(level)) {
return;
}
LogRecord lr = new LogRecord(level, msg);
doLog(lr);
}
public boolean isLoggable(Level level) {
if (level.intValue() < levelValue || levelValue == offValue) {
return false;
}
return true;
}
Filter过滤日志
JDK提供了一个filter接口来定制日志的过滤功能,只要实现isLoggable接口,在进行日志输出前,会判断是否进行输出。
public interface Filter {
public boolean isLoggable(LogRecord record);
}
public void log(LogRecord record) {
if (!isLoggable(record.getLevel())) {
return;
}
Filter theFilter = filter;
// 如果Logger对象有通过setFilter设置过滤器,在这里会进行判断是否输出日志
if (theFilter != null && !theFilter.isLoggable(record)) {
return;
}
// 省略后面的代码
......
// 从这里可以看出真正实现日志输出到控制台或文件是靠handler来实现的
for (Handler handler : loggerHandlers) {
handler.publish(record);
}
// 省略后面的代码
......
}