Log4J使用外部配置
使用 Java语言开发的程序员和爱好者们应当对log4j并不陌生。 它是一个Java的日志系统实现。log4j主页
解决方案
先上解决方案一便以后查阅。
- 更改位置需要放在Main类的静态初始化块最开始的位置。
- 两个方式:
- 配置系统变量log4j.defaultInitOveride。此方法要比较繁琐,且对用户行为不可控,不推荐这种。
- 在代码中使用
System.setProperty("log4j.defaultInitOverride", "true")
配置变量。这个接口在初始时调用,完全程序员指定,即使外部有配置也可覆盖,所以推荐这种方式来避免用户的奇葩行为。
- 调用
PropertyConfigurator.configure
接口指定外部配置的位置。
背景
最近做一个决策服务器,为了跟踪调试我需要大量的Debug级别的日志,这台服务器在部署生产环境时则不需要这么多日志。我决定让用户使用外部自己的日志配置来定义日志,如果用户没有指定自定义的日志配置,那么就使用默认内建的日志配置。这样也方便随时更改日志而无需重新打包(默认的配置会被打进jar包)。
踩坑过程
如果不做额外的操作, 那么log4j的配置文件默认去代码目录寻找配置文件。 经过谷歌搜了一下如何使用自定义配置文件,得到了一个方法:
1 |
|
我只使用了Configurator.configure 来指定了外部配置文件后发现,只是外部的配置文件替换了默认配置的配置,但是没有删除外部配置的Logger。明显与预期不符。
于是开始阅读 Configurator.configure 的实现代码。以下是adoptOpenJdk14版本的源码:
1 |
|
我发现,接口的最后是用LogManager.getLoggerRepository()。
如果使用默认位置配置的话,那么是无需指定配置的,可以直接通过 LogManager.getLogger() 来获取logger,由此不难想到,默认配置的加载过程
- 要么是在LogManager的静态初始化块中完成。
- 要么是在getLogger()初次调用时完成。
首先检查LogManager的初始化代码:
1 |
|
其中,第12行代码发现,若DEFAULT_INIT_OVERRIDE_KEY这个环境变量被配置且不为"false",就会执行加载默认配置的过程。那我们想要log4j不加载默认配置,只需要将DEFAULT_INIT_OVERRIDE_KEY 变量设置为一个不为"false"的值即可达到目的,这里我使用的是"true"。
1 |
|
DEFAULT_INIT_OVERRIDE_KEY 定义的是log4j.defaultInitOverride
所以,只需要:
- 在操作系统变量配置 log4j.defaultInitOverride=true
- 或者虚拟机启动参数 -Dlog4j.defaultInitOverride=true
- 或者在LogManager类被初始化前的位置调用Java代码
System.setProperty("log4j.defaultInitOverride", "true")
这三个操作任选其一即可。
结合我的需求,我不想让用户有太多干扰能力,也降低用户的使用学习成本,选择了最后一个方案。
1 |
|
额外一些说明
为什么要把initLog4j方法调用放在Main类的静态初始化块中且放在初始化块最前面?
这牵扯到Java虚拟机类加载的机制。
类加载各个过程严格按照图中所示顺序启动。但是它们并不要求按要求结束,但这个不重要。这里重要的是在一个类的相关功能要能使用前,必然会先完成初始化。
其中,我们类的静态代码块在初始化过程中执行。它在类使用前执行完。
那问题就归结在了何时会触发类的加载,这里指的是org.apache.log4j.LogManager首次加载。
按照jvm规范,当执行以下四个指令时,触发类加载:
- new
- getstatic
- putstatic
- invokestatic
其中 getstatic 和 putstatic 两个执行分别是读写类的静态成员产生,而invokestatic指令在调用类的静态方法时产生。 而这些语句,都可以在出现在类的静态初始化块中。所以,Main类的静态初始化块就很有可能使用别的类的静态域,它又可能使用其他类的静态域,这将会构成一个复杂的依赖链。我们很难保持,或者很难长期保持我们的initLog4j方法总在这些之前执行,所以,我们把它放到了一个最稳妥的地方,在Main类最上方,使用一个静态块来抢先完成配置。