掘金 后端 ( ) • 2024-04-10 09:42

theme: smartblue

log4j2自动配置

  1. log4j2在程序启动时会检查系统配置项log4j2.configurationFile指定的文件,如果此配置项被设置,则优先找到对应的ConfigurationFactory去解析加载此配置项指定的配置文件,支持多个文件逗号分隔
  2. 如果1中的系统配置项没有被指定,则按下面优先级顺序加载。首先寻找classpath下的log4j2-test.properties文件
  3. 如果没有找到这样的文件,就寻找classpath下的log4j2-test.yml或者log4j2-test.yaml文件
  4. 如果没有找到这样的文件,就寻找classpath下的log4j2-test.json或者log4j2-test.jsn文件
  5. 如果没有找到这样的文件,就寻找classpath下的log4j2-test.xml文件
  6. 如果上述test文件没有找到,则尝试寻找classpath下的log4j2.yml或者log4j2.properties文件
  7. 如果没有找到这样的文件,就尝试寻找classpath下的log4j2.yml或者log4j2.yaml文件
  8. 寻找classpath下的log4j2.json或者log4j2.jsn文件
  9. 寻找classpath下的log4j2.xml文件
  10. 如果上述配置文件都无法找到,则将使用DefaultConfiguration。 这将导致日志输出转到控制台。

这也就是为什么我们之间在resources下新建一个log4j2.xml文件就能被识别到的原因。因为log4j2可以自动探测到。

具体源码可以参考:

ConfigurationFactory.Factory#getConfiguration(LoggerContext, String, URI);

log4j2较log4j的优势

log4j2用户手册:https://logging.apache.org/log4j/2.x/manual/

Log4j1.x已经在各种应用程序中被广泛采用和利用。它的最后一次发布发生在2012年,2015年8月,Log4j 1.x宣布其生命终结。

那么,是什么让升级到Log4j2成为一个明智的决定呢?以下是一些令人信服的理由。

  1. 审计日志:Log4j 2被设计为可用作审计日志框架。Log4j 1.x和Logback在重新配置时都会丢失事件。Log4j2不会。在Logback中,Appenders中的异常对应用程序永远不可见。在Log4j 2中,可以配置Appender以允许异常渗透到应用程序。
  2. 异步记录器:Log4j 2包含基于LMAX Disruptor库的下一代异步记录器。在多线程场景中,异步记录器的吞吐量是Log4j 1.x和Logback的10倍,延迟低几个数量级。
  3. 无垃圾:Log4j2对独立应用程序是无垃圾的,在稳态日志记录期间对web应用程序是低垃圾的。这降低了垃圾收集器的压力,并可以提供更好的响应时间性能。
  4. 插件:Log4j2使用了一个插件系统,通过添加新的Appenders、Filters、Layouts、Lookups和Pattern Converter,可以非常容易地扩展框架,而无需对Log4j进行任何更改。
  5. 简单配置:由于插件系统配置更简单。配置中的条目不需要指定类名。
  6. 自定义日志级别:支持自定义日志级别。自定义日志级别可以在代码或配置中定义。
  7. Lambdas:支持lambda表达式。只有在启用了请求的日志级别的情况下,运行在Java 8上的客户端代码才能使用lambda表达式来延迟构建日志消息。不需要显式级别检查,从而产生更干净的代码。
  8. Messages:支持Message对象。消息允许对感兴趣的复杂结构的支持通过日志记录系统进行传递并进行有效操作。用户可以自由创建自己的消息类型,并编写自定义布局、过滤器和查找来操作它们。
  9. 过滤器:Log4j 1.x支持在Appenders上使用过滤器。Logback添加了TurboFilters,允许在Logger处理事件之前对其进行过滤。Log4j 2支持过滤器,这些过滤器可以配置为在事件由Logger处理之前处理事件,就像它们由Logger或Appender处理一样。
  10. Appender Layout:许多Logback Appender不接受Layout,只会以固定格式发送数据。大多数Log4j2 Appender接受Layout,允许以任何所需格式传输数据。
  11. 高级布局:Log4j 1.x和Logback中的布局返回一个字符串。这导致了Logback编码器中讨论的问题。Log4j2采用了一种更简单的方法,即Layouts总是返回一个字节数组。这样做的优点是,它意味着它们几乎可以在任何Appender中使用,而不仅仅是写入OutputStream的Appender。
  12. Syslog支持:Syslog Appender支持TCP和UDP,并支持BSD Syslog和RFC 5424格式。
  13. Java并发:Log4j2利用了Java 5的并发支持,并在尽可能低的级别执行锁定。Log4j 1.x存在已知的死锁问题,其中许多在Logback中得到了修复,但许多Logback类仍然需要相当高级别的同步。

Hello world!

引入依赖

<!--log4j2的日志门面-->  
<dependency>  
    <groupId>org.apache.logging.log4j</groupId>  
    <artifactId>log4j-api</artifactId>  
    <version>2.11.1</version>  
</dependency>  
<!--log4j2的日志实现-->  
<dependency>  
    <groupId>org.apache.logging.log4j</groupId>  
    <artifactId>log4j-core</artifactId>  
    <version>2.10.0</version>  
</dependency>

示例代码:

package com.matio.log4j2.helloworld;

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class HelloWorld {
    public static void main(String[] args) {
        // System.setProperty("org.apache.logging.log4j.level", "debug");
        Logger logger = LogManager.getLogger(HelloWorld.class);
        System.out.println("log4j2 的日志默认级别:error");
        //打印日志
        logger.fatal("严重错误,一般造成系统崩溃并终止运行");
        logger.error("错误信息,不会影响系统运行");         //默认级别
        logger.warn("警告信息,可能会发生问题");
        logger.info("运行信息,数据连接,网络连接,IO操作等");
        logger.debug("调试信息,一般在开发中使用,记录程序变量传递信息等等");
        logger.trace("追踪信息,记录程序所有的流程信息");
        
        logger.error("Logging in user {} with birthday {}", "matio", "2000");
        
        logger.printf(Level.ERROR, "Logging in user %1$s with birthday %2$tm %2$te,%2$tY", "matio", "");
        
        Logger logger2 = LogManager.getFormatterLogger("Foo");
        logger2.error("Logging in user %s with birthday %s", "matio", "xxx");
        logger2.error("Logging in user %1$s with birthday %2$tm %2$te,%2$tY", "matio", "xxxx");
        logger2.error("Integer.MAX_VALUE = %,d", Integer.MAX_VALUE);
        logger2.error("Long.MAX_VALUE = %,d", Long.MAX_VALUE);
    }
}

打印结果如下:

ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console. Set system property 'log4j2.debug' to show Log4j2 internal initialization logging.
log4j2 的日志默认级别:error
19:30:55.289 [main] FATAL com.matio.log4j2.helloworld.HelloWorld - 严重错误,一般造成系统崩溃并终止运行
19:30:55.290 [main] ERROR com.matio.log4j2.helloworld.HelloWorld - 错误信息,不会影响系统运行

log4j2如果没有指定配置文件,那么默认会输出console,默认日志级别为error

log4j2架构

Log4J2类结构如下图所示:

image.png

更多与log4j2架构有关的文章可以参考:# 11java日志之log4j2架构篇

LogManager.getLogger(HelloWorld.class)源码分析

执行这句代码最终的目的是为了获取一个Logger实现。

再看源码之前需要先了解几个概念:

  1. LoggerContextFactory:负责生产LoggerContext的接口。log4j-api-2.x包中该接口的默认实现是SimpleLoggerContextFactory,如果额外引入了log4j-core-2.x包,那么该接口的默认实现是Log4jContextFactory(可以参考LogManager静态代码块)。这里也从侧面说明了LoggerContextFactory其实是log4j2的日志门面实现接口,通过spi机制可以让用户定制自己的日志实现。

  2. LoggerContext:负责生产Logger的接口。log4j-api-2.x包中该接口的默认实现是SimpleLoggerContext,如果额外引入了log4j-core-2.x包,那么该接口的默认实现是org.apache.logging.log4j.core.LoggerContext。(log4j-api-2.x是日志门面,log4j-core-2.x是日志实现)

获取LoggerContextFactory该接口的实现类

LogManager类在被加载的时候会执行其静态代码块,见下:

static {
    final PropertiesUtil managerProps = PropertiesUtil.getProperties();
    final String factoryClassName = managerProps.getStringProperty("log4j2.loggerContextFactory");
    if (factoryClassName != null) {
        factory = LoaderUtil.newCheckedInstanceOf(factoryClassName, LoggerContextFactory.class);
    }
    if (factory == null) {
        final SortedMap<Integer, LoggerContextFactory> factories = new TreeMap<>();
        // note that the following initial call to ProviderUtil may block until a Provider has been installed when
        // running in an OSGi environment
        if (ProviderUtil.hasProviders()) {
            for (final Provider provider : ProviderUtil.getProviders()) {
                final Class<? extends LoggerContextFactory> factoryClass = provider.loadLoggerContextFactory();
                if (factoryClass != null) {
                    factories.put(provider.getPriority(), factoryClass.newInstance());
                }
            }
            if (factories.isEmpty()) {
                factory = new SimpleLoggerContextFactory();
            } else if (factories.size() == 1) {
                factory = factories.get(factories.lastKey());
            }
        } else {
            factory = new SimpleLoggerContextFactory();
        }
    }
}

这段代码主要功能就是获取一个LoggerContextFactory实现,默认是SimpleLoggerContextFactory。如果引入了log4j-core-2.x,则变成了Log4jContextFactory。(这里也侧面说明了log4j-core其实是log4j2的日志实现,log4j-api其实是log4j2的一个门面)那么是如何获取的呢?主要分为三步:

第一步:加载配置PropertySource。log4j2配置通过SPI加载PropertySource,按照顺序依次是:

  1. JVM系统变量优先级最高:SystemPropertiesPropertySource(System.getProperties())
  2. 其次是resources下用户自定义配置文件:log4j2.component.properties
  3. 最后则是计算机环境变量:EnvironmentPropertySource(System.getenv())

具体使用可以参考以下代码:

System.out.println(PropertiesUtil.getProperties().getStringProperty("aaa"));

第二步:从上述配置中加载key=log4j2.loggerContextFactory对应的value并反射生成LoggerContextFactory实现类,如果成功则返回,否则继续执行第三步;

第三步:通过SPI机制加载META-INF/services/org.apache.logging.log4j.spi.Provider文件,log4j-core包中提供了一个Log4jProvider,该类会返回Log4jContextFactory

获取LoggerContext该接口的实现类

Log4jContextFactory调用getContext()方法返回一个LoggerContext对象,并且调用其start()方法初始化。

@Override
public LoggerContext getContext(final String fqcn, final ClassLoader loader, final Object externalContext,
                                final boolean currentContext) {
    // 实例化LoggerContext
    final LoggerContext ctx = selector.getContext(fqcn, loader, currentContext);
    if (externalContext != null && ctx.getExternalContext() == null) {
        ctx.setExternalContext(externalContext);
    }
    if (ctx.getState() == LifeCycle.State.INITIALIZED) {
        // 启动LoggerContext
        ctx.start();
    }
    return ctx;
}

启动LoggerContext

当创建好一个LoggerContext实例后,会调用其start()方法进行初始化:

@Override
public void start() {
    // 省去一下无关代码...
    reconfigure();
    if (this.configuration.isShutdownHookEnabled()) {
        setUpShutdownHook();
    }
}

private void reconfigure(final URI configURI) {
    final ClassLoader cl = ClassLoader.class.isInstance(externalContext) ? (ClassLoader) externalContext : null;
    final Configuration instance = ConfigurationFactory.getInstance().getConfiguration(this, contextName, configURI, cl);
    if (instance == null) {
        LOGGER.error("Reconfiguration failed: No configuration found for '{}' at '{}' in '{}'", contextName, configURI, cl);
    } else {
        setConfiguration(instance);
    }
}

这里有一个很重要的代码:

final Configuration instance = ConfigurationFactory.getInstance().getConfiguration(this, contextName, configURI, cl);

通过ConfigurationFactory工厂去产生一个Configuration对象。

我们都知道log4j2中提供了很多中类型的插件(不理解什么是插件的可以看看我的另外一篇文章:# 12.java日志之log4j2插件plugin篇),其中有一种插件类型是ConfigurationFactory。这类插件就是专门去解析配置文件的,也就是将一个配置文件解析成一个Configuration对象。

log4j2支持XML、JSON、YAML和properties四种配置文件格式,各自对应Log4j2中的一个Configuration实现类,分别是: XmlConfigurationJsonConfigurationYamlConfigurationPropertiesConfiguration。每一个Configuration都包含了对应配置文件中的所有配置,那么log4j2是如何解析找到配置文件并且根据对应格式去解析成Configuration的呢?先看ConfigurationFactory.getInstance()这个方法里面做了什么事:


List<ConfigurationFactory> factories = null;
ConfigurationFactory configFactory = new Factory();

public static ConfigurationFactory getInstance() {
    if (factories == null) {
        LOCK.lock();
        try {
            if (factories == null) {
                final List<ConfigurationFactory> list = new ArrayList<>();
                final String factoryClass = PropertiesUtil.getProperties().getStringProperty("log4j.configurationFactory");
                if (factoryClass != null) {
                    addFactory(list, factoryClass);
                }
                final PluginManager manager = new PluginManager("ConfigurationFactory");
                manager.collectPlugins();
                final Map<String, PluginType<?>> plugins = manager.getPlugins();
                final List<Class<? extends ConfigurationFactory>> ordered = new ArrayList<>(plugins.size());
                for (final PluginType<?> type : plugins.values()) {
                    try {
                        ordered.add(type.getPluginClass().asSubclass(ConfigurationFactory.class));
                    } catch (final Exception ex) {
                        LOGGER.warn("Unable to add class {}", type.getPluginClass(), ex);
                    }
                }
                Collections.sort(ordered, OrderComparator.getInstance());
                for (final Class<? extends ConfigurationFactory> clazz : ordered) {
                    addFactory(list, clazz);
                }
                factories = Collections.unmodifiableList(list);
            }
        } finally {
            LOCK.unlock();
        }
    }
    return configFactory;
}

这个方法其实就是扫描classpath下所有ConfigurationFactory类型的插件,然后按照其优先级排序,然后赋值给factories对象。扫描出的结果默认按照以下优先级排序依次是:

  1. PropertiesConfigurationFactory:支持的配置文件后缀:.properties
  2. YamlConfigurationFactory:支持的配置文件后缀:.yml、.yaml
  3. JsonConfigurationFactory:支持的配置文件后缀:.json、.jsn
  4. XmlConfigurationFactory:支持的配置文件后缀:.xml、*

这四个类其实都是ConfigurationFactory接口的四个实现,负责解析对应后缀的配置文件的,然后产生一个Configuration实例,它包含了这个配置文件中的所有信息。依次对应PropertiesConfigurationYamlConfigurationJsonConfigurationXmlConfiguration

当然log4j2也支持扩展点,支持用户配置log4j.configurationFactory实现自定义ConfigurationFactory类型的插件,然后赋予它更高的优先级。另外我们也可以配置log4j.configurationFactoryXmlConfigurationFactory,从而将xml配置优先级变成最高

自动配置原理

Log4j2能够在初始化期间自动配置自身。 当Log4j2启动时,它将找到所有ConfigurationFactory插件,并按照从最高到最低的顺序排列它们。 Log4j包含四个ConfigurationFactory实现:一个用于JSON,一个用于YAML,一个用properties,一个用于XML。

回到上面的那个方法。看看该方法的返回值,其实固定为log4j2内部的一个Factory对象,它其实也是ConfigurationFactory接口的实现。就是通过该类来遍历factories尝试解析各种类型的配置文件的,见以下方法:

ConfigurationFactory.Factory#getConfiguration(LoggerContext, String, URI);

这个方法里面逻辑有点复杂,但是其执行的流程却异常清晰,如下:

  1. log4j2在程序启动时会检查系统配置项log4j2.configurationFile指定的文件,如果此配置项被设置,则优先找到对应的ConfigurationFactory去解析加载此配置项指定的配置文件,支持多个文件逗号分隔
  2. 如果1中的系统配置项没有被指定,则按下面优先级顺序加载。首先寻找classpath下的log4j2-test.properties文件
  3. 如果没有找到这样的文件,就寻找classpath下的log4j2-test.yml或者log4j2-test.yaml文件
  4. 如果没有找到这样的文件,就寻找classpath下的log4j2-test.json或者log4j2-test.jsn文件
  5. 如果没有找到这样的文件,就寻找classpath下的log4j2-test.xml文件
  6. 如果上述test文件没有找到,则尝试寻找classpath下的log4j2.yml或者log4j2.properties文件
  7. 如果没有找到这样的文件,就尝试寻找classpath下的log4j2.yml或者log4j2.yaml文件
  8. 寻找classpath下的log4j2.json或者log4j2.jsn文件
  9. 寻找classpath下的log4j2.xml文件
  10. 如果上述配置文件都无法找到,则将使用DefaultConfiguration。 这将导致日志输出转到控制台。

这也就是为什么我们之间在resources下新建一个log4j2.xml文件就能被识别到的原因。因为log4j2可以自动探测到。