[日志框架] Log4j2 变量配置管理
目录
- 序
- Log4j2变量配置的获取方法/Config Item Lookup Methods
- bundle | 资源绑定 | ResourceBundle
- sys | 系统属性、VM Options | System.getProperty(xxx)
- env | 环境变量 | System.getenv(xxx)
- main | 启动参数 | args
- %X{mdcVariable} | slf4j的MDC机制 | MDC.put/get/remove
- 总览
- 注意事项
- Y 推荐文献
- X 参考文献
序
-
log4j2 支持从本地配置文件(log4j2.properties/xml/yaml)、从远程NACOS等处加载配置,并将获取到的变量配置注入到日志中。
-
因此,也由此衍生出多种lookup。
lookup
方法,可以理解为在日志框架Appender的pattern
中读取各种来源的配置变量的方法,读取后可直接作用于日志记录上。
- 参考版本 : slf4j.version = 1.7.30 / log4j.version=2.20.0
org.slf4j:slf4j-api:${slf4j.version}
org.apache.logging.log4j:log4j-api:${log4j.version}
org.apache.logging.log4j:log4j-core:${log4j.version}
org.apache.logging.log4j:log4j-slf4j-impl:${log4j.version}
org.apache.logging.log4j:log4j-jul:${log4j.version}
Log4j2变量配置的获取方法/Config Item Lookup Methods
支持多种方式动态读取配置
bundle | 资源绑定 | ResourceBundle
- bundle获取配置变量的方式:
${bundle:BundleName:BundleKey}
bundle: 固定前缀, 标识读取配置文件
-
BundleName: 配置文件名(如application.properties)
-
BundleKey: 配置key
-
案例:从
application.properties
中读取key为log.path
的值
- 方式1:log4j2.properties : pattern
${bundle:application:log.path}
- 方式2:java 应用程序代码中加载
//ResourceBundle.getBundle(bundleName).getString(bundleKey)
//加载资源文件 : 在Java中,可以使用 java.util.ResourceBundle 类来加载资源文件
//ResourceBundle rb = ResourceBundle.getBundle("application", Locale.getDefault());//读取`[resources|classes]/application.properties`资源文件
//String demoKey = rb.getString("org.example.confgKey");//"hello"
sys | 系统属性、VM Options | System.getProperty(xxx)
- sys获取配置变量的方式:
${sys:some.property}
或${sys:some.property:-default_value}
sys: 标识读取系统属性, 基本是通过System.getProperty()可读取的属性, jvm参数等
some.property: 属性的key
default_value: 默认值
- eg: $
从系统属性中读取test.log.path属性值, 未取到时默认值为/opt/logs/
env | 环境变量 | System.getenv(xxx)
env
基本上和sys
一致, 不同点在于读取系统环境变量
main | 启动参数 | args
- 假定启动参数:
--log.appender.kafka.producer.bootstrap.servers 127.0.0.1:9092 -logLevel INFO
log4j2.xml 的 pattern
<Property name="log.layout.consolePattern">
${main:\\-logLevel} | ${main:\\-\-log\.appender\.kafka\.producer\.bootstrap\.servers} | %c{1}:%L - %m%n
</Property>
%X{mdcVariable}
| slf4j的MDC机制 | MDC.put/get/remove
- 推荐文献
Slf4j的 MDC 机制
- 定位:日志门面框架,不负责具体实现
SLF4J
(Simple Logging Facade for Java)是一个日志门面框架(Facade),它允许开发者在代码中使用统一的日志接口,而底层可以切换不同的日志框架(如 Logback、Log4j2 等)。
SLF4J 提供了MDC
(Mapped Diagnostic Context
,映射诊断上下文)机制,用于在日志中添加上下文信息,以便更好地跟踪和调试日志。
- 经典应用场景
- 需要将业务系统全局唯一的流程流水id打印到log4j2的日志文件中
即可将流水ID注入到 MDC 中,再在 log4j2的配置文件中引用该变量,即可输出
- MDC 的作用
MDC 是一个线程安全的上下文存储机制,允许开发者在日志中添加与当前线程相关的动态信息。
这些信息可以是用户 ID、请求 ID、事务 ID 等,有助于在复杂的日志中快速定位问题。
MDC 源码解读
MDC 对外开放的API / 应用程序中如何使用 MDC
public class MDC {
//Put a context value as identified by key
//into the current thread's context map.
public static void put(String key, String val);
//Get the context identified by the key parameter.
public static String get(String key);
//Remove the context identified by the key parameter.
public static void remove(String key);
//Clear all entries in the MDC.
public static void clear();
}
- 设置 MDC 值:使用
MDC.put(String key, String value)
方法将键值对放入当前线程的上下文中。- 获取 MDC 值:使用
MDC.get(String key)
方法从上下文中获取值。- 清除 MDC 值:使用
MDC.clear()
方法清除当前线程的上下文。- 在日志中使用 MDC:在日志框架的配置文件中,可以通过占位符(如
%X{key}
)引用 MDC 中的值。
源码分析:以MDC#put
方法为入口
public class MDC {
static final String NULL_MDCA_URL = "http://www.slf4j.org/codes.html#null_MDCA";
static final String NO_STATIC_MDC_BINDER_URL = "http://www.slf4j.org/codes.html#no_static_mdc_binder";
static MDCAdapter mdcAdapter;
/**
* An adapter to remove the key when done.
*/
public static class MDCCloseable implements Closeable {
private final String key;
private MDCCloseable(String key) {
this.key = key;
}
public void close() {
MDC.remove(this.key);
}
}
private MDC() {
}
/**
* As of SLF4J version 1.7.14, StaticMDCBinder classes shipping in various bindings
* come with a getSingleton() method. Previously only a public field called SINGLETON
* was available.
*
* @return MDCAdapter
* @throws NoClassDefFoundError in case no binding is available
* @since 1.7.14
*/
private static MDCAdapter bwCompatibleGetMDCAdapterFromBinder() throws NoClassDefFoundError {
try {
return StaticMDCBinder.getSingleton().getMDCA();
} catch (NoSuchMethodError nsme) {
// binding is probably a version of SLF4J older than 1.7.14
return StaticMDCBinder.SINGLETON.getMDCA();
}
}
//静态代码块,调用put方法时,先执行
static {
try {
mdcAdapter = bwCompatibleGetMDCAdapterFromBinder();
} catch (NoClassDefFoundError ncde) {
mdcAdapter = new NOPMDCAdapter();
String msg = ncde.getMessage();
if (msg != null && msg.contains("StaticMDCBinder")) {
Util.report("Failed to load class \"org.slf4j.impl.StaticMDCBinder\".");
Util.report("Defaulting to no-operation MDCAdapter implementation.");
Util.report("See " + NO_STATIC_MDC_BINDER_URL + " for further details.");
} else {
throw ncde;
}
} catch (Exception e) {
// we should never get here
Util.report("MDC binding unsuccessful.", e);
}
}
/**
* Put a diagnostic context value (the <code>val</code> parameter) as identified with the
* <code>key</code> parameter into the current thread's diagnostic context map. The
* <code>key</code> parameter cannot be null. The <code>val</code> parameter
* can be null only if the underlying implementation supports it.
*
* <p>
* This method delegates all work to the MDC of the underlying logging system.
*
* @param key non-null key
* @param val value to put in the map
*
* @throws IllegalArgumentException
* in case the "key" parameter is null
*/
public static void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key parameter cannot be null");
}
if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
}
mdcAdapter.put(key, val);
}
}
Log4jMDCAdapter
public class Log4jMDCAdapter implements MDCAdapter {
@Override
public void put(final String key, final String val) {
ThreadContext.put(key, val);
}
.....
}
CopyOnWriteSortedArrayThreadContextMap
最终进入CopyOnWriteSortedArrayThreadContextMap中
class CopyOnWriteSortedArrayThreadContextMap implements ReadOnlyThreadContextMap, ObjectThreadContextMap, CopyOnWrite {
.......
private final ThreadLocal<StringMap> localMap;
//构造方法
public CopyOnWriteSortedArrayThreadContextMap() {
this.localMap = createThreadLocalMap();
}
// LOG4J2-479: by default, use a plain ThreadLocal, only use InheritableThreadLocal if configured.
// (This method is package protected for JUnit tests.)
private ThreadLocal<StringMap> createThreadLocalMap() {
if (inheritableMap) {
return new InheritableThreadLocal<StringMap>() {
@Override
protected StringMap childValue(final StringMap parentValue) {
if (parentValue == null) {
return null;
}
final StringMap stringMap = createStringMap(parentValue);
stringMap.freeze();
return stringMap;
}
};
}
// if not inheritable, return plain ThreadLocal with null as initial value
return new ThreadLocal<>();
}
......
}
分析结论
- 到此,我们可以看到
MDC
底层用的是ThreadLocal
。
a)ThreadLocal 很多地方叫做线程本地变量,也有些地方叫做线程本地存储。
b)ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
c)ThreadLocal 使用场景为用来解决数据库连接、Session 管理等。
主要说明了两点:
MDC 主要用于保存上下文,区分不同的请求来源。
MDC 管理是按线程划分,并且子线程会自动继承母线程的上下文。
InheritableThreadLocal 说明:该类扩展了 ThreadLocal,为子线程提供从父线程那里继承的值:在创建子线程时,子线程会接收所有可继承的线程局部变量的初始值,以获得父线程所具有的值。通常,子线程的值与父线程的值是一致的;但是,通过重写这个类中的 childValue 方法,子线程的值可以作为父线程值的一个任意函数。
案例
- step1 在应用程序运行过程中注入 MDC 变量,业务场景退出时注销MDC变量
例如:用户HTTP请求过程 / 程序启停过程 / ...
import lombok.extern.slf4j.Slf4j;
import org.example.app.constant.Constants;
import org.slf4j.MDC;
import java.util.UUID;
@Slf4j
public class LogTest {
private final static String TRACE_ID_PARAM = "traceId";
public static void main(String[] args) {
//注入 MDC 变量
MDC.put(TRACE_ID_PARAM, String.valueOf( UUID.randomUUID() ) );
log.info("running for handle business! {}:{}", TRACE_ID_PARAM, MDC.get(TRACE_ID_PARAM) );//处理业务逻辑
// 清理MDC
//MDC.clear();
MDC.remove( TRACE_ID_PARAM );//或仅清理需要的属性
log.info("clear MDC after | {} : {}", TRACE_ID_PARAM, MDC.get( TRACE_ID_PARAM ) );
}
}
- log4j2.xml : pattern 中 读取 MDC 变量
...
[%d{yyyy/MM/dd HH:mm:ss.SSS}] [%X{traceId}] [%-5p] [%t] [%C{1}.java:%L %M] %m%n
...
demo log
[2025/02/22 21:04:04.707] [55fab1b9-b171-4af7-ad2c-18f1ac56d378] [INFO ] [main] [LogTest.java:26 main] running for handle business! traceId:55fab1b9-b171-4af7-ad2c-18f1ac56d378
[2025/02/22 21:04:04.710] [] [INFO ] [main] [LogTest.java:31 main] clear MDC after | traceId : null
总之,MDC 是 SLF4J 提供的一种强大的机制,允许开发者在日志中添加上下文信息。它特别适用于多线程环境(如 Web 应用),可以帮助开发者快速定位问题,而无需额外的日志解析工具。
总览
Prefix | Context | Usage/Demo |
---|---|---|
base64 | Base64 encoded data. The format is base64:Base64encodeddata.Forexample:{base64:SGVsbG8gV29ybGQhCg==} yields Hello World!. | ${base64:SGVsbG8gV29ybGQhCg==} (未试验OK;可尝试自定义 Base64Layout) |
bundle | Resource bundle. The format is bundle:BundleName:BundleKey.Thebundlenamefollowspackagenamingconventions,forexample:{bundle:com.domain.Messages:MyKey}. | |
ctx | Thread Context Map (MDC) | 读取自定义的MDC属性: [%X{log.appender.kafka.producer.bootstrap.servers}] |
date | Inserts the current date and/or time using the specified format | %d{yyyy-MM-dd HH:mm:ss.SSS} |
env | System environment variables. The formats are env:ENVNAMEand{env:ENV_NAME:-default_value}. | ${env:HOST_IP:-127.0.0.1} |
jndi | A value set in the default JNDI Context. | $${jndi:app_name} |
jvmrunargs | A JVM input argument accessed through JMX, but not a main argument; see RuntimeMXBean.getInputArguments(). Not available on Android. | 略 |
log4j | Log4j configuration properties. The expressions log4j:configLocationand{log4j:configParentLocation} respectively provide the absolute path to the log4j configuration file and its parent folder. | 略 |
main | A value set with MapLookup.setMainArguments(String[]) | ${main:0} / ${main:\\-logLevel} :读取应用程序启动参数-logLevel 、${main:\\-\-log\.appender\.kafka\.producer\.bootstrap\.servers} |
map | A value from a MapMessage | 略 |
sd | A value from a StructuredDataMessage. The key "id" will return the name of the StructuredDataId without the enterprise number. The key "type" will return the message type. Other keys will retrieve individual elements from the Map. | 略 |
sys | System properties. The formats are sys:some.propertyand{sys:some.property:-default_value}. | ${sys:java.home}% |
注意事项
- 个人实践观点:
sys/env/main/...
等配置变量的 lookup 方法,仅适用于 Appender 的 pattern 中;针对 日志框架(Log4j2) 在更早时/在框架启动时即需要出入到 KafkaAppender 的参数变量,需另寻方法
二者在日志框架运行的的生命周期顺序中是不同的,需要理解到这一点。
后者更早,在业务类中logger
作为static
属性,将最早开始执行日志框架的启动程序(LoggerFactory.getLogger(XXX.class)
)
- 问题:java 中 static 属性 和 static 代码块,哪个先执行?
静态变量初始化和静态代码块的执行顺序是:按照它们在类中出现的顺序进行的。
静态变量初始化先于静态代码块执行,但静态代码块可以访问已经初始化的静态变量。
public class LogTestEntry {
static {
System.out.println("hello");//code 1
}
private static final Logger logger = LoggerFactory.getLogger(LogTestEntry.class);//code 2
public static void main(String[] args) throws Exception {
//... //code 3
}
Y 推荐文献
Java 内置的 ResourceBundle / Locale
- Apache Log4j 2.x
X 参考文献

本文链接: https://www.cnblogs.com/johnnyzen/p/18731370
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章