1. 概述
Log4j 2 的强大扩展能力很大程度上依赖于其插件(Plugin)机制。像 Appender、Layout、Filter 这些核心组件,本质上都是插件。这类插件被称为 核心插件(Core Plugins),Log4j 2 官方提供了丰富的内置选项,基本能满足大多数场景。
但总有特殊需求,比如想把日志写入 Kafka、在日志中打印 Docker 容器名,或者实现特定的过滤逻辑。这时候,官方插件就不够用了。
✅ 本教程将深入 Log4j 2 的插件扩展机制,手把手教你如何实现自定义插件,应对这些“非主流”需求。
2. Log4j 2 插件分类
Log4j 2 的插件体系非常清晰,主要分为五大类:
- **核心插件 (Core Plugins)**:如 Appender、Layout、Filter 等,是日志处理流程的基石。
- **转换器 (Convertors)**:通常指
PatternConverter
,用于格式化PatternLayout
中的占位符。 - **键提供器 (Key Providers)**:不太常用,用于提供键值对。
- **查找器 (Lookups)**:允许在配置文件中动态注入运行时值,比如系统属性、环境变量、日期等。
- **类型转换器 (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
属性进行查找。
查找顺序如下(优先级从高到低):
- 内置列表:
log4j2-core.jar
包里的Log4j2Plugins.dat
文件,包含了所有官方插件的列表。 - OSGi Bundles:如果你在 OSGi 环境下,会检查相应的 Bundle。
- 系统属性:通过
-Dlog4j.plugin.packages=com.example.plugins
指定包名。 - 编程式配置:调用
PluginManager.addPackages("com.example")
。 - 配置文件:在
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