log4j源码分析(基于1.2.17版本)

log4j源码分析(版本1.2.17)

liyakun.hit
2017-12-04

1. Log4j使用方式

1.1 引入jar包

在pom文件中添加如下依赖:

log4j
log4j
1.2.17

1.2 代码集成

在源码中集成Log4j的使用示例如下:

1
2
3
4
5
6
7
8
9
import org.apache.log4j.Logger;

public class YK {
private static Logger logger = Logger.getLogger(YK.class);

public static void main(String[] args) {
logger.info("你的消息");
}
}

1.3 修改配置

上面的源码中的修改,只是代表了日志会进入到log4j的系统中,但是日志最终会以什么格式输出到哪些地方还没有被指定,这些信息是通过修改配置文件log4j.properties来指定的,先看一个log4j.properties的配置示例:
log4j.rootLogger=INFO, file, databus

log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.file=${log.file}
log4j.appender.file.append=false
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p %-60c - %m%n

log4j.appender.databus=com.data.databus.DatabusAppender
log4j.appender.databus.channel=container_level_log
log4j.appender.databus.layout=org.apache.log4j.PatternLayout
log4j.appender.databus.threshold=INFO
log4j.appender.databus.layout.ConversionPattern=databus4j %X{ip} %X{user} %X{pid} %X{cid} %d{yyyy-MM-dd_HH:mm:ss} %p %c{1}: %m

简单介绍一下:

  • 首先是要配置log4j.rootLogger,其值的第一个字段INFO代表着rootLogger的日志级别,这个字段是可选项,如果不配置,则默认为所有日志。后面的值每一个都代表一个log处理器(appender),每个appender之间是并列的,每条日志都会经过所有appender的处理。
  • 然后是为每个appender写出它们各个的相关配置,这些相关配置主要包括以下四种:
    • log4j.appender.$name是这个处理器载入的类
    • log4j.appender.$name.$others是要传递给这个类的参数,可以任意多种,只要相应的appender的类能处理就行。
    • log4j.appender.$name.layout是日志的行格式的处理类
    • log4j.appender.$name.layout.$others是要传递给行格式处理类的参数,可以任意多种。

重点介绍一下PatternLayout的参数ConversionPattern,对于格式的控制主要包含两类内容:

  • 字段类型
    • %p: 输出日志信息优先级,即DEBUG,INFO,WARN,ERROR,FATAL,
    • %d: 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyyy-MM-dd HH:mm:ss,SSS},输出类似:2011-10-18 22:10:28,921
    • %r: 输出自应用启动到输出该log信息耗费的毫秒数
    • %c: 输出日志信息所属的类目,通常就是所在类的全名
    • %t: 输出产生该日志事件的线程名
    • %l: 输出日志事件的发生位置,相当于%C.%M(%F:%L)的组合,包括类目名、发生的线程,以及在代码中的行数。
    • %x: 输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像java servlets这样的多客户多线程的应用中。
    • %%: 输出一个”%”字符
    • %F: 输出日志消息产生时所在的文件名称
    • %L: 输出代码中的行号
    • %m: 输出代码中指定的消息,产生的日志具体信息
    • %n: 输出一个回车换行符,Windows平台为”\r\n”,Unix平台为”\n”输出日志信息换行
  • 字段控制
    • %c{2} : 保留类名值的最后两个部分,比如类名是a.b.c.d,那么%c{2}的值为c.d
    • %20c : 指定类名最小的宽度是20,如果类名小于20的话,默认的情况下右对齐。
    • %-20c : 指定类名最小的宽度是20,如果类名小于20的话,”-“号指定左对齐。
    • %.30c : 指定类名最大的宽度是30,如果类名大于30的话,就会将左边多出的字符截掉,但小于30的话也不会有空格。
    • %20.30c : 如果类名小于20就补空格,并且右对齐,如果长于30字符,就把左边的字符截掉
    • %-20.30c : 若名字空间长度小于20,则右边用空格填充;若名字空间长度超过30,截去多余字符

下面解读一下上面的例子中配置文件:

  1. 第一个appender名字叫做file,类型是由log4j官方实现的org.apache.log4j.FileAppender
    • 这个FileAppender要处理一个名为file的参数,这个参数的值是从jvm中读取的变量log.file的值
    • 这个FileAppender还要处理一个名为append的参数,这个参数代表在第一交写入前是否需要把原文件清空
    • 这个FileAppender使用的行格式方法为log4j中实现的PatternLayout,这个格式允许用户灵活地自定义日志格式
    • 这个FileAppender中的PatternLayout接收一个ConversionPattern的参数,这个参数的值代表要使用的行格式,在这个例子中是:
      • [以yyyy-MM-dd HH:mm:ss,SSS为格式的时间] [日志级别(至少5位长度,左对齐)] [类名(至少60位长度,左对齐)] - [消息内容][换行]
  2. 第二个appender的名字叫做databus,类型是由用户自己实现(后面介绍如何自定义appender)的com.data.databus.DatabusAppender
    • 这个appender会处理自己定义好的参数channel,threshold
    • 这个appender使用的行格式方法也是PatternLayout
    • 这个appender使用的PatternLayout的ConversionPattern参数有所不同,并且包含一些不存在上面介绍的字段类型,比如%X{ip} %X{user} %X{pid} %X{cid}这些,这些是用户自定义的,后面会介绍。

2. 源码分析

用户直接使用的代码一共只有两行,分别是:

  • 初始化方法:private static Logger logger = Logger.getLogger(YK.class);
  • 添加日志方法:logger.info(“你的消息”);

下面分别沿着这两条线进行分析。

2.1 初始化方法

从private static Logger logger = Logger.getLogger(YK.class);这行代码入手。从Import中可以看到Logger类的位置是:org.apache.log4j.Logger。找到它的getLogger(Class)方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
//位置:log4j/src/main/java/org/apache/log4j/Logger.java
/**
* Shorthand for <code>getLogger(clazz.getName())</code>.
*
* @param clazz The name of <code>clazz</code> will be used as the
* name of the logger to retrieve. See {@link #getLogger(String)}
* for more detailed information.
*/
static
public
Logger getLogger(Class clazz) {
return LogManager.getLogger(clazz.getName());
}

跟进到LogManager里面,找到getLogger(String)方法:

1
2
3
4
5
6
7
8
9
10
//位置:log4j/src/main/java/org/apache/log4j/LogManager.java  
/**
Retrieve the appropriate {@link Logger} instance.
*/
public
static
Logger getLogger(String name) {
// Delegate the actual manufacturing of the logger to the logger repository.
return repositorySelector.getLoggerRepository().getLogger(name);
}

到这里先暂停不进入getLogger(name)方法的内部,先要找到静态成员变量repositorySelector初始化的位置如下:

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
45
46
47
48
49
50
51
52
53
54
//位置:log4j/src/main/java/org/apache/log4j/LogManager.java
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);
}
}

// If we have a non-null url, then delegate the rest of the
// configuration to the OptionConverter.selectAndConfigure
// method.
if(url != null) {
LogLog.debug("Using URL ["+url+"] for automatic log4j configuration.");
OptionConverter.selectAndConfigure(url, configuratorClassName,
LogManager.getLoggerRepository());
} else {
LogLog.debug("Could not find resource: ["+configurationOptionStr+"].");
}
}
}
  1. 首先是创建了一个Hierarchy的实例h,它实现了LoggerRepository接口,是一个Logger的容器。
  2. 然后使用h创建了DefaultRepositorySelector(默认的Logger容器选择器)的实例repositorySelector。
  3. 接下来是找配置文件的过程,先找log4j.configuration,如果没有,再找log4j.xml,如果还没有,再找log4j.properties
  4. 最后,调用OptionConverter.selectAndConfigure(url, configuratorClassName, LogManager.getLoggerRepository());方法,对Logger容器进行初始化。

下面继续跟进到OptionConverter类中:

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
45
46
47
//位置:log4j/src/main/java/org/apache/log4j/helpers/OptionConverter.java
/**
Configure log4j given a URL.

<p>The url must point to a file or resource which will be interpreted by
a new instance of a log4j configurator.

<p>All configurations steps are taken on the
<code>hierarchy</code> passed as a parameter.

<p>
@param url The location of the configuration file or resource.
@param clazz The classname, of the log4j configurator which will parse
the file or resource at <code>url</code>. This must be a subclass of
{@link Configurator}, or null. If this value is null then a default
configurator of {@link PropertyConfigurator} is used, unless the
filename pointed to by <code>url</code> ends in '.xml', in which case
{@link org.apache.log4j.xml.DOMConfigurator} is used.
@param hierarchy The {@link org.apache.log4j.Hierarchy} to act on.

@since 1.1.4 */

static
public
void selectAndConfigure(URL url, String clazz, LoggerRepository hierarchy) {
Configurator configurator = null;
String filename = url.getFile();

if(clazz == null && filename != null && filename.endsWith(".xml")) {
clazz = "org.apache.log4j.xml.DOMConfigurator";
}

if(clazz != null) {
LogLog.debug("Preferred configurator class: " + clazz);
configurator = (Configurator) instantiateByClassName(clazz,
Configurator.class,
null);
if(configurator == null) {
LogLog.error("Could not instantiate configurator ["+clazz+"].");
return;
}
} else {
configurator = new PropertyConfigurator();
}

configurator.doConfigure(url, hierarchy);
}

首先根据参数选择合适的解析类,这里假设使用了log4j.properties,那么会使用PropertyConfigurator来进行解析,下面进入它的doConfigure方法:

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
//位置:log4j/src/main/java/org/apache/log4j/PropertyConfigurator.java
/**
Read configuration options from url <code>configURL</code>.
*/
public
void doConfigure(java.net.URL configURL, LoggerRepository hierarchy) {
Properties props = new Properties();
LogLog.debug("Reading configuration from URL " + configURL);
InputStream istream = null;
try {
istream = configURL.openStream();
props.load(istream);
}
catch (Exception e) {
LogLog.error("Could not read configuration file from URL [" + configURL
+ "].", e);
LogLog.error("Ignoring configuration file [" + configURL +"].");
return;
}
finally {
if (istream != null) {
try {
istream.close();
} catch(Exception ignore) {
}
}
}
doConfigure(props, hierarchy);
}

这个方法主要是读取了配置文件,放到props对象里面,然后调用doConfigure(props, hierarchy)方法,继续跟进:

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
//位置:log4j/src/main/java/org/apache/log4j/PropertyConfigurator.java
/**
Read configuration options from <code>properties</code>.

See {@link #doConfigure(String, LoggerRepository)} for the expected format.
*/
public
void doConfigure(Properties properties, LoggerRepository hierarchy) {

String value = properties.getProperty(LogLog.DEBUG_KEY);
if(value == null) {
value = properties.getProperty("log4j.configDebug");
if(value != null)
LogLog.warn("[log4j.configDebug] is deprecated. Use [log4j.debug] instead.");
}

if(value != null) {
LogLog.setInternalDebugging(OptionConverter.toBoolean(value, true));
}

String thresholdStr = OptionConverter.findAndSubst(THRESHOLD_PREFIX,
properties);
if(thresholdStr != null) {
hierarchy.setThreshold(OptionConverter.toLevel(thresholdStr,
(Level) Level.ALL));
LogLog.debug("Hierarchy threshold set to ["+hierarchy.getThreshold()+"].");
}

configureRootCategory(properties, hierarchy);
configureLoggerFactory(properties);
parseCatsAndRenderers(properties, hierarchy);

LogLog.debug("Finished configuring.");
// We don't want to hold references to appenders preventing their
// garbage collection.
registry.clear();
}
  1. 首先配置是否打开自己的调试(打日志)功能,然后找log4j.threshold来设置最低的日志处理级别。
  2. configureRootCategory是用来处理配置文件中的rootLogger的。这个下面会详细介绍。
  3. configureLoggerFactory是用来配置logger的工厂类
  4. parseCatsAndRenderers是用来配置一些非rootLogger这些配置的其它配置。比如打印对象时的render方法,String类型不支持render,只能使用layout控制。

下面仔细看一下configureRootCategory方法里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//位置:log4j/src/main/java/org/apache/log4j/PropertyConfigurator.java
void configureRootCategory(Properties props, LoggerRepository hierarchy) {
String effectiveFrefix = ROOT_LOGGER_PREFIX;
String value = OptionConverter.findAndSubst(ROOT_LOGGER_PREFIX, props);

if(value == null) {
value = OptionConverter.findAndSubst(ROOT_CATEGORY_PREFIX, props);
effectiveFrefix = ROOT_CATEGORY_PREFIX;
}

if(value == null)
LogLog.debug("Could not find root logger information. Is this OK?");
else {
Logger root = hierarchy.getRootLogger();
synchronized(root) {
parseCategory(props, root, effectiveFrefix, INTERNAL_ROOT_NAME, value);
}
}
}

先找”log4j.rootLogger”,再找”log4j.rootCategory”,这个rootCategory是历史遗留问题,可以忽略。看到这个同步锁,就知道关键点要来了,下面进入到parseCategory()方法里面。

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
45
46
47
48
49
50
51
52
53
54
55
56
//位置:log4j/src/main/java/org/apache/log4j/PropertyConfigurator.java
/**
This method must work for the root category as well.
*/
void parseCategory(Properties props, Logger logger, String optionKey,
String loggerName, String value) {

LogLog.debug("Parsing for [" +loggerName +"] with value=[" + value+"].");
// We must skip over ',' but not white space
StringTokenizer st = new StringTokenizer(value, ",");

// If value is not in the form ", appender.." or "", then we should set
// the level of the loggeregory.

if(!(value.startsWith(",") || value.equals(""))) {

// just to be on the safe side...
if(!st.hasMoreTokens())
return;

String levelStr = st.nextToken();
LogLog.debug("Level token is [" + levelStr + "].");

// If the level value is inherited, set category level value to
// null. We also check that the user has not specified inherited for the
// root category.
if(INHERITED.equalsIgnoreCase(levelStr) ||
NULL.equalsIgnoreCase(levelStr)) {
if(loggerName.equals(INTERNAL_ROOT_NAME)) {
LogLog.warn("The root logger cannot be set to null.");
} else {
logger.setLevel(null);
}
} else {
logger.setLevel(OptionConverter.toLevel(levelStr, (Level) Level.DEBUG));
}
LogLog.debug("Category " + loggerName + " set to " + logger.getLevel());
}

// Begin by removing all existing appenders.
logger.removeAllAppenders();

Appender appender;
String appenderName;
while(st.hasMoreTokens()) {
appenderName = st.nextToken().trim();
if(appenderName == null || appenderName.equals(","))
continue;
LogLog.debug("Parsing appender named \"" + appenderName +"\".");
appender = parseAppender(props, appenderName);
if(appender != null) {
logger.addAppender(appender);
}
}
}

  1. 首先为rootlogger设置LEVEL
  2. 然后调用removeAllAppenders方法清除所有的appender
  3. 然后开始依次添加appender。具体来说,就是先在rootLogger那一行找到一个名字,然后调用parseAppender(props, appenderName)看能否实例化一个Appender,如果可以实例化成功,那就把它添加到logger中。这里不再继续跟进到logger.addAppender(appender)方法内部了,但是要提一点,就是这个方法中,在logger对象中创建了一个AppenderAttachableImpl的对象aai,然后调用aai.addAppender(newAppender)方法,把这个appender放入到aai中了,后面打印日志的时候还需要使用这个对象。

下面再跟进到parseAppender(props, appenderName)方法中:

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
//位置:log4j/src/main/java/org/apache/log4j/PropertyConfigurator.java
Appender parseAppender(Properties props, String appenderName) {
Appender appender = registryGet(appenderName);
if((appender != null)) {
LogLog.debug("Appender \"" + appenderName + "\" was already parsed.");
return appender;
}
// Appender was not previously initialized.
String prefix = APPENDER_PREFIX + appenderName;
String layoutPrefix = prefix + ".layout";

appender = (Appender) OptionConverter.instantiateByKey(props, prefix,
org.apache.log4j.Appender.class,
null);
if(appender == null) {
LogLog.error(
"Could not instantiate appender named \"" + appenderName+"\".");
return null;
}
appender.setName(appenderName);

if(appender instanceof OptionHandler) {
if(appender.requiresLayout()) {
Layout layout = (Layout) OptionConverter.instantiateByKey(props,
layoutPrefix,
Layout.class,
null);
if(layout != null) {
appender.setLayout(layout);
LogLog.debug("Parsing layout options for \"" + appenderName +"\".");
//configureOptionHandler(layout, layoutPrefix + ".", props);
PropertySetter.setProperties(layout, props, layoutPrefix + ".");
LogLog.debug("End of parsing for \"" + appenderName +"\".");
}
}
//configureOptionHandler((OptionHandler) appender, prefix + ".", props);
PropertySetter.setProperties(appender, props, prefix + ".");
LogLog.debug("Parsed \"" + appenderName +"\" options.");
}
registryPut(appender);
return appender;
}

  1. 先看是否已经添加了,如果已经添加,就返回
  2. 然后找到appenderName对应的类,通过反射进行实例化,设置appendername。
  3. 再找到appenderName使用的layout对应的类,通过反射进行实例化。
  4. 最后返回这个实例化的appender。

下面回到LogManager类里面,沿着一开始跟进的路线继续:

repositorySelector.getLoggerRepository()这个方法返回的对象是Hierarchy的实例,下面进入它的getLogger(name)方法内部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//位置:log4j/src/main/java/org/apache/log4j/Hierarchy.java
/**
Return a new logger instance named as the first parameter using
the default factory.

<p>If a logger of that name already exists, then it will be
returned. Otherwise, a new logger will be instantiated and
then linked with its existing ancestors as well as children.

@param name The name of the logger to retrieve.

*/
public
Logger getLogger(String name) {
return getLogger(name, defaultFactory);
}

这里的defaultFactory是在构造函数里面创建的DefaultCategoryFactory类的实例,下面继续跟进到getLogger(name, defaultFactory)方法内部:

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
45
46
47
48
//位置:log4j/src/main/java/org/apache/log4j/Hierarchy.java
/**
Return a new logger instance named as the first parameter using
<code>factory</code>.

<p>If a logger of that name already exists, then it will be
returned. Otherwise, a new logger will be instantiated by the
<code>factory</code> parameter and linked with its existing
ancestors as well as children.

@param name The name of the logger to retrieve.
@param factory The factory that will make the new logger instance.

*/
public
Logger getLogger(String name, LoggerFactory factory) {
//System.out.println("getInstance("+name+") called.");
CategoryKey key = new CategoryKey(name);
// Synchronize to prevent write conflicts. Read conflicts (in
// getChainedLevel method) are possible only if variable
// assignments are non-atomic.
Logger logger;

synchronized(ht) {
Object o = ht.get(key);
if(o == null) {
logger = factory.makeNewLoggerInstance(name);
logger.setHierarchy(this);
ht.put(key, logger);
updateParents(logger);
return logger;
} else if(o instanceof Logger) {
return (Logger) o;
} else if (o instanceof ProvisionNode) {
//System.out.println("("+name+") ht.get(this) returned ProvisionNode");
logger = factory.makeNewLoggerInstance(name);
logger.setHierarchy(this);
ht.put(key, logger);
updateChildren((ProvisionNode) o, logger);
updateParents(logger);
return logger;
}
else {
// It should be impossible to arrive here
return null; // but let's keep the compiler happy.
}
}
}

先对全局的Hashtable的对象ht进行同步锁,然后从里面根据key名取出logger对象,根据logger对象的不同,有以下三种情况:

  • logger对象为null,此时调用DefaultCategoryFactory类中的makeNewLoggerInstance方法创建一个Logger的实例
  • logger对象为一个Logger类的实例,那么直接返回它
  • logger对象为一个ProvisionNode类的实例,那么也通过DefaultCategoryFactory类中的makeNewLoggerInstance方法创建一个Logger的实例,并重新梳理一下各个logger的父子关系。再返回这个Logger类的实例。

这里的的ProvisionNode类是什么含义呢?其实Log4j对于logger的管理是树形的,比如loger(“x.y.z”)的父亲节点是loger(“x.y”),如果只创建了loger(“x.y.z”)而没有实例化loger(“x.y”),就会先放置一个ProvisionNode类(其实就是一个Vector)的实例来充当父亲节点。

无论如何,最终是返回了一个Logger类(log4j/src/main/java/org/apache/log4j/Logger.java)的实例。

2.2 添加日志方法

从用户使用的这一行代码logger.info(“你的消息”)入手。

根据上面的初始化方法的分析,可以得知,这里的logger方法其实是一个Logger类的实例。但是log4j/src/main/java/org/apache/log4j/Logger.java这个类中并没有声明info(String)这个方法,是因Logger类继承了Category类,下面看一下Category类中的Info(String)方法:

1
2
3
4
5
6
7
8
//位置:log4j/src/main/java/org/apache/log4j/Category.java
public
void info(Object message) {
if(repository.isDisabled(Level.INFO_INT))
return;
if(Level.INFO.isGreaterOrEqual(this.getEffectiveLevel()))
forcedLog(FQCN, Level.INFO, message, null);
}

先看一下是否禁用了INFO这个Level,然后确认一下是否比当前有效的level要大,就调用forcedLog方法,下面继续跟进:

1
2
3
4
5
6
7
8
//位置:log4j/src/main/java/org/apache/log4j/Category.java
/**
This method creates a new logging event and logs the event
without further checks. */
protected
void forcedLog(String fqcn, Priority level, Object message, Throwable t) {
callAppenders(new LoggingEvent(fqcn, this, level, message, t));
}

这个方法比较简单,先是创建了一个LoggingEvent对象,然后继续跟进到callAppenders方法中:

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
//位置:log4j/src/main/java/org/apache/log4j/Category.java
/**
Call the appenders in the hierrachy starting at
<code>this</code>. If no appenders could be found, emit a
warning.

<p>This method calls all the appenders inherited from the
hierarchy circumventing any evaluation of whether to log or not
to log the particular log request.

@param event the event to log. */
public
void callAppenders(LoggingEvent event) {
int writes = 0;

for(Category c = this; c != null; c=c.parent) {
// Protected against simultaneous call to addAppender, removeAppender,...
synchronized(c) {
if(c.aai != null) {
writes += c.aai.appendLoopOnAppenders(event);
}
if(!c.additive) {
break;
}
}
}

if(writes == 0) {
repository.emitNoAppenderWarning(this);
}
}

这里的外层循环,是由于log4j的树形结构管理的功能,一个logger要处理的日志,它的父亲节点也要处理。
循环里面首先是以当前logger对象进行同步锁,然后把输入的LoggingEvent对象放到类AppenderAttachableImpl的appendLoopOnAppenders方法中。如果一个也没有放入,就打印一个warning。

下面跟进到appendLoopOnAppenders(LoggingEvent)方法内部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//位置:log4j/src/main/java/org/apache/log4j/helpers/AppenderAttachableImpl.java
/**
Call the <code>doAppend</code> method on all attached appenders. */
public
int appendLoopOnAppenders(LoggingEvent event) {
int size = 0;
Appender appender;

if(appenderList != null) {
size = appenderList.size();
for(int i = 0; i < size; i++) {
appender = (Appender) appenderList.elementAt(i);
appender.doAppend(event);
}
}
return size;
}

这个方法内部,就是简单的把每个appender遍历了一下,然后依次调用了各自的doAppend(LoggingEvent)方法。

这里的appender的具体实现可能会有很多种,现在为了介绍方便,下面就以org.apache.log4j.FileAppender这个类来进行介绍,同时,使用org.apache.log4j.PatternLayout来作为介绍的layout。

下面进入到FileAppender类的doAppend(LoggingEvent)方法,这个类继承了WriterAppender类,然后WriterAppender类继承了AppenderSkeleton抽象类,在AppenderSkeleton这个抽象类中定义了doAppend(LoggingEvent)方法,下面跟进到这个类中:

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
//位置:log4j/src/main/java/org/apache/log4j/AppenderSkeleton.java
/**
* This method performs threshold checks and invokes filters before
* delegating actual logging to the subclasses specific {@link
* AppenderSkeleton#append} method.
* */
public
synchronized
void doAppend(LoggingEvent event) {
if(closed) {
LogLog.error("Attempted to append to closed appender named ["+name+"].");
return;
}

if(!isAsSevereAsThreshold(event.getLevel())) {
return;
}

Filter f = this.headFilter;

FILTER_LOOP:
while(f != null) {
switch(f.decide(event)) {
case Filter.DENY: return;
case Filter.ACCEPT: break FILTER_LOOP;
case Filter.NEUTRAL: f = f.getNext();
}
}

this.append(event);
}
  1. 这是一个类级别同步的方法
  2. 首先看是否已经关闭,再看是否能通过过滤,最后调用自己的append(LoggingEvent)方法。

由于FileAppender中依然没有实现append方法,看WriterAppender中的append方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//位置:log4j/src/main/java/org/apache/log4j/WriterAppender.java
public
void append(LoggingEvent event) {

// Reminder: the nesting of calls is:
//
// doAppend()
// - check threshold
// - filter
// - append();
// - checkEntryConditions();
// - subAppend();

if(!checkEntryConditions()) {
return;
}
subAppend(event);
}

这个方法中,只是检查了一些条件,然后调用了subAppend()方法,继续跟进:

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
//位置:log4j/src/main/java/org/apache/log4j/WriterAppender.java
/**
Actual writing occurs here.

<p>Most subclasses of <code>WriterAppender</code> will need to
override this method.

@since 0.9.0 */
protected
void subAppend(LoggingEvent event) {
this.qw.write(this.layout.format(event));

if(layout.ignoresThrowable()) {
String[] s = event.getThrowableStrRep();
if (s != null) {
int len = s.length;
for(int i = 0; i < len; i++) {
this.qw.write(s[i]);
this.qw.write(Layout.LINE_SEP);
}
}
}

if(this.immediateFlush) {
this.qw.flush();
}
}
  1. 首先是调用了layout的format方法,来把LoggingEvent格式化为一个字符串,这个不再详细介绍了,详细的内容可以参考:log4j/src/main/java/org/apache/log4j/helpers/PatternParser.java。
  2. 然后判断layout是否忽略了抛出的异常信息(PatternLayout中会忽略异常信息),如果在layout中忽略了,那么就在WriterAppender中把异常栈信息一行一行的拿出来处理一下。
  3. 最后看一下是否需要立即刷新到文件,默认是true,即每次调用都会刷新到文件中。

然后,在这个方法里面通过this.qw.write(String)来处理日志,这个qw是在FileAppender的构造函数中实例化的,过程如下:

1
2
3
4
5
6
7
//位置:log4j/src/main/java/org/apache/log4j/FileAppender.java
public
FileAppender(Layout layout, String filename, boolean append, boolean bufferedIO,
int bufferSize) throws IOException {
this.layout = layout;
this.setFile(filename, append, bufferedIO, bufferSize);
}

然后进入到setFile方法中:

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
45
46
47
48
49
//位置:log4j/src/main/java/org/apache/log4j/FileAppender.java
public
synchronized
void setFile(String fileName, boolean append, boolean bufferedIO, int bufferSize)
throws IOException {
LogLog.debug("setFile called: "+fileName+", "+append);

// It does not make sense to have immediate flush and bufferedIO.
if(bufferedIO) {
setImmediateFlush(false);
}

reset();
FileOutputStream ostream = null;
try {
//
// attempt to create file
//
ostream = new FileOutputStream(fileName, append);
} catch(FileNotFoundException ex) {
//
// if parent directory does not exist then
// attempt to create it and try to create file
// see bug 9150
//
String parentName = new File(fileName).getParent();
if (parentName != null) {
File parentDir = new File(parentName);
if(!parentDir.exists() && parentDir.mkdirs()) {
ostream = new FileOutputStream(fileName, append);
} else {
throw ex;
}
} else {
throw ex;
}
}
Writer fw = createWriter(ostream);
if(bufferedIO) {
fw = new BufferedWriter(fw, bufferSize);
}
this.setQWForFiles(fw);
this.fileName = fileName;
this.fileAppend = append;
this.bufferedIO = bufferedIO;
this.bufferSize = bufferSize;
writeHeader();
LogLog.debug("setFile ended");
}

这里主要是负责创建目录,然后创建一个OutputStreamWriter对象fw,再调用setQWForFiles方法:

1
2
3
4
5
//位置:log4j/src/main/java/org/apache/log4j/FileAppender.java
protected
void setQWForFiles(Writer writer) {
this.qw = new QuietWriter(writer, errorHandler);
}

这样,qw对象就实例化出来,qw.write(String)方法,就可以把日志写入到相应的文件中。


以上就是Log4j的工作原理,深入了解一下,还是挺有意思的。

后面有时间的时候,争取再写一篇如何定制化实现自己的Appender的文章。