0%

Log4J 使用外部配置

Log4J使用外部配置

使用 Java语言开发的程序员和爱好者们应当对log4j并不陌生。 它是一个Java的日志系统实现。log4j主页

解决方案

先上解决方案一便以后查阅。

  1. 更改位置需要放在Main类的静态初始化块最开始的位置。
  2. 两个方式:
    • 配置系统变量log4j.defaultInitOveride。此方法要比较繁琐,且对用户行为不可控,不推荐这种。
    • 在代码中使用System.setProperty("log4j.defaultInitOverride", "true") 配置变量。这个接口在初始时调用,完全程序员指定,即使外部有配置也可覆盖,所以推荐这种方式来避免用户的奇葩行为。
  3. 调用 PropertyConfigurator.configure接口指定外部配置的位置。

背景

最近做一个决策服务器,为了跟踪调试我需要大量的Debug级别的日志,这台服务器在部署生产环境时则不需要这么多日志。我决定让用户使用外部自己的日志配置来定义日志,如果用户没有指定自定义的日志配置,那么就使用默认内建的日志配置。这样也方便随时更改日志而无需重新打包(默认的配置会被打进jar包)。

踩坑过程

如果不做额外的操作, 那么log4j的配置文件默认去代码目录寻找配置文件。 经过谷歌搜了一下如何使用自定义配置文件,得到了一个方法:

1
PropertyConfigurator.configure();

我只使用了Configurator.configure 来指定了外部配置文件后发现,只是外部的配置文件替换了默认配置的配置,但是没有删除外部配置的Logger。明显与预期不符。

于是开始阅读 Configurator.configure 的实现代码。以下是adoptOpenJdk14版本的源码:

1
2
3
4
5
6
7
static
public
void configure(String configFilename) {
new PropertyConfigurator().doConfigure(configFilename,
LogManager.getLoggerRepository());
}

我发现,接口的最后是用LogManager.getLoggerRepository()。

如果使用默认位置配置的话,那么是无需指定配置的,可以直接通过 LogManager.getLogger() 来获取logger,由此不难想到,默认配置的加载过程

  • 要么是在LogManager的静态初始化块中完成。
  • 要么是在getLogger()初次调用时完成。

首先检查LogManager的初始化代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
 static {
// By default we use a DefaultRepositorySelector which always returns 'h'.
Hierarchy h = new Hierarchy(new RootLogger((Level) Level.DEBUG));
repositorySelector = new DefaultRepositorySelector(h);

/** Search for the properties file log4j.properties in the CLASSPATH. */
String override =OptionConverter.getSystemProperty(DEFAULT_INIT_OVERRIDE_KEY,
null);

// if there is no default init override, then get the resource
// specified by the user or the default config file.
if(override == null || "false".equalsIgnoreCase(override)) {

String configurationOptionStr = OptionConverter.getSystemProperty(
DEFAULT_CONFIGURATION_KEY,
null);

String configuratorClassName = OptionConverter.getSystemProperty(
CONFIGURATOR_CLASS_KEY,
null);

URL url = null;

// if the user has not specified the log4j.configuration
// property, we search first for the file "log4j.xml" and then
// "log4j.properties"
if(configurationOptionStr == null) {
url = Loader.getResource(DEFAULT_XML_CONFIGURATION_FILE);
if(url == null) {
url = Loader.getResource(DEFAULT_CONFIGURATION_FILE);
}
} else {
try {
url = new URL(configurationOptionStr);
} catch (MalformedURLException ex) {
// so, resource is not a URL:
// attempt to get the resource from the class path
url = Loader.getResource(configurationOptionStr);

//////////////////////Ignore Code ////////////////
}
}
}

其中,第12行代码发现,若DEFAULT_INIT_OVERRIDE_KEY这个环境变量被配置且不为"false",就会执行加载默认配置的过程。那我们想要log4j不加载默认配置,只需要将DEFAULT_INIT_OVERRIDE_KEY 变量设置为一个不为"false"的值即可达到目的,这里我使用的是"true"。

1
2
3
4
5
6
7
/**
* @deprecated This variable is for internal use only. It will
* become private in future versions.
*/
public static final String DEFAULT_INIT_OVERRIDE_KEY =
"log4j.defaultInitOverride";

DEFAULT_INIT_OVERRIDE_KEY 定义的是log4j.defaultInitOverride所以,只需要:

  • 在操作系统变量配置 log4j.defaultInitOverride=true
  • 或者虚拟机启动参数 -Dlog4j.defaultInitOverride=true
  • 或者在LogManager类被初始化前的位置调用Java代码System.setProperty("log4j.defaultInitOverride", "true")

这三个操作任选其一即可。

结合我的需求,我不想让用户有太多干扰能力,也降低用户的使用学习成本,选择了最后一个方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Main {
static {
initLog4j();
}

//other static field

public static void main(String[] args) throws Exception {
//ServerStart Code
}

private static void initLog4j() {
//check configuration file
Path configurationPath = Paths.get("cfg", "log4j.properties");
if (!Files.exists(configurationPath) || !Files.isRegularFile(configurationPath)) {
return;
}

try {
PropertyConfigurator.configure(configurationPath.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}

额外一些说明

为什么要把initLog4j方法调用放在Main类的静态初始化块中且放在初始化块最前面?

这牵扯到Java虚拟机类加载的机制。

类加载各个过程严格按照图中所示顺序启动。但是它们并不要求按要求结束,但这个不重要。这里重要的是在一个类的相关功能要能使用前,必然会先完成初始化。

其中,我们类的静态代码块在初始化过程中执行。它在类使用前执行完。

那问题就归结在了何时会触发类的加载,这里指的是org.apache.log4j.LogManager首次加载。

按照jvm规范,当执行以下四个指令时,触发类加载:

  • new
  • getstatic
  • putstatic
  • invokestatic

其中 getstatic 和 putstatic 两个执行分别是读写类的静态成员产生,而invokestatic指令在调用类的静态方法时产生。 而这些语句,都可以在出现在类的静态初始化块中。所以,Main类的静态初始化块就很有可能使用别的类的静态域,它又可能使用其他类的静态域,这将会构成一个复杂的依赖链。我们很难保持,或者很难长期保持我们的initLog4j方法总在这些之前执行,所以,我们把它放到了一个最稳妥的地方,在Main类最上方,使用一个静态块来抢先完成配置。