log4j2(一) 获取 ILoggerFactory
关于log4j2的初始化流程,现在项目基本都是springboot项目,就需要结合 springboot 源码来解析,这块可以参考Springboot - Log4J2LoggingSystem源码解析
一. Springboot项目集成log4j2
因为spring-boot-starter依赖中日志使用的是spring-boot-starter-logging,这里面是用的是logback,所以需要先剔除此依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
再添加log4j2依赖
<!-- log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
如果我们想使用 yml 后缀的配置文件还需要再加一个依赖
<!-- 识别 yaml/yml 文件 -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
然后相关依赖版本如下
log4j - version
以下源码基于 log4j 2.7。
二. Log4j2 的组成
先看一张log4j官方提供的类图
先简单了解一下这些类
-
org.apache.logging.log4j.Logger.javaLogger
接口是配置文件中 Logger / Root / AsyncLogger / AsyncRoot 标签的抽象,每一个Logger标签对应一个 Logger 对象
-
LoggerConfig
用于描述 Logger,每个 Logger 都应该对应一个 LoggerConfig,其中存放当前 Logger 的详细信息,比如日志级别、输出Appender、配置的Filter和最重要的命名。 -
LoggerContext
日志上下文,在代码中用的变量是anchor,是锚点的意思,用于管理所有的Logger,需要注意的是,一个项目中可以有多个日志上下文。 -
org.apache.logging.log4j.core.config.Configuration.javaConfiguration
日志配置接口抽象,每一个LoggerContext对应一个Configuration,包含相关上下文中所有的Logger、Appender和Filter。在重配置期间,新与旧的Configuration将同时存在。当所有的Logger对象都被重定向到新的Configuration对象后,旧的Configuration对象将被停用和丢弃。
-
Appender
日志具体输出,常见的有控制台输出ConsoleAppender、文件输出FileAppender、异步输出AsyncAppender。其他详见log4j - Appender -
Filter
过滤器,一条日志信息可以抽象成LogEvent,每个LogEvent对象到达目的地之前都会经过很多过滤器,根据其结果来判断下一步如何处理。可以在四个地方配置过滤:
Log4j allows a filter to be specified in any of 4 places:
- At the same level as the appenders, loggers and properties elements. These filters can accept or reject events before they have been passed to a LoggerConfig.
- In a logger element. These filters can accept or reject events for specific loggers.
- In an appender element. These filters can prevent or cause events to be processed by the appender.
- In an appender reference element. These filters are used to determine if a Logger should route the event to an appender.
过滤器的种类也很多,比如根据日志级别。Filter会返回一个枚举值Filter.Result,有三种:
- ACCEPT(接受然后直接输出)
- NEUTRAL(传递给下一个过滤器处理)
- DENY(拒绝并结束过滤处理流程)
/**
* The event will be processed without further filtering based on the log Level.
*/
ACCEPT,
/**
* No decision could be made, further filtering should occur.
*/
NEUTRAL,
/**
* The event should not be processed.
*/
DENY;
详细参见log4j - Filters
三. 初始化
log4j的初始化流程实在有点饶,这里简要讲讲。
1. ILoggerFactory的获取
LoggerFactory#getLogger(String)
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
在只有一个log4j2的依赖时,加载流程大致如下:
log4j2 - getILoggerFactory
在getILoggerFactory()
方法中做了这么几件事
- 查找类
org/slf4j/impl/StaticLoggerBinder.class
所在jar包,返回包名+类全路径名集合
// We need to use the name of the StaticLoggerBinder class, but we can't
// reference
// the class itself.
private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
static Set<URL> findPossibleStaticLoggerBinderPathSet() {
// use Set instead of list in order to deal with bug #138
// LinkedHashSet appropriate here because it preserves insertion order
// during iteration
Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
try {
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration<URL> paths;
if (loggerFactoryClassLoader == null) {
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}
while (paths.hasMoreElements()) {
URL path = paths.nextElement();
staticLoggerBinderPathSet.add(path);
}
} catch (IOException ioe) {
Util.report("Error getting resources from path", ioe);
}
return staticLoggerBinderPathSet;
}
- 然后如果存在多个还会打印警告信息,LoggerFactory#reportMultipleBindingAmbiguity 打印具体路径与Jar包
private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
Util.report("Class path contains multiple SLF4J bindings.");
for (URL path : binderPathSet) {
Util.report("Found binding in [" + path + "]");
}
Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
}
}
- 通过StaticLoggerBinder类对Log4jLoggerFactory进行初始化,Log4jLoggerFactory是StaticLoggerBinder的一个静态字段。
- 如果存在多个StaticLoggerBinder.class的包,则打印信息表明当前选用的是哪个日志框架,这里就是org.apache.logging.slf4j.Log4jLoggerFactory
LoggerFactory#reportActualBinding 打印实际绑定日志框架
private static void reportActualBinding(Set<URL> binderPathSet) {
// binderPathSet can be null under Android
if (binderPathSet != null && isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
Util.report("Actual binding is of type [" + StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr() + "]");
}
}
- 返回Log4jLoggerFactory
再看下 log4j2 的 StaticLoggerBinder 源码
package org.slf4j.impl;
import org.apache.logging.slf4j.Log4jLoggerFactory;
import org.slf4j.ILoggerFactory;
import org.slf4j.spi.LoggerFactoryBinder;
public final class StaticLoggerBinder implements LoggerFactoryBinder {
public static String REQUESTED_API_VERSION = "1.6";
private static final String LOGGER_FACTORY_CLASS_STR = Log4jLoggerFactory.class.getName();
private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
private final ILoggerFactory loggerFactory = new Log4jLoggerFactory();
private StaticLoggerBinder() {
}
public static StaticLoggerBinder getSingleton() {
return SINGLETON;
}
public ILoggerFactory getLoggerFactory() {
return this.loggerFactory;
}
public String getLoggerFactoryClassStr() {
return LOGGER_FACTORY_CLASS_STR;
}
}
- 此StaticLoggerBinder属于
log4j-slf4j-impl-2.7.jar
包 - 单例实现
- 默认ILoggerFactory实现是Log4jLoggerFactory
下一篇看看后续Logger的获取。
讨论
1. 多个 org/slf4j/impl/StaticLoggerBinder.class
存在时如何选择
添加 logback 依赖(直接放开spring-boot-starter-logging
也行),这样我们项目就有两个 org/slf4j/impl/StaticLoggerBinder.class
了
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
然后启动,提示
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/F:/repository/ch/qos/logback/logback-classic/1.1.11/logback-classic-1.1.11.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/F:/repository/org/apache/logging/log4j/log4j-slf4j-impl/2.7/log4j-slf4j-impl-2.7.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]
- 两个路径
[jar:file:/F:/repository/ch/qos/logback/logback-classic/1.1.11/logback-classic-1.1.11.jar!/org/slf4j/impl/StaticLoggerBinder.class]
/[jar:file:/F:/repository/org/apache/logging/log4j/log4j-slf4j-impl/2.7/log4j-slf4j-impl-2.7.jar!/org/slf4j/impl/StaticLoggerBinder.class]
均存在 StaticLoggerBinder 类 - 实际使用了
[ch.qos.logback.classic.util.ContextSelectorStaticBinder]
,也就是使用了logback作为当前项目的日志框架 - 有疑问可以查看 http://www.slf4j.org/codes.html#multiple_bindings 寻求解释
我的疑问就是为啥不用log4j2而用logback,就打开这个网址查了下,里面最后有一段
The warning emitted by SLF4J is just that, a warning. Even when multiple bindings are present, SLF4J will pick one logging framework/implementation and bind with it. The way SLF4J picks a binding is determined by the JVM and for all practical purposes should be considered random. As of version 1.6.6, SLF4J will name the framework/implementation class it is actually bound to.
告诉我们
- 警告只是警告,并不会导致项目起不来
- 这种多日志框架的情况下,SLF4J 选取日志框架的方式取决于JVM,并且你可以认为这是随机选取的
- 1.6.6版本后,SLF4J 会命名(?)其实际选取的日志框架
好吧,随机的我也是醉了。。
参考
http://logging.apache.org/log4j/2.x/log4j-users-guide.pdf
http://www.slf4j.org/codes.html#multiple_bindings
回家,风雨兼程。