文章目錄
情景
之所以想寫這篇文章是因爲經常看到一些相關聯的問題:
- 怎麼有這麼多非本項目的log出現? 譬如引入了其他的sdk,他們又很無節操的打了很多日誌。
- 怎麼去除不必要的包的日誌?
- 爲什麼logger的名字能對應到指定的package或者類呢?
以上這些問題都是因爲沒搞明白,log4j是怎麼去獲取logger的,本文將通過 slf4j-log4j-impl 根據class去查找logger的過程來解答上述的疑問。
解析
從例子說起
- 打印日誌的例子
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HelloWorld {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(HelloWorld.class);
logger.info("Hello World");
}
}
- 深入到LoggerFactory.getLogger方法
public static Logger getLogger(Class<?> clazz) {
Logger logger = getLogger(clazz.getName());
if (DETECT_LOGGER_NAME_MISMATCH) {
//省略部分打印信息的代碼...
}
return logger;
}
這個DETECT_LOGGER_NAME_MISMATCH是什麼意思呢,就是第一步中的參數並不是當前這個類的時候, 如果這個配置爲true,則會打印一條類似下面的信息:
SLF4J: Detected logger name mismatch. Given name: "com.keven.demos.log.TestNameMismatch"; computed name: "com.keven.demos.log.AsyncLoggerDemo".
SLF4J: See http://www.slf4j.org/codes.html#loggerNameMismatch for an explanation
從LoggerFactory獲取logger
- 我們接着看getLogger方法:
//此處變成通過class的全名(package name + class name)獲取logger
public static Logger getLogger(Class<?> clazz) {
Logger logger = getLogger(clazz.getName());
//省略非關鍵部分代碼...
}
//這裏就是說通過slf4j的實現類的loggerFactory獲取logger
//如何獲取實現類的logger factory,這個在之前的文章中已經分析過了,有興趣的同學可以看看
//https://blog.csdn.net/sweetyi/article/details/104633321
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
從LoggerContext中獲取logger
- 在這裏ILoggerFactory的實現類是:org.apache.logging.slf4j.Log4jLoggerFactory, 這個工廠類中沒有getLogger方法,它會調用父類AbstractLoggerAdapter的getLogger方法,如下:
public L getLogger(final String name) {
//獲取logger配置的上下文,這裏不展開
final LoggerContext context = getContext();
//這裏是做一層logger的緩存
final ConcurrentMap<String, L> loggers = getLoggersInContext(context);
//根據名字獲取logger
final L logger = loggers.get(name);
if (logger != null) {
return logger;
}
//沒找到現成的logger,重新生成一個
loggers.putIfAbsent(name, newLogger(name, context));
return loggers.get(name);
}
- getLoggersInContext方法是用來獲取context所對應的logger的內存緩存, 以下省略了用讀寫鎖加解鎖的代碼,不是重點。
/**
* A map to store loggers for their given LoggerContexts.
* context可能有多個,所需要找出的是對應context的緩存
*/
protected final Map<LoggerContext, ConcurrentMap<String, L>> registry = new WeakHashMap<>();
public ConcurrentMap<String, L> getLoggersInContext(final LoggerContext context) {
ConcurrentMap<String, L> loggers;
//爲了方便閱讀分析,省略加鎖代碼...
loggers = registry.get (context);
//爲了方便閱讀分析,省略解鎖代碼...
if (loggers != null) {
return loggers;
} else {
//爲了方便閱讀分析,省略加鎖代碼...
loggers = registry.get (context);
if (loggers == null) {
loggers = new ConcurrentHashMap<> ();
registry.put (context, loggers);
}
return loggers;
//爲了方便閱讀分析,省略解鎖代碼...
}
}
- 主要是講對logger做了緩存,那麼緩存的logger是怎麼生成的呢?入口從Log4jLoggerFactory.newLogger說起
@Override
protected Logger newLogger(final String name, final LoggerContext context) {
final String key = Logger.ROOT_LOGGER_NAME.equals(name) ? LogManager.ROOT_LOGGER_NAME : name;
//Log4jLogger是爲了適配slf4j的接口做的一層適配,這裏運用到了適配器的設計模式
//這裏的newLogger方法也就是對context的getLogger包裝了一層
return new Log4jLogger(context.getLogger(key), name);
}
//LoggerContext的getLogger方法
public Logger getLogger(final String name) {
return getLogger(name, null);
}
//LoggerContext的getLogger方法
public Logger getLogger(final String name, final MessageFactory messageFactory) {
//loggerRegistry的主要目的是對messageFactory, 和logger做了一層內存緩存,是這樣一個結構:Map<factoryName, Map<classname, logger>>
//這裏就不做展開了
Logger logger = loggerRegistry.getLogger(name, messageFactory);
if (logger != null) {
AbstractLogger.checkMessageFactory(logger, messageFactory);
return logger;
}
//重點生成logger的實例
logger = newInstance(this, name, messageFactory);
loggerRegistry.putIfAbsent(name, messageFactory, logger);
return loggerRegistry.getLogger(name, messageFactory);
}
創建Logger
- 接上面newInstance方法:
protected Logger newInstance(final LoggerContext ctx, final String name, final MessageFactory messageFactory) {
return new Logger(ctx, name, messageFactory);
}
protected Logger(final LoggerContext context, final String name, final MessageFactory messageFactory) {
super(name, messageFactory);
this.context = context;
privateConfig = new PrivateConfig(context.getConfiguration(), this);
}
真正的主角LoggerConfig
- 你以爲故事到這裏就結束了麼?我們看到,創建Logger的時候,創建了一個PrivateConfig,這個類的構造方法如下:你會發現它其實就是對LoggerConfig的一層包裝
public PrivateConfig(final Configuration config, final Logger logger) {
this.config = config;
this.loggerConfig = config.getLoggerConfig(getName());
this.loggerConfigLevel = this.loggerConfig.getLevel();
this.intLevel = this.loggerConfigLevel.intLevel();
this.logger = logger;
}
- 那麼我們就看看怎麼獲取LoggerConfig呢?
public LoggerConfig getLoggerConfig(final String loggerName) {
//根據類的全名獲取LoggerConfig
LoggerConfig loggerConfig = loggerConfigs.get(loggerName);
if (loggerConfig != null) {
return loggerConfig;
}
String substr = loggerName;
//每次去掉名字中.後面的單詞,然後嘗試從已有的LoggerConfigs中獲取config,也就是說可能根據包名找到LoggerConfig
while ((substr = NameUtil.getSubName(substr)) != null) {
loggerConfig = loggerConfigs.get(substr);
if (loggerConfig != null) {
return loggerConfig;
}
}
//如果前面兩種方法都沒有找到就返回RootLoggerConfig
return root;
}
獲取LoggerConfig流程圖
驗證
上面我們說到LoggerConfig纔是真正的主角,我們看看打印日誌是個什麼調用鏈, 中間會省略部分過程
ps: ReliabilityStrategy只是對LoggerConfig的一層包裝
小結
代碼解析完了,開篇的問題解答一下
- 怎麼有這麼多非本項目的log出現? 譬如引入了其他的sdk,他們又很無節操的打了很多日誌。
==》因爲設置了RootLogger,它是所有的Logger的默認配置 - 怎麼去除不必要的包的日誌?指定logger的名字爲想要過去的包的名字,但不設置相應的appender
<Logger name="your.package.name" level="${level}" additivity="false" >
</Logger>
- 爲什麼logger的名字能對應到指定的package或者類呢?
這個就是本文的內容了