扩展 Log4j

Log4j 2 提供了许多方法来操作和扩展它。本节概述了 Log4j 2 实现直接支持的各种方法。

LoggerContextFactory

LoggerContextFactory 将 Log4j API 绑定到其实现。Log4j LogManager 使用 java.util.ServiceLoader 来定位 org.apache.logging.log4j.spi.Provider 的所有实例,从而定位 LoggerContextFactory。每个实现都必须提供一个扩展 org.apache.logging.log4j.spi.Provider 的类,并且应该有一个无参数构造函数,该构造函数委托给 Provider 的构造函数,并传递 Priority、它兼容的 API 版本以及实现 org.apache.logging.log4j.spi.LoggerContextFactory 的类。Log4j 将比较当前 API 版本,如果兼容,则将实现添加到提供程序列表中。org.apache.logging.log4j.LogManager 中的 API 版本仅在 API 中添加了实现需要知道的特性时才会更改。如果找到多个有效的实现,则将使用 Priority 的值来标识具有最高优先级的工厂。最后,将实例化实现 org.apache.logging.log4j.spi.LoggerContextFactory 的类并将其绑定到 LogManager。在 Log4j 2 中,这是由 Log4jContextFactory 提供的。

应用程序可以通过以下方式更改将使用的 LoggerContextFactory

  1. 创建与日志记录实现的绑定。
    1. 实现新的 LoggerContextFactory
    2. 实现一个扩展 org.apache.logging.spi.Provider. 的类,该类具有一个无参数构造函数,该构造函数使用 Priority、API 版本、LoggerContextFactory 类以及可选的 ThreadContextMap 实现类调用超类的构造函数。
    3. 创建一个包含实现 org.apache.logging.spi.Provider 的类的名称的 META-INF/services/org.apache.logging.spi.Provider 文件。
  2. 将系统属性 log4j2.loggerContextFactory 设置为要使用的 LoggerContextFactory 类的名称。
  3. 在名为 log4j2.component.properties 的属性文件中设置属性 log4j2.loggerContextFactory 为要使用的 LoggerContextFactory 类的名称。属性文件必须位于类路径中。

ContextSelector

ContextSelector 由 Log4j LoggerContext 工厂 调用。它们执行定位或创建 LoggerContext 的实际工作,LoggerContext 是记录器及其配置的锚点。ContextSelector 可以自由地实现任何他们想要管理 LoggerContext 的机制。默认的 Log4jContextFactory 检查是否存在名为“Log4jContextSelector”的系统属性。如果找到,则该属性应包含实现要使用的 ContextSelector 的类的名称。

Log4j 提供了五个 ContextSelector

BasicContextSelector
使用已存储在 ThreadLocal 中的 LoggerContext 或公共 LoggerContext。
ClassLoaderContextSelector
将 LoggerContext 与创建 getLogger 调用调用者的 ClassLoader 关联。这是默认的 ContextSelector。
JndiContextSelector
通过查询 JNDI 来定位 LoggerContext。从 Log4j 2.17.0 开始,JNDI 操作要求将 log4j2.enableJndiContextSelector=true 设置为系统属性或相应的环境变量,才能使此查找正常工作。请参阅 enableJndiContextSelector 系统属性。
AsyncLoggerContextSelector
创建一个 LoggerContext,确保所有记录器都是 AsyncLogger。
BundleContextSelector
将 LoggerContext 与创建 getLogger 调用调用者的捆绑包的 ClassLoader 关联。这在 OSGi 环境中默认启用。

ConfigurationFactory

修改日志记录配置方式通常是最受关注的领域之一。执行此操作的主要方法是实现或扩展 ConfigurationFactory。Log4j 提供了两种添加新 ConfigurationFactory 的方法。第一种方法是将名为“log4j.configurationFactory”的系统属性定义为应首先搜索配置的类的名称。第二种方法是将 ConfigurationFactory 定义为插件。

然后按顺序处理所有 ConfigurationFactory。每个工厂都会在其 getSupportedTypes 方法上被调用,以确定它支持的文件扩展名。如果找到具有指定文件扩展名之一的配置文件,则控制权将传递给该 ConfigurationFactory,以加载配置并创建 Configuration 对象。

大多数 Configuration 扩展 BaseConfiguration 类。此类期望子类将处理配置文件并创建节点对象的层次结构。每个节点都非常简单,它包含节点的名称、与节点关联的名称/值对、节点的 PluginType 以及所有子节点的列表。然后,BaseConfiguration 将传递节点树并从该树中实例化配置对象。

@Plugin(name = "XMLConfigurationFactory", category = "ConfigurationFactory")
@Order(5)
public class XMLConfigurationFactory extends ConfigurationFactory {

    /**
     * Valid file extensions for XML files.
     */
    public static final String[] SUFFIXES = new String[] {".xml", "*"};

    /**
     * Returns the Configuration.
     * @param loggerContext The logger context.
     * @param source The InputSource.
     * @return The Configuration.
     */
    @Override
    public Configuration getConfiguration(final LoggerContext loggerContext, final ConfigurationSource source) {
        return new XmlConfiguration(loggerContext, source);
    }

    /**
     * Returns the file suffixes for XML files.
     * @return An array of File extensions.
     */
    public String[] getSupportedTypes() {
        return SUFFIXES;
    }
}

LoggerConfig

LoggerConfig 对象是应用程序创建的记录器与配置联系的地方。Log4j 实现要求所有 LoggerConfig 都基于 LoggerConfig 类,因此希望进行更改的应用程序必须通过扩展 LoggerConfig 类来执行此操作。要声明新的 LoggerConfig,请将其声明为类型为“Core”的插件,并提供应用程序应在配置中指定为元素名称的名称。LoggerConfig 还应定义一个 PluginFactory,该工厂将创建 LoggerConfig 的实例。

以下示例显示了根 LoggerConfig 如何简单地扩展通用 LoggerConfig。

@Plugin(name = "root", category = "Core", printObject = true)
public static class RootLogger extends LoggerConfig {

    @PluginFactory
    public static LoggerConfig createLogger(@PluginAttribute(value = "additivity", defaultBooleanValue = true) boolean additivity,
                                            @PluginAttribute(value = "level", defaultStringValue = "ERROR") Level level,
                                            @PluginElement("AppenderRef") AppenderRef[] refs,
                                            @PluginElement("Filters") Filter filter) {
        List<AppenderRef> appenderRefs = Arrays.asList(refs);
        return new LoggerConfig(LogManager.ROOT_LOGGER_NAME, appenderRefs, filter, level, additivity);
    }
}

LogEventFactory

LogEventFactory 用于生成 LogEvent。应用程序可以通过将系统属性 Log4jLogEventFactory 的值设置为自定义 LogEventFactory 类的名称来替换标准 LogEventFactory。

注意:当 log4j 配置为具有 所有记录器异步 时,日志事件将在环形缓冲区中预先分配,并且不会使用 LogEventFactory。

MessageFactory

MessageFactory 用于生成 Message 对象。应用程序可以通过将系统属性 log4j2.messageFactory 的值设置为自定义 MessageFactory 类的名称来替换标准 ParameterizedMessageFactory(或无垃圾模式下的 ReusableMessageFactory)。

Logger.entry()Logger.exit() 方法的流消息使用单独的 FlowMessageFactory。应用程序可以通过将系统属性 log4j2.flowMessageFactory 的值设置为自定义 FlowMessageFactory 类的名称来替换 DefaultFlowMessageFactory。

查找

查找是执行参数替换的方式。在配置初始化期间,将创建一个“Interpolator”,它定位所有查找并注册它们,以便在需要解析变量时使用。插值器将变量名称的“前缀”部分与注册的查找匹配,并将控制权传递给它以解析变量。

查找必须使用类型为“Lookup”的 Plugin 注释声明。Plugin 注释中指定的名称将用于匹配前缀。与其他插件不同,查找不使用 PluginFactory。相反,它们需要提供一个不接受任何参数的构造函数。下面的示例显示了一个查找,它将返回系统属性的值。

提供的查找在此处记录:查找

@Plugin(name = "sys", category = "Lookup")
public class SystemPropertiesLookup implements StrLookup {

    /**
     * Lookup the value for the key.
     * @param key  the key to be looked up, may be null
     * @return The value for the key.
     */
    public String lookup(String key) {
        return System.getProperty(key);
    }

    /**
     * Lookup the value for the key using the data in the LogEvent.
     * @param event The current LogEvent.
     * @param key  the key to be looked up, may be null
     * @return The value associated with the key.
     */
    public String lookup(LogEvent event, String key) {
        return System.getProperty(key);
    }
}

过滤器

正如预期的那样,过滤器用于在日志事件通过日志系统时拒绝或接受它们。过滤器使用类型为“Core”且元素类型为“filter”的 Plugin 注释声明。Plugin 注释中的 name 属性用于指定用户应使用哪个元素来启用过滤器。将 printObject 属性指定为“true”表示调用 toString 将在处理配置时将参数格式化为过滤器。过滤器还必须指定一个 PluginFactory 方法,该方法将被调用以创建过滤器。

下面的示例显示了一个过滤器,用于根据其日志级别拒绝 LogEvents。请注意所有过滤器方法都解析为单个过滤器方法的典型模式。

@Plugin(name = "ThresholdFilter", category = "Core", elementType = "filter", printObject = true)
public final class ThresholdFilter extends AbstractFilter {

    private final Level level;

    private ThresholdFilter(Level level, Result onMatch, Result onMismatch) {
        super(onMatch, onMismatch);
        this.level = level;
    }

    public Result filter(Logger logger, Level level, Marker marker, String msg, Object[] params) {
        return filter(level);
    }

    public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) {
        return filter(level);
    }

    public Result filter(Logger logger, Level level, Marker marker, Message msg, Throwable t) {
        return filter(level);
    }

    @Override
    public Result filter(LogEvent event) {
        return filter(event.getLevel());
    }

    private Result filter(Level level) {
        return level.isAtLeastAsSpecificAs(this.level) ? onMatch : onMismatch;
    }

    @Override
    public String toString() {
        return level.toString();
    }

    /**
     * Create a ThresholdFilter.
     * @param loggerLevel The log Level.
     * @param match The action to take on a match.
     * @param mismatch The action to take on a mismatch.
     * @return The created ThresholdFilter.
     */
    @PluginFactory
    public static ThresholdFilter createFilter(@PluginAttribute(value = "level", defaultStringValue = "ERROR") Level level,
                                               @PluginAttribute(value = "onMatch", defaultStringValue = "NEUTRAL") Result onMatch,
                                               @PluginAttribute(value = "onMismatch", defaultStringValue = "DENY") Result onMismatch) {
        return new ThresholdFilter(level, onMatch, onMismatch);
    }
}

追加器

追加器传递一个事件,(通常)调用一个布局来格式化事件,然后以任何所需的方式“发布”事件。追加器被声明为类型为“Core”且元素类型为“appender”的插件。Plugin 注释中的 name 属性指定用户必须在其配置中提供哪个元素才能使用追加器。如果 toString 方法呈现传递给追加器的属性的值,则追加器应将 printObject 指定为“true”。

追加器还必须声明一个 PluginFactory 方法,该方法将创建追加器。下面的示例显示了一个名为“Stub”的追加器,它可以用作初始模板。

大多数追加器使用管理器。管理器实际上“拥有”资源,例如 OutputStream 或套接字。当发生重新配置时,将创建一个新的追加器。但是,如果先前管理器中的任何重要内容都没有改变,则新的追加器将简单地引用它,而不是创建一个新的。这确保了在重新配置发生时不会丢失事件,而无需在重新配置发生时暂停日志记录。

@Plugin(name = "Stub", category = "Core", elementType = "appender", printObject = true)
public final class StubAppender extends AbstractOutputStreamAppender<StubManager> {

    private StubAppender(String name,
                         Layout<? extends Serializable> layout,
                         Filter filter,
                         boolean ignoreExceptions,
                         StubManager  manager) {
        super(name, layout, filter, ignoreExceptions, true, manager);
    }

    @PluginFactory
    public static StubAppender createAppender(@PluginAttribute("name") String name,
                                              @PluginAttribute("ignoreExceptions") boolean ignoreExceptions,
                                              @PluginElement("Layout") Layout layout,
                                              @PluginElement("Filters") Filter filter) {

        if (name == null) {
            LOGGER.error("No name provided for StubAppender");
            return null;
        }

        StubManager manager = StubManager.getStubManager(name);
        if (manager == null) {
            return null;
        }
        if (layout == null) {
            layout = PatternLayout.createDefaultLayout();
        }
        return new StubAppender(name, layout, filter, ignoreExceptions, manager);
    }
}

布局

布局将事件格式化为可打印的文本,该文本由追加器写入某个目标。所有布局都必须实现 Layout 接口。将事件格式化为字符串的布局应扩展 AbstractStringLayout,它将负责将字符串转换为所需的字节数组。

每个布局都必须使用 Plugin 注释将自己声明为插件。类型必须为“Core”,元素类型必须为“layout”。如果插件的 toString 方法将提供对象及其参数的表示,则 printObject 应设置为 true。插件的名称必须与用户应使用哪个值将其指定为其追加器配置中的元素相匹配。插件还必须提供一个用 PluginFactory 注释的静态方法,并且每个方法参数都用 PluginAttr 或 PluginElement 注释,具体取决于情况。

@Plugin(name = "SampleLayout", category = "Core", elementType = "layout", printObject = true)
public class SampleLayout extends AbstractStringLayout {

    protected SampleLayout(boolean locationInfo, boolean properties, boolean complete,
                           Charset charset) {
    }

    @PluginFactory
    public static SampleLayout createLayout(@PluginAttribute("locationInfo") boolean locationInfo,
                                            @PluginAttribute("properties") boolean properties,
                                            @PluginAttribute("complete") boolean complete,
                                            @PluginAttribute(value = "charset", defaultStringValue = "UTF-8") Charset charset) {
        return new SampleLayout(locationInfo, properties, complete, charset);
    }
}

模式转换器

模式转换器由 PatternLayout 用于将日志事件格式化为可打印的字符串。每个转换器负责一种类型的操作,但是转换器可以自由地以复杂的方式格式化事件。例如,有几个转换器可以操作 Throwables 并以各种方式格式化它们。

模式转换器首先必须使用标准 Plugin 注释将自己声明为插件,但必须在 type 属性上指定“Converter”的值。此外,转换器还必须指定 ConverterKeys 属性以定义可以在模式中指定的标记(以“%”字符开头)以标识转换器。

与大多数其他插件不同,转换器不使用 PluginFactory。相反,每个转换器都需要提供一个静态的 newInstance 方法,该方法接受一个字符串数组作为唯一参数。字符串数组是在花括号中指定的值,这些值可以跟随转换器键。

以下显示了转换器插件的骨架。

@Plugin(name = "query", category = "Converter")
@ConverterKeys({"q", "query"})
public final class QueryConverter extends LogEventPatternConverter {

    public QueryConverter(String[] options) {
    }

    public static QueryConverter newInstance(final String[] options) {
      return new QueryConverter(options);
    }
}

插件构建器

一些插件需要很多可选的配置选项。当插件需要很多选项时,使用构建器类而不是工厂方法(参见 Joshua Bloch 的《Effective Java》中的“Item 2:在遇到多个构造函数参数时考虑使用构建器”)更易于维护。与使用带注释的工厂方法相比,使用带注释的构建器类还有一些其他优势。

  • 如果属性名称与字段名称匹配,则无需指定属性名称。
  • 可以在代码中指定默认值,而不是通过注释(还可以允许运行时计算的默认值,这在注释中不允许)。
  • 添加新的可选参数不需要对现有的程序化配置进行重构。
  • 使用构建器编写单元测试比使用带可选参数的工厂方法更容易。
  • 默认值是通过代码指定的,而不是依赖于反射和注入,因此它们在程序化和配置文件中都能正常工作。

以下是来自 ListAppender 的插件工厂示例

@PluginFactory
public static ListAppender createAppender(
        @PluginAttribute("name") @Required(message = "No name provided for ListAppender") final String name,
        @PluginAttribute("entryPerNewLine") final boolean newLine,
        @PluginAttribute("raw") final boolean raw,
        @PluginElement("Layout") final Layout<? extends Serializable> layout,
        @PluginElement("Filter") final Filter filter) {
    return new ListAppender(name, filter, layout, newLine, raw);
}

以下是使用构建器模式而不是工厂方法的相同工厂

@PluginBuilderFactory
public static Builder newBuilder() {
    return new Builder();
}

public static class Builder implements org.apache.logging.log4j.core.util.Builder<ListAppender> {

    @PluginBuilderAttribute
    @Required(message = "No name provided for ListAppender")
    private String name;

    @PluginBuilderAttribute
    private boolean entryPerNewLine;

    @PluginBuilderAttribute
    private boolean raw;

    @PluginElement("Layout")
    private Layout<? extends Serializable> layout;

    @PluginElement("Filter")
    private Filter filter;

    public Builder setName(final String name) {
        this.name = name;
        return this;
    }

    public Builder setEntryPerNewLine(final boolean entryPerNewLine) {
        this.entryPerNewLine = entryPerNewLine;
        return this;
    }

    public Builder setRaw(final boolean raw) {
        this.raw = raw;
        return this;
    }

    public Builder setLayout(final Layout<? extends Serializable> layout) {
        this.layout = layout;
        return this;
    }

    public Builder setFilter(final Filter filter) {
        this.filter = filter;
        return this;
    }

    @Override
    public ListAppender build() {
        return new ListAppender(name, filter, layout, entryPerNewLine, raw);
    }
}

注释中唯一的区别是使用 @PluginBuilderAttribute 而不是 @PluginAttribute,以便可以使用默认值和反射,而不是在注释中指定它们。两种注释都可以在构建器中使用,但前者更适合字段注入,而后者更适合参数注入。否则,所有相同的注释(@PluginConfiguration@PluginElement@PluginNode@PluginValue)都支持在字段上使用。请注意,仍然需要一个工厂方法来提供构建器,并且此工厂方法应使用 @PluginBuilderFactory 注释。

在解析配置后构建插件时,如果可用,将使用插件构建器,否则将使用插件工厂方法作为后备。如果插件既没有工厂,那么它就不能从配置文件中使用(当然,它仍然可以以编程方式使用)。

以下是如何以编程方式使用插件工厂与插件构建器的示例

ListAppender list1 = ListAppender.createAppender("List1", true, false, null, null);
ListAppender list2 = ListAppender.newBuilder().setName("List1").setEntryPerNewLine(true).build();

自定义 ContextDataProvider

ContextDataProvider(在 Log4j 2.13.2 中引入)是一个接口,应用程序和库可以使用它将额外的键值对注入 LogEvent 的上下文数据中。Log4j 的 ThreadContextDataInjector 使用 java.util.ServiceLoader 来定位和加载 ContextDataProvider 实例。Log4j 本身使用 org.apache.logging.log4j.core.impl.ThreadContextDataProvider 将 ThreadContextData 添加到 LogEvent 中。自定义实现应实现 org.apache.logging.log4j.core.util.ContextDataProvider 接口,并通过在名为 META-INF/services/org.apache.logging.log4j.core.util.ContextDataProvider 的文件中定义实现类来将其声明为服务。

自定义 ThreadContextMap 实现

可以通过将系统属性 log4j2.garbagefreeThreadContextMap 设置为 true 来安装一个无垃圾的基于 StringMap 的上下文映射。(Log4j 必须启用才能使用 ThreadLocals。)

可以通过将系统属性 log4j2.threadContextMap 设置为实现 ThreadContextMap 接口的类的完全限定类名来安装任何自定义 ThreadContextMap 实现。通过还实现 ReadOnlyThreadContextMap 接口,您的自定义 ThreadContextMap 实现将可通过 ThreadContext::getThreadContextMap 方法供应用程序访问。

自定义插件

请参阅手册的插件部分。