前言
日志对于一个系统的重要性不言而喻。运行时调试,线上问题排查。java经过多年的发展,SLF4J框架已经占据了半边江山了。
SLF4J概述
SLF4J(Simple logging Facade for Java),意思为简单日志门面,它是把不同的日志系统的实现进行了具体的抽象化,只提供了统一的日志使用接口,使用时只需要按照其提供的接口方法进行调用即可,由于它只是一个接口,并不是一个具体的可以直接单独使用的日志框架,所以最终日志的格式、记录级别、输出方式等都要通过接口绑定的具体的日志系统来实现,这些具体的日志系统就有log4j,logback,java.util.logging等,它们才实现了具体的日志系统的功能。
slf4j绑定原理
slf4j-api是如何绑定通过接口绑定的具体的日志系统来实现的呢?先从以下代码查看
//使用slf4j的常成代码
org.slf4j.Logger LOG = LoggerFactory.getLogger("some-logger-name");
//根据LoggerFactory.getLogger深入
//LoggerFactory::getILoggerFactory
//LoggerFactory::performInitialization
//LoggerFactory::bind
private final static void bind() {
//查询路径下所有的org/slf4j/impl/StaticLoggerBinder.class
Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
//选择路径下的第一个StaticLoggerBinder进行加载
StaticLoggerBinder.getSingleton();
}
可见slf4j-api利用《类加载机制》来绑定具体的日志系统实现,我们只要简单的类就可以轻松绑定
package org.slf4j.impl;
import org.slf4j.ILoggerFactory;
import org.slf4j.spi.LoggerFactoryBinder;
public class StaticLoggerBinder implements LoggerFactoryBinder {
public static StaticLoggerBinder getSingleton(){
return new StaticLoggerBinder();
}
@Override
public ILoggerFactory getLoggerFactory() {
return null;
}
@Override
public String getLoggerFactoryClassStr() {
return "yoyoyo";
}
}
Logbak原理
logback由三个部分组成,logback-core, logback-classic, logback-access。其中logback-core是其他两个模块的基础。logback一般不能单独使用需要和slf4j配合使用,slf4j定义日志接口,具体实现由logback提供。
logback加载原理
根据slf4j绑定原理,在logback-classic包中发现StaticLoggerBinder
// slf4j会在classpath中寻找org/slf4j/impl/StaticLoggerBinder.class
// 静态调用SINGLETON.init()
// 上下文初始化器 new ContextInitializer(defaultLoggerContext).autoConfig()
public void autoConfig() throws JoranException {
// logback也会在classpath中寻找配置文件,
// 先找logback.configurationFile
// 没有则找logback.groovy,
// 若logback.groovy也没有,则找logback-test.xml,
// 若logback-test.xml还是没有,则找logback.xml
URL url = findURLOfDefaultConfigurationFile(true);
if (url != null) {
configureByResource(url);
} else {
//加载环境中的配置
Configurator c = EnvUtil.loadFromServiceLoader(Configurator.class);
if (c != null) {
try {
c.setContext(loggerContext);
c.configure(loggerContext);
} catch (Exception e) {
throw new LogbackException(String.format("Failed to initialize Configurator: %s using ServiceLoader", c != null ? c.getClass()
.getCanonicalName() : "null"), e);
}
} else {
// 没有找到配置文件,则使用默认的配置器,那么日志只会打印在控制台
BasicConfigurator basicConfigurator = new BasicConfigurator();
basicConfigurator.setContext(loggerContext);
basicConfigurator.configure(loggerContext);
}
}
}
logback 支持采用 xml、grovy 和 SPI 的方式配置文件后面分析下xml 文件配置的方式。logback 依赖于 Joran(一个成熟的,灵活的并且强大的配置框架 ),本质上是采用 SAX 方式解析 XML。
1. AppenderAction解析appender标签,将appender放入appenderBag(hashmap)
2. LoggerAction解析logger标签,根据name创建logger对象,并建立父子关系
3. AppenderRefAction解析appender-ref标签,将appender绑定logger
其中logger的父子级创建如下所示
//LoggerAction.begin
//loggerContext.getLogger(loggerName)
//1. 如果获取的是 root logger,直接返回;
//2. 如果获取的是 loggerCache 中缓存的 logger,直接返回;
//3. 循环获取 logger name 中包含的所有 logger,如果不存在就创建并放入缓存;
//4. 返回 logger name 对应的 logger。
public final Logger getLogger(final String name) {
// 如果获取的是root logger,直接返回
if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {
return root;
}
int i = 0;
Logger logger = root;
// 在loggerCache中缓存着已经创建的logger,如果存在,直接返回
Logger childLogger = (Logger) loggerCache.get(name);
if (childLogger != null) {
return childLogger;
}
// 如果还找不到,就需要创建
// 注意,要获取以cn.zzs.logback.LogbackTest为名的logger,名为cn、cn.zzs、cn.zzs.logback的logger不存在的话也会被创建
// 例 cn.zzs 的父亲是 cn
// cn的父亲是root
String childName;
while (true) {
// 从起始位置i开始,获取“.”的位置
int h = LoggerNameUtil.getSeparatorIndexOf(name, i);
// 截取logger的名字
if (h == -1) {
childName = name;
} else {
childName = name.substring(0, h);
}
// 修改起始位置,以获取下一个“.”的位置
i = h + 1;
synchronized (logger) {
// 判断当前logger是否存在以childName命名的子级
childLogger = logger.getChildByName(childName);
if (childLogger == null) {
// 通过当前logger来创建以childName命名的子级
childLogger = logger.createChildByName(childName);
// 放入缓存
loggerCache.put(childName, childLogger);
// logger总数量+1
incSize();
}
}
// 当前logger修改为子级logger
logger = childLogger;
// 如果当前logger是最后一个,则跳出循环
if (h == -1) {
return childLogger;
}
}
}
logback日志输出
代码中关于日志的使用非常简单
//如果配置文件中"com.LogTest",通过缓存就可以拿到配置文件中的logger
//如果不存在,使用默认配置创建,com.LogTest-> com -> root的logger
1. Logger logger = LoggerFactory.getLogger("com.LogTest");
//使用logger打印日志
2. logger.debug("yoyo");
//logger.debug
//Logger.filterAndLog_0_Or3Plus
//主要功能是过滤日志,发现日想等级<logger想要的等级,我就过滤
private void filterAndLog_0_Or3Plus(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,
final Throwable t) {
// 使用TurboFilter过滤当前日志,判断是否通过
final FilterReply decision = loggerContext.getTurboFilterChainDecision_0_3OrMore(marker, this, level, msg, params, t);
// 返回NEUTRAL表示没有TurboFilter,即无需过滤
if (decision == FilterReply.NEUTRAL) {
// 如果需要打印日志的等级小于有效日志等级,则直接返回
if (effectiveLevelInt > level.levelInt) {
return;
}
} else if (decision == FilterReply.DENY) {
// 如果不通过,则不打印日志,直接返回
return;
}
// 创建LoggingEvent
buildLoggingEventAndAppend(localFQCN, marker, level, msg, params, t);
}
//logger.buildLoggingEventAndAppend
//Logger.callAppenders
//additive = false 向父级Appender传递
public void callAppenders(ILoggingEvent event) {
int writes = 0;
// 通知LoggingEvent给当前logger的持有的和继承的appender处理日志事件
for (Logger l = this; l != null; l = l.parent) {
writes += l.appendLoopOnAppenders(event);
// 如果设置了logger的additivity=false,则不会继续查找父级的appender
// 如果没有设置,则会一直查找到root logger
if (!l.additive) {
break;
}
}
// 当前logger未设置appender,在控制台打印提醒
if (writes == 0) {
loggerContext.noAppenderDefinedWarning(this);
}
}