log4j2(一) 获取 ILoggerFactory

2020-04-19  本文已影响0人  sunyelw

关于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官方提供的类图


先简单了解一下这些类

  1. Logger 接口是配置文件中 Logger / Root / AsyncLogger / AsyncRoot 标签的抽象,每一个Logger标签对应一个 Logger 对象

    org.apache.logging.log4j.Logger.java
  2. LoggerConfig 用于描述 Logger,每个 Logger 都应该对应一个 LoggerConfig,其中存放当前 Logger 的详细信息,比如日志级别、输出Appender、配置的Filter和最重要的命名。

  3. LoggerContext 日志上下文,在代码中用的变量是anchor,是锚点的意思,用于管理所有的Logger,需要注意的是,一个项目中可以有多个日志上下文。

  4. Configuration 日志配置接口抽象,每一个LoggerContext对应一个Configuration,包含相关上下文中所有的Logger、Appender和Filter。在重配置期间,新与旧的Configuration将同时存在。当所有的Logger对象都被重定向到新的Configuration对象后,旧的Configuration对象将被停用和丢弃。

    org.apache.logging.log4j.core.config.Configuration.java
  5. Appender 日志具体输出,常见的有控制台输出ConsoleAppender、文件输出FileAppender、异步输出AsyncAppender。其他详见log4j - Appender

  6. Filter 过滤器,一条日志信息可以抽象成LogEvent,每个LogEvent对象到达目的地之前都会经过很多过滤器,根据其结果来判断下一步如何处理。可以在四个地方配置过滤:

Log4j allows a filter to be specified in any of 4 places:

  1. 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.
  2. In a logger element. These filters can accept or reject events for specific loggers.
  3. In an appender element. These filters can prevent or cause events to be processed by the appender.
  4. In an appender reference element. These filters are used to determine if a Logger should route the event to an appender.

过滤器的种类也很多,比如根据日志级别。Filter会返回一个枚举值Filter.Result,有三种:

/**
 * 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()方法中做了这么几件事

// 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;
}
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.");
    }
}
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() + "]");
    }
}

再看下 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;
    }
}

下一篇看看后续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]

我的疑问就是为啥不用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.

告诉我们

  1. 警告只是警告,并不会导致项目起不来
  2. 这种多日志框架的情况下,SLF4J 选取日志框架的方式取决于JVM,并且你可以认为这是随机选取的
  3. 1.6.6版本后,SLF4J 会命名(?)其实际选取的日志框架

好吧,随机的我也是醉了。。

参考
http://logging.apache.org/log4j/2.x/log4j-users-guide.pdf
http://www.slf4j.org/codes.html#multiple_bindings


回家,风雨兼程。

上一篇下一篇

猜你喜欢

热点阅读