1. 概述

Log4j 2 的强大扩展能力很大程度上依赖于其插件(Plugin)机制。像 Appender、Layout、Filter 这些核心组件,本质上都是插件。这类插件被称为 核心插件(Core Plugins),Log4j 2 官方提供了丰富的内置选项,基本能满足大多数场景。

但总有特殊需求,比如想把日志写入 Kafka、在日志中打印 Docker 容器名,或者实现特定的过滤逻辑。这时候,官方插件就不够用了。

✅ 本教程将深入 Log4j 2 的插件扩展机制,手把手教你如何实现自定义插件,应对这些“非主流”需求。

2. Log4j 2 插件分类

Log4j 2 的插件体系非常清晰,主要分为五大类:

  1. **核心插件 (Core Plugins)**:如 Appender、Layout、Filter 等,是日志处理流程的基石。
  2. **转换器 (Convertors)**:通常指 PatternConverter,用于格式化 PatternLayout 中的占位符。
  3. **键提供器 (Key Providers)**:不太常用,用于提供键值对。
  4. **查找器 (Lookups)**:允许在配置文件中动态注入运行时值,比如系统属性、环境变量、日期等。
  5. **类型转换器 (Type Converters)**:用于将配置中的字符串转换为 Java 对象。

Log4j 2 提供了一套统一的注解机制来实现所有这些类型的插件,核心就是 @Plugin 注解

⚠️ 和 Log4j 1.x 相比,Log4j 2 的扩展方式更优雅。在 1.x 中,扩展一个 Appender 只能通过继承并重写其类。而在 2.x 中,你只需用 @Plugin 注解一个新类,并赋予它一个名字,就能无缝集成到配置中,简单粗暴。

接下来,我们重点看几个常用类别的自定义实现。

3. 核心插件

3.1. 实现自定义核心插件

Appender、Layout、Filter 都属于核心插件。虽然内置的够多,但遇到特殊场景,比如想把日志暂存到内存列表里做分析,就得自己写一个 ListAppender

@Plugin(name = "ListAppender", 
  category = Core.CATEGORY_NAME, 
  elementType = Appender.ELEMENT_TYPE)
public class ListAppender extends AbstractAppender {

    private List<LogEvent> logList;

    protected ListAppender(String name, Filter filter) {
        super(name, filter, null);
        logList = Collections.synchronizedList(new ArrayList<>());
    }

    @PluginFactory
    public static ListAppender createAppender(
      @PluginAttribute("name") String name, 
      @PluginElement("Filter") final Filter filter) {
        return new ListAppender(name, filter);
    }

    @Override
    public void append(LogEvent event) {
        if (event.getLevel().isLessSpecificThan(Level.WARN)) {
            error("Unable to log less than WARN level.");
            return;
        }
        logList.add(event);
    }
}

关键点解析:

  • @Plugin:声明这是一个插件,name 是你在 XML 配置中引用它的名字。
  • @PluginFactory:静态工厂方法,负责创建插件实例。
  • @PluginAttribute:标注方法参数,表示这个参数来自配置文件的属性(attribute)。
  • @PluginElement:标注方法参数,表示这个参数来自配置文件的嵌套元素(element),比如 <Filter>

定义好后,就可以在 log4j2.xml 里像使用内置 Appender 一样使用它:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration xmlns:xi="http://www.w3.org/2001/XInclude"
    packages="com.baeldung" status="WARN">
    <Appenders>
        <ListAppender name="ListAppender">
            <BurstFilter level="INFO" rate="16" maxBurst="100"/>
        </ListAppender>
    </Appenders>
    <Loggers>
        <Root level="DEBUG">
            <AppenderRef ref="ConsoleAppender" />
            <AppenderRef ref="ListAppender" />
        </Root>
    </Loggers>
</Configuration>

3.2. 使用插件构建器 (Plugin Builders)

上面的例子很简单,只有一个 name 参数。但现实中的 Appender 通常很复杂,比如一个 KafkaAppender,需要配置 IP、端口、Topic、分区等一堆参数。

如果都堆在 @PluginFactory 方法的参数列表里,代码会非常难看。Log4j 2 提供了基于建造者模式 (Builder Pattern) 的解决方案。

@Plugin(name = "Kafka2", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE)
public class KafkaAppender extends AbstractAppender {

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

        @PluginBuilderAttribute("name")
        @Required
        private String name;

        @PluginBuilderAttribute("ip")
        private String ipAddress;

        @PluginBuilderAttribute("port")
        private int port = 9092;

        @PluginBuilderAttribute("topic")
        private String topic;

        @PluginBuilderAttribute("partition")
        private String partition;

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

        @PluginElement("Filter")
        private Filter filter;

        // ... getters and setters

        @Override
        public KafkaAppender build() {
            return new KafkaAppender(
              getName(), getFilter(), getLayout(), true, 
              new KafkaBroker(ipAddress, port, topic, partition));
        }
    }

    private KafkaBroker broker;

    private KafkaAppender(String name, Filter filter, Layout<? extends Serializable> layout, 
      boolean ignoreExceptions, KafkaBroker broker) {
        super(name, filter, layout, ignoreExceptions);
        this.broker = broker;
    }

    @Override
    public void append(LogEvent event) {
        connectAndSendToKafka(broker, event);
    }
}

这样,你的配置文件就能写得非常清晰:

<Kafka2 name="KafkaLogger" ip="192.168.1.100" port="9010" topic="app-logs" partition="p-1">
    <PatternLayout pattern="%d %p %m%n" />
</Kafka2>

@PluginBuilderAttribute 告诉 Log4j 2 哪些字段需要从配置中注入。Builder 模式让复杂插件的实现变得井井有条。

3.3. 扩展现有插件

理论上,你可以通过给自定义插件起一个和内置插件相同的名字来“覆盖”它。比如,你想增强 RollingFileAppender 的功能:

@Plugin(name = "RollingFile", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE)
public class RollingFileAppender extends AbstractAppender {
    // ...
}

⚠️ 但是!官方强烈不建议这么做。 因为 Log4j 2 在发现同名插件时,会使用类路径中先被发现的那个。这会导致行为不确定,非常容易踩坑。

✅ 正确的做法是:给你的插件起一个独一无二的名字(比如 MyRollingFile),然后在配置文件中使用这个名字。这样逻辑清晰,维护性好。

4. 转换器插件 (Converter Plugin)

PatternLayout 是最常用的 Layout,它通过一个模式字符串(pattern)来定义日志输出格式,比如 %d %p %m%n

这里的 %d 就是一个**转换模式 (conversion pattern)**。Log4j 2 内部有一个 DatePatternConverter 来处理它,将其转换为实际的日期。

假设你的应用运行在 Docker 里,想在每条日志里打印容器名。这就需要自定义一个转换器。

@Plugin(name = "DockerPatternConverter", category = PatternConverter.CATEGORY)
@ConverterKeys({"docker", "container"})
public class DockerPatternConverter extends LogEventPatternConverter {

    private DockerPatternConverter(String[] options) {
        super("Docker", "docker");
    }

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

    @Override
    public void format(LogEvent event, StringBuilder toAppendTo) {
        toAppendTo.append(dockerContainer());
    }

    private String dockerContainer() {
        // 这里可以读取环境变量、调用 Docker API 等
        return "app-container-prod-01";
    }
}

关键点:

  • @ConverterKeys({"docker", "container"}):这个注解是转换器插件的标志。它定义了你在 PatternLayout 中可以使用的占位符。上面的代码意味着 %docker%container 都会被这个转换器处理。
  • format 方法:将实际内容(容器名)追加到输出字符串中。

配置文件中使用:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration packages="com.baeldung" status="WARN">
    <Appenders>
        <Console name="DockerConsoleLogger" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss} [%t] %-5level %docker %msg%n" />
        </Console>
    </Appenders>
    <Loggers>
        <Logger name="com.example" level="INFO">
            <AppenderRef ref="DockerConsoleLogger" />
        </Logger>
    </Loggers>
</Configuration>

输出效果:

14:23:45 [main] INFO  app-container-prod-01 This is a log message.

5. 查找器插件 (Lookup Plugin)

查找器 (Lookup) 允许你在配置文件中使用动态值,语法是 ${lookup: key}$${lookup: key}(延迟解析)。

比如,RollingFileAppender 经常用 date 查找器来按日期分割日志文件:

<RollingFile name="Rolling-File" fileName="logs/app.log" 
  filePattern="logs/app-$${date:yyyy-MM-dd}.%i.log.gz">
    <PatternLayout pattern="%d %p %m%n" />
    <SizeBasedTriggeringPolicy size="10MB" />
</RollingFile>

如果你想从 Kafka 的某个 Topic 里读取一个配置值(比如日志级别)来动态设置文件名,就需要自定义一个 KafkaLookup

@Plugin(name = "kafka", category = StrLookup.CATEGORY)
public class KafkaLookup implements StrLookup {

    @Override
    public String lookup(String key) {
        // key 是配置中传入的,比如 "topic-1"
        return getFromKafka(key);
    }

    @Override
    public String lookup(LogEvent event, String key) {
        // 另一个重载方法,可以访问当前的 LogEvent
        return getFromKafka(key);
    }

    private String getFromKafka(String topicName) {
        // 模拟从 Kafka 获取最新消息
        return "v1.2.3";
    }
}

配置文件中使用:

<RollingFile name="Rolling-File" fileName="logs/app.log" 
  filePattern="logs/app-$${kafka:deployment-tag}.%i.log.gz">
    <PatternLayout pattern="%d %p %m%n" />
    <SizeBasedTriggeringPolicy size="10MB" />
</RollingFile>

⚠️ 注意:查找器插件比较特殊,它**没有 @PluginFactory**。Log4j 2 会直接调用其默认构造函数来创建实例。

6. 插件发现机制

最后,揭秘 Log4j 2 是如何“找到”你写的这些插件的。

核心原则:通过 @Plugin 注解中的 name 属性进行查找

查找顺序如下(优先级从高到低):

  1. 内置列表log4j2-core.jar 包里的 Log4j2Plugins.dat 文件,包含了所有官方插件的列表。
  2. OSGi Bundles:如果你在 OSGi 环境下,会检查相应的 Bundle。
  3. 系统属性:通过 -Dlog4j.plugin.packages=com.example.plugins 指定包名。
  4. 编程式配置:调用 PluginManager.addPackages("com.example")
  5. 配置文件:在 log4j2.xml<Configuration> 标签里通过 packages="com.example" 属性指定。

关键点:为了让 @Plugin 注解生效,必须开启注解处理。Maven/Gradle 项目通常没问题,但如果你是手动编译,需要注意。

⚠️ 再次强调插件同名问题:如果两个插件名字一样,先被发现的生效。如果你想“覆盖”官方插件(不推荐),必须确保你的 jar 包在类路径上位于 log4j2-core.jar 之前

7. 总结

本文系统地介绍了 Log4j 2 的插件体系:

  • 掌握了五大插件类别,特别是核心插件、转换器、查找器的自定义方法。
  • 理解了 @Plugin@PluginFactory@ConverterKeys 等核心注解的用法。
  • 学会了用 Builder 模式处理复杂配置。
  • 了解了插件发现的优先级顺序,避免了同名插件的坑。

通过这些知识,你可以轻松扩展 Log4j 2,满足各种定制化日志需求。

所有示例代码均已上传至 GitHub: https://github.com/baeldung/tutorials/tree/master/logging-modules/log4j2


原始标题:Log4j 2 Plugins | Baeldung