(1)Apache log4j-1.2.17源码学习笔记http://blog.csdn.net/zilong_zilong/article/details/78715500
(2)Apache log4j-1.2.17问答式学习笔记http://blog.csdn.net/zilong_zilong/article/details/78916626 
(3)JDK Logging源码学习笔记 http://aperise.iteye.com/blog/2411850 

1.log4j-1.2.17介绍 


       断点调试记录日志,是程序员排查问题的2个有效手段,断点调试需要对全盘代码熟门熟路,费时费力,如果代码不开源那么此种方法就不能使用,相对于断点调试,记录日志提供了另外一种更有效的排错方法,预先植入了有效的日志信息,后期只需通过配置文件即可管理日志,借助工具扫描日志文件内容可以有效的监测当前运行的系统是否运行正常。记录日志作为一个通用的重要的模块,所以开源组织分别推出了自己的日志框架,比如Apache Log4j,Apache Log4j 2、Apache Commons Logging、Slf4j、Logback和Jul (Java Util Logging),今天我要分享的是Apache Log4j日志框架。

       谈到日志框架log4j,它有两个分支,分别是log4j-1.xlog4j-2.x。在2015年08月05日,Apache宣布终止关于log4j-1.x的维护和更新,转而全力维护和更新log4j-2.x,官网也引导开发者尽量使用log4j-2.x。log4j-1.x的最后一个版本是log4j-1.2.17,虽然log4f-1.x停止了维护和更新,但是目前仍在各个系统中被广泛使用。

       大多程序员接触log4j-1.2.17可能就是那个深入脑海的log4j.properties,对其初始化和实现原理可能并不关心,毕竟繁忙的编码工作占据了很多时间,但是空闲之余,还是有必要对log4j-1.2.17有个深入了解。log4j-1.2.17是在哪里初始化的?为何配置文件的名字就为log4j.properties?有哪些日志相关输出配置?这些问题将在此博客进行一一解答,只有深入了解了其原理,才能使日志配置更精简和满足日常的排错工作。

 

2.log4j-1.2.17在maven项目中使用回顾

       我们在使用Apache log4j时候,很自然的在项目中引入了如下的maven依赖:

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

       然后也很自然的在maven的目录src/main/resource下创建了log4j.properties文件,配置了如下内容:

log4j.rootLogger=DEBUG,Info,Warn,Error

log4j.appender.Info=org.apache.log4j.DailyRollingFileAppender
log4j.appender.Info.File=${log4j.dir}/info.log
log4j.appender.Info.DatePattern=yyyy-MM-dd'.log'
log4j.appender.Info.layout=org.apache.log4j.PatternLayout
log4j.appender.Info.layout.ConversionPattern=[%-5p] %d{HH:mm:ss:SSS} method:%C{1}.%M%n%m%n
log4j.appender.Info.Threshold=INFO

log4j.appender.Warn=org.apache.log4j.DailyRollingFileAppender
log4j.appender.Warn.File=${log4j.dir}/warn.log
log4j.appender.Warn.DatePattern=yyyy-MM-dd'.log'
log4j.appender.Warn.layout=org.apache.log4j.PatternLayout
log4j.appender.Warn.layout.ConversionPattern=[%-5p] %d{HH:mm:ss:SSS} method:%C{1}.%M%n%m%n
log4j.appender.Warn.Threshold=WARN

log4j.appender.Error=org.apache.log4j.DailyRollingFileAppender
log4j.appender.Error.File=${log4j.dir}/error.log
log4j.appender.Error.DatePattern=yyyy-MM-dd'.log'
log4j.appender.Error.layout=org.apache.log4j.PatternLayout
log4j.appender.Error.layout.ConversionPattern=[%-5p] %d{HH:mm:ss:SSS} method:%C{1}.%M%n%m%n
log4j.appender.Error.Threshold=ERROR

       最后很自然的在我们的类里面使用着Logger打印日志:

package com.log4jtest
public class LoggerTest{
	private static Logger logger = Logger.getLogger(LoggerTest.class);
	public void main(String[] args){
		logger.debug("debug");
		logger.info("info");
		logger.error("error");
	}
}

       一切都感觉理所当然,顺理成章,一气呵成log4j-1.2.17干的事多了,程序员干的事情就相对少了所以感觉log4j-1.2.17如此的好用,所以log4j-1.2.17有这么广泛的使用

       我想说的是程序员不能只满足于此,在空闲的时候不妨静下心来阅读以下log4j-1.2.17的源代码,研究一下其初始化过程,日志输出操作如何进行,日志格式化如何进行的,这样程序员才会一步步朝着架构师的道路前进,提升自己,提升工作效率,在工作中寻找创新点。

 

3.log4j-1.2.17初始化(加载配置文件和解析配置文件)代码分析

       在打印日志时候我们很自然的添加了这行代码:

Logger logger = Logger.getLogger(LoggerTest.class);
       这里就可以作为我们查看Apache log4j-1.2.17源代码的一个入口,在官网下载源码 log4j-1.2.17.zip,查看 Logger.java的代码如下:
  static public Logger getLogger(Class clazz) {
    return LogManager.getLogger(clazz.getName());
  }
       我们看到这里Logger类调用了 LogManager.javagetLogger方法,那么我们有必要查看下LogManager的代码,LogManager里面有个 static代码块,代码如下:
	static {
		//默认设置的日志级别为DEBUGB
		Hierarchy h = new Hierarchy(new RootLogger((Level) Level.DEBUG));
                //默认使用的是DefaultRepositorySelector
		repositorySelector = new DefaultRepositorySelector(h);

		//读取操作系统配置的环境变量log4j.defaultInitOverride的值
		String override = OptionConverter.getSystemProperty(DEFAULT_INIT_OVERRIDE_KEY, null);

		if (override == null || "false".equalsIgnoreCase(override)) {
                // 如果操作系统没有配置环境变量log4j.defaultInitOverride,那么这里会进入
                        //读取操作系统配置的环境变量log4j.configuration的值
			String configurationOptionStr = OptionConverter.getSystemProperty(DEFAULT_CONFIGURATION_KEY, null);
                        //读取操作系统配置的环境变量log4j.configuratorClass的值
			String configuratorClassName = OptionConverter.getSystemProperty(CONFIGURATOR_CLASS_KEY, null);

			URL url = null;

			//如果没有配置环境变量log4j.configuration,那么就去寻找用户是否配置了文件log4j.xml,如果log4j.xml也找不到那么就加载默认配置文件log4j.properties
			if (configurationOptionStr == null) {
				url = Loader.getResource(DEFAULT_XML_CONFIGURATION_FILE);
				if (url == null) {
					url = Loader.getResource(DEFAULT_CONFIGURATION_FILE);
				}
			} else {
				try {
                                       //如果配置了环境变量log4j.configuration,那么以环境变量log4j.configuration的值构造URL对象
					url = new URL(configurationOptionStr);
				} catch (MalformedURLException ex) {
					// so, resource is not a URL:
					// attempt to get the resource from the class path
                                        //环境变量log4j.configuration配置的值非URL,视图在classpath读取相关文件资源
					url = Loader.getResource(configurationOptionStr);
				}
			}

			// 如果url非空,那么让OptionConverter.selectAndConfigure代理剩下的配置过程
			if (url != null) {
				LogLog.debug("Using URL [" + url + "] for automatic log4j configuration.");
				try {
					OptionConverter.selectAndConfigure(url, configuratorClassName, LogManager.getLoggerRepository());
				} catch (NoClassDefFoundError e) {
					LogLog.warn("Error during default initialization", e);
				}
			} else {
				LogLog.debug("Could not find resource: [" + configurationOptionStr + "].");
			}
		} else {
			LogLog.debug("Default initialization of overridden by " + DEFAULT_INIT_OVERRIDE_KEY + "property.");
		}
	}
       上面的代码已经完全解释了Apache log4j-1.2.17如何初始化的核心过程,总结如下:

 

  1. 首先如果操作系统设置了环境变量log4j.defaultInitOverride=false或者没有设置,这里的初始化过程才会执行,否则不执行;
  2. 接着读取操作系统配置的环境变量log4j.configuration的值,该值告诉log4j框架加载哪个配置文件,如果没有配置该环境变量,那么先读取名字为log4j.xml的配置文件,如果log4j.xml也不存在,就读取默认的配置文件log4j.properties;
  3. 试图将步骤2中读取的配置文件转换为URL形式;
  4. 如果步骤3中配置文件不是URL的形式,那么就从工程的classpath路径加载配置文件;
  5. 如果仍然无法找到配置文件,那么就放弃出花花过程。

       用一张流程图解释如下:


       在上面的代码中我们看到配置文件加载的优先顺序为:


       接下来通过工具类OptionConverter.javastatic public void selectAndConfigure(URL url, String clazz, LoggerRepository hierarchy) 方法来做初始化,我们继续跟踪OptionConverter.java的相关代码,注释如下:

/**
 * 通过配置文件初始化log4j-1.2.17
 * @param url 需要加载的资源配置文件
 * @param clazz 采用clazz读取解析配置文件,clazz通过操作系统环境变量log4j.configuratorClass配置,默认值为null
 * @param hierarchy 实现类DefaultRepositorySelector
 */

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

	//采用org.apache.log4j.xml.DOMConfigurator读取解析XML配置文件
	if (clazz == null && filename != null && filename.endsWith(".xml")) {
		clazz = "org.apache.log4j.xml.DOMConfigurator";
	}

	if (clazz != null) {//操作系统环境变量log4j.configuratorClass配置了配置文件读取解析器
		LogLog.debug("Preferred configurator class: " + clazz);
		//采用操作系统环境变量log4j.configuratorClass指定的解析器读取解析配置文件
		configurator = (Configurator) instantiateByClassName(clazz, Configurator.class, null);
		if (configurator == null) {
			LogLog.error("Could not instantiate configurator [" + clazz + "].");
			return;
		}
	} else {//操作系统环境变量log4j.configuratorClass没有配置文件读取解析器
		//采用org.apache.log4j.PropertyConfigurator读取解析XML配置文件
		configurator = new PropertyConfigurator();
	}

	//调用相应的配置文件解析器读取解析配置文件
	configurator.doConfigure(url, hierarchy);
}

       上面代码根据配置文件的类型选择不同的读取解析器进行解析,这里log4j-1.2.17提供的所有的配置文件读取解析器相关类继承关系图如下:


       因为常用的配置文件为log4j.properties,所以这里我以PropertyConfigurator.java类进行讲解,先通过一张处理流程图来对PropertyConfigurator的读取配置过程有个大致了解:


       给出PropertyConfigurator.java的相关注释代码如下:

/**
 * 读取解析配置文件log4j.properties
 */
public void doConfigure(Properties properties, LoggerRepository hierarchy) {
	repository = hierarchy;
	// 如果log4j.properties中配置了log4j.debug=true或者log4j.configDebug=true,其值非空且非false就置默认值true开启log4j-1.2.17自己的日志打印,否则不开启
	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));
	}

	// 如果log4j.properties中配置了log4j.reset=true,其值非空且为true就重置Hierarchy
	String reset = properties.getProperty(RESET_KEY);
	if (reset != null && OptionConverter.toBoolean(reset, false)) {
		hierarchy.resetConfiguration();
	}

	// 如果log4j.properties中配置了log4j.threshold,其值非空且为值(TRACE、DEBUG、INFO、WARN、ERROR、FATAL、OFF、ALL)之一,就设置Hierarchy的thresholdInt和Threshold,非空且不是上述值时候默认设置为ALL
	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() + "].");
	}

	// 开始解析根节点RootLogger
	configureRootCategory(properties, hierarchy);
	// 读取log4j.properties中log4j.loggerFactory配置的值,默认的Logger工厂类为DefaultCategoryFactory,这里会进行覆盖
	configureLoggerFactory(properties);
	// 调用PropertyConfigurator的
	parseCatsAndRenderers(properties, hierarchy);
	// 开始解析非根节点Logger,默认设置非根节点Logger的父节点为RootLogger,从log4j.properties读取log4j.additivity.Appendername值设置Logger的日志传播属性additive
	parseCatsAndRenderers(properties, hierarchy);

	// 如果配置文件log4j.properties里配置了log4j.debug=true或者log4j.configDebug=true,就可以打印此日志来跟踪log4j的加载过程
	LogLog.debug("Finished configuring.");
	// We don't want to hold references to appenders preventing their
	// garbage collection.
	registry.clear();
}

/**
 * 读取解析配置文件log4j.properties
 */
public void doConfigure(java.net.URL configURL, LoggerRepository hierarchy) {
	// 创建Properties对象,用于加载log4j.properties
	Properties props = new Properties();
	// 如果配置文件log4j.properties里配置了log4j.debug=true或者log4j.configDebug=true,就可以打印此日志来跟踪log4j的加载过程
	LogLog.debug("Reading configuration from URL " + configURL);
	InputStream istream = null;
	URLConnection uConn = null;
	try {
		// 打开文件log4j.properties,创建文件流istream,然后用Properties进行加载
		uConn = configURL.openConnection();
		uConn.setUseCaches(false);
		istream = uConn.getInputStream();
		props.load(istream);
	} catch (Exception e) {
		if (e instanceof InterruptedIOException || e instanceof InterruptedException) {
			Thread.currentThread().interrupt();
		}
		// 如果配置文件log4j.properties里配置了log4j.debug=true或者log4j.configDebug=true,就可以打印此日志来跟踪log4j的加载过程
		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 (InterruptedIOException ignore) {
				Thread.currentThread().interrupt();
			} catch (IOException ignore) {
			} catch (RuntimeException ignore) {
			}
		}
	}
	// 读取解析配置文件log4j.properties
	doConfigure(props, hierarchy);
}

/**
 * 读取log4j.properties中log4j.loggerFactory配置的值,
 * 默认的Logger工厂类为DefaultCategoryFactory,这里会进行覆盖
 */
protected void configureLoggerFactory(Properties props) {
	String factoryClassName = OptionConverter.findAndSubst(LOGGER_FACTORY_KEY, props);
	if (factoryClassName != null) {
		LogLog.debug("Setting category factory to [" + factoryClassName + "].");
		loggerFactory = (LoggerFactory) OptionConverter.instantiateByClassName(factoryClassName,
				LoggerFactory.class, loggerFactory);
		PropertySetter.setProperties(loggerFactory, props, FACTORY_PREFIX + ".");
	}
}

/*
 * 开始解析根节点RootLogger
 */
void configureRootCategory(Properties props, LoggerRepository hierarchy) {
	// 从配置文件log4j.properties中读取log4j.rootLogger配置的值
	String effectiveFrefix = ROOT_LOGGER_PREFIX;
	String value = OptionConverter.findAndSubst(ROOT_LOGGER_PREFIX, props);

	if (value == null) {
		// 如果配置文件log4j.properties中log4j.rootLogger找不到,从配置文件log4j.properties中读取log4j.rootCategory配置的值
		value = OptionConverter.findAndSubst(ROOT_CATEGORY_PREFIX, props);
		effectiveFrefix = ROOT_CATEGORY_PREFIX;
	}

	if (value == null)
		// 如果配置文件log4j.properties中log4j.rootLogger找不到并且log4j.rootCategory配置找不到
		// 如果配置文件log4j.properties里配置了log4j.debug=true或者log4j.configDebug=true,就可以打印此日志来跟踪log4j的加载过程
		LogLog.debug("Could not find root logger information. Is this OK?");
	else {
		// 从Hierarchy中取得默认的实现RootLogger
		Logger root = hierarchy.getRootLogger();
		synchronized (root) {
			// 开始解析根节点Logger父类Category
			parseCategory(props, root, effectiveFrefix, INTERNAL_ROOT_NAME, value);
		}
	}
}

/**
 * /开始解析非根节点Logger,默认设置非根节点Logger的父节点为RootLogger,
 * 从log4j.properties读取log4j.additivity.Appendername值设置Logger的日志传播属性additive
 */
protected void parseCatsAndRenderers(Properties props, LoggerRepository hierarchy) {
	Enumeration enumeration = props.propertyNames();
	while (enumeration.hasMoreElements()) {
		String key = (String) enumeration.nextElement();
		if (key.startsWith(CATEGORY_PREFIX) || key.startsWith(LOGGER_PREFIX)) {// 配置文件中配置以log4j.category.或者log4j.logger.打头,这里会执行

			String loggerName = null;
			if (key.startsWith(CATEGORY_PREFIX)) {
				loggerName = key.substring(CATEGORY_PREFIX.length());
			} else if (key.startsWith(LOGGER_PREFIX)) {
				loggerName = key.substring(LOGGER_PREFIX.length());
			}
			String value = OptionConverter.findAndSubst(key, props);
			Logger logger = hierarchy.getLogger(loggerName, loggerFactory);
			synchronized (logger) {
				// 开始解析根节点Logger父类Category
				parseCategory(props, logger, key, loggerName, value);
				// 解析logger的父类Category传播属性additive,additive决定着叶子节点的logger是否将日志输出转发给根节点的logger也打印输出一次
				parseAdditivityForLogger(props, logger, loggerName);
			}
		} else if (key.startsWith(RENDERER_PREFIX)) {// 配置文件中配置以log4j.renderer.打头,这里会执行
			String renderedClass = key.substring(RENDERER_PREFIX.length());
			String renderingClass = OptionConverter.findAndSubst(key, props);
			if (hierarchy instanceof RendererSupport) {
				RendererMap.addRenderer((RendererSupport) hierarchy, renderedClass, renderingClass);
			}
		} else if (key.equals(THROWABLE_RENDERER_PREFIX)) {// 配置文件中配置以log4j.throwableRenderer打头,这里会执行
			if (hierarchy instanceof ThrowableRendererSupport) {
				ThrowableRenderer tr = (ThrowableRenderer) OptionConverter.instantiateByKey(props,
						THROWABLE_RENDERER_PREFIX, org.apache.log4j.spi.ThrowableRenderer.class, null);
				if (tr == null) {
					LogLog.error("Could not instantiate throwableRenderer.");
				} else {
					PropertySetter setter = new PropertySetter(tr);
					setter.setProperties(props, THROWABLE_RENDERER_PREFIX + ".");
					((ThrowableRendererSupport) hierarchy).setThrowableRenderer(tr);
				}
			}
		}
	}
}

/**
 * 解析logger的父类Category传播属性additive
 * additive决定着叶子节点的logger是否将日志输出转发给根节点的logger也打印输出一次
 */
void parseAdditivityForLogger(Properties props, Logger cat, String loggerName) {
	String value = OptionConverter.findAndSubst(ADDITIVITY_PREFIX + loggerName, props);
	LogLog.debug("Handling " + ADDITIVITY_PREFIX + loggerName + "=[" + value + "]");
	// touch additivity only if necessary
	if ((value != null) && (!value.equals(""))) {
		boolean additivity = OptionConverter.toBoolean(value, true);
		LogLog.debug("Setting additivity for \"" + loggerName + "\" to " + additivity);
		cat.setAdditivity(additivity);
	}
}

/**
 * 开始解析根节点Logger父类Category
 */
void parseCategory(Properties props, Logger logger, String optionKey, String loggerName, String value) {
	// 如果配置文件log4j.properties里配置了log4j.debug=true或者log4j.configDebug=true,就可以打印此日志来跟踪log4j的加载过程
	LogLog.debug("Parsing for [" + loggerName + "] with value=[" + value + "].");
	// 比如之前我们在配置文件log4j.properties首行配置了log4j.rootLogger=DEBUG,Info,Warn,Error
	// 那么这里的value值为DEBUG,Info,Warn,Error,并且是逗号分隔
	StringTokenizer st = new StringTokenizer(value, ",");

	if (!(value.startsWith(",") || value.equals(""))) {// 配置的log4j.rootLogger或者log4j.rootCategory的值非空且不是逗号开头这里会执行
		// 为了安全起见,这里检查下配置的值DEBUG,Info,Warn,Error通过逗号分隔后的数组长度是否大于0,否则直接返回
		if (!st.hasMoreTokens())
			return;

		// 解析的Logger父类Category的日志级别设置为读取的值,比如配置了log4j.rootLogger=DEBUG,Info,Warn,Error,那么这里设置日志级别为DEBUG
		String levelStr = st.nextToken();
		// 如果配置文件log4j.properties里配置了log4j.debug=true或者log4j.configDebug=true,就可以打印此日志来跟踪log4j的加载过程
		LogLog.debug("Level token is [" + levelStr + "].");

		if (INHERITED.equalsIgnoreCase(levelStr) || NULL.equalsIgnoreCase(levelStr)) {// 如果设置的log4j.rootLogger=inherited或者为字符串"null"这里会执行
			if (loggerName.equals(INTERNAL_ROOT_NAME)) {
				LogLog.warn("The root logger cannot be set to null.");
			} else {
				logger.setLevel(null);
			}
		} else {
			// 转换配置中读取的日志级别为枚举值(TRACE、DEBUG、INFO、WARN、ERROR、FATAL、OFF、ALL),不能转换就设置默认值DEBUG
			logger.setLevel(OptionConverter.toLevel(levelStr, (Level) Level.DEBUG));
		}
		// 如果配置文件log4j.properties里配置了log4j.debug=true或者log4j.configDebug=true,就可以打印此日志来跟踪log4j的加载过程
		LogLog.debug("Category " + loggerName + " set to " + logger.getLevel());
	}

	// 初始化logger的appender之前首先清理掉所有的appender
	logger.removeAllAppenders();

	Appender appender;
	String appenderName;
	// 如果log4j.properties配置了log4j.rootLogger=DEBUG,Info,Warn,Error,那么这里会分别解析到appender为Info,Warn,Error这3个
	while (st.hasMoreTokens()) {
		appenderName = st.nextToken().trim();
		// appender名字为空或者直接为逗号,读取下一个appender名字
		if (appenderName == null || appenderName.equals(","))
			continue;
		// 如果配置文件log4j.properties里配置了log4j.debug=true或者log4j.configDebug=true,就可以打印此日志来跟踪log4j的加载过程
		LogLog.debug("Parsing appender named \"" + appenderName + "\".");
		// 这里解析所有以log4j.appender.appendername打头的properties中的值
		// 注意appendername为真实的appender的名字,比如刚才的log4j.appender.Info,log4j.appender.Warn,log4j.appender.Info.Error这3个
		appender = parseAppender(props, appenderName);
		if (appender != null) {
			// logger的类型为AppenderAttachableImpl的变量aai里维护着logger的所有appender,这里往aai里添加appender的一个引用,方便logger找到appender
			logger.addAppender(appender);
		}
	}
}

/**
 * 这里解析所有以log4j.appender.appendername打头的properties中的值
 * 注意appendername为真实的appender的名字
 * ,比如log4j.properties配置了log4j.rootLogger=DEBUG,Info,Warn,Error
 * 那么会去找log4j.appender.Info,log4j.appender.Warn,log4j.appender.Info.
 * Error这3个开头的所有properties的配置值
 */
Appender parseAppender(Properties props, String appenderName) {
	// 往PropertyConfigurator类的类型为Hashtable的变量registry里找是否已经有名字为appenderName的appender解析过
	Appender appender = registryGet(appenderName);
	// 如果之前解析过名字为appenderName的appender,那么这里不会重复解析,直接返回之前解析好的appender
	if ((appender != null)) {
		// 如果配置文件log4j.properties里配置了log4j.debug=true或者log4j.configDebug=true,就可以打印此日志来跟踪log4j的加载过程
		LogLog.debug("Appender \"" + appenderName + "\" was already parsed.");
		return appender;
	}
	// 拼接appender对应的配置字符串的前缀,前缀="log4j.appender."+appenderName
	// 比如为log4j.appender.Info
	// 比如为log4j.appender.Warn
	// 比如为log4j.appender.Error
	String prefix = APPENDER_PREFIX + appenderName;
	// 拼接appender对应的layout配置字符串的前缀,前缀="log4j.appender."+appenderName+".layout"
	// 比如为log4j.appender.Info.layout
	// 比如为log4j.appender.Warn.layout
	// 比如为log4j.appender.Error.layout
	String layoutPrefix = prefix + ".layout";

	// 从log4j.properties中读取配置【"log4j.appender."+appenderName】的值然后通过【JDK的反射机制】调用这个appender的无参构造方法创建一个appender对象
	// 比如配置了log4j.appender.Info=org.apache.log4j.DailyRollingFileAppender
	// 那么会反射org.apache.log4j.DailyRollingFileAppender创建一个DailyRollingFileAppender类的对象
	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);

	// OptionHandler是一个接口类,定义了抽象方法activateOptions()
	// appender可以根据需要实现接口OptionHandler来进行除了成员变量属性之外的其他相关设置
	if (appender instanceof OptionHandler) {
		if (appender.requiresLayout()) {
			// 如果一个appender的实现类需要设置layout,这里通过反射layout的无参构造方法创建一个layout实现类的对象
			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 + "\".");
			}
		}

		// 如果一个appender的实现类需要设置errorhandler,这里通过反射errorhandler的无参构造方法创建一个errorhandler实现类的对象
		final String errorHandlerPrefix = prefix + ".errorhandler";
		String errorHandlerClass = OptionConverter.findAndSubst(errorHandlerPrefix, props);
		if (errorHandlerClass != null) {
			ErrorHandler eh = (ErrorHandler) OptionConverter.instantiateByKey(props, errorHandlerPrefix,
					ErrorHandler.class, null);
			if (eh != null) {
				appender.setErrorHandler(eh);
				LogLog.debug("Parsing errorhandler options for \"" + appenderName + "\".");
				parseErrorHandler(eh, errorHandlerPrefix, props, repository);
				final Properties edited = new Properties();
				final String[] keys = new String[] { errorHandlerPrefix + "." + ROOT_REF,
						errorHandlerPrefix + "." + LOGGER_REF, errorHandlerPrefix + "." + APPENDER_REF_TAG };
				for (Iterator iter = props.entrySet().iterator(); iter.hasNext();) {
					Map.Entry entry = (Map.Entry) iter.next();
					int i = 0;
					for (; i < keys.length; i++) {
						if (keys[i].equals(entry.getKey()))
							break;
					}
					if (i == keys.length) {
						edited.put(entry.getKey(), entry.getValue());
					}
				}
				// 通过JDK的PropertySetter设置errorHandlerPrefix实现类的对象的成员变量属性值
				PropertySetter.setProperties(eh, edited, errorHandlerPrefix + ".");
				LogLog.debug("End of errorhandler parsing for \"" + appenderName + "\".");
			}

		}

		// 通过JDK的PropertySetter设置appender实现类的对象的成员变量属性值
		// 比如配置了log4j.appender.Info=org.apache.log4j.DailyRollingFileAppender
		// 那么会反射org.apache.log4j.DailyRollingFileAppender创建一个DailyRollingFileAppender类的对象
		// 然后这里设置DailyRollingFileAppender对象的所有成员变量的值
		PropertySetter.setProperties(appender, props, prefix + ".");
		LogLog.debug("Parsed \"" + appenderName + "\" options.");
	}

	// 从配置文件中寻找所有以"log4j.appender." + appenderName + ".filter."开头的配置的值
	// 比如我们配置了log4j.appender.Info.filter=org.apache.log4j.varia.LevelMatchFilter
	// 比如我们配置了log4j.appender.Info.filter.acceptOnMatch=true
	// 那么会反射org.apache.log4j.varia.LevelMatchFilter创建对象LevelMatchFilter并设置其成员变量值,最后添加LevelMatchFilter到appender
	parseAppenderFilters(props, appenderName, appender);
	// 往PropertyConfigurator类的类型为Hashtable的变量registry里记录已经解析完毕的appender对象的引用,防止重复解析
	registryPut(appender);
	return appender;
}

       在上面我们也看到关于日志过滤器的配置,这里将日志过滤器的相关类继承关系图绘图如下:


       到这里为止,我们完成了log4j-1.2.17初始化源代码截图,我们可以得到如下的组成关系图:

 

4.Logger.getLogger(LoggerTest.class)代码分析

       在上面的分析中我们知道,log4j-1.2.17通过类LogManager的静态代码块读取了配置文件,然后针对不同配置文件类型采用不用的解析器(以.lf5结尾的英汉词典文件解析器DefaultLF5Configurator、以.xml结尾的XML文件解析器DOMConfigurator和以.properties结尾的文件解析器PropertyConfigurator)读取解析配置文件,够建了一个基本的RootLogger,用图表示如下:


       从上面我们知道,log4j-1.2.17通过LogManager的代码块解析了log4j.properties文件,创建了RootLogger,RootLogger有3个appender,会将日志文件输出到info.log,warn.log和error.log这3个文件。开篇我们提到了这么一段使用日志的代码:

package com.log4jtest
public class LoggerTest{
	private static Logger logger = Logger.getLogger(LoggerTest.class);
	public void main(String[] args){
		logger.debug("debug");
		logger.info("info");
		logger.error("error");
	}
}

       现在我们来看下Logger.getLogger(LoggerTest.class)干了些啥,先来一张图来诠释:


       上图告诉我们创建了一个全新的Logger,这个Logger没有任何的appender,其parent属性指向了起初创建的RootLogger,我们查看Logger.java的相关代码,注释如下:

/**
 * 传入Logger名字,获取真对该名字唯一的一个Logger,只会在第一次创建,第二次是直接返回第一次创建的Logger对象
 */
static public Logger getLogger(Class clazz) {
	return LogManager.getLogger(clazz.getName());
}

       继续跟进LogManager.java的相关代码,注释如下:

/**
 * 传入Logger名字,获取真对该名字唯一的一个Logger,只会在第一次创建,第二次是直接返回第一次创建的Logger对象
 */
public static Logger getLogger(final String name) {
	return getLoggerRepository().getLogger(name);
}
/**
 * repositorySelector在LogManager的静态代码块已经创建,默认实现为DefaultRepositorySelector
 */
static public LoggerRepository getLoggerRepository() {
	if (repositorySelector == null) {
		repositorySelector = new DefaultRepositorySelector(new NOPLoggerRepository());
		guard = null;
		Exception ex = new IllegalStateException("Class invariant violation");
		String msg = "log4j called after unloading, see http://logging.apache.org/log4j/1.2/faq.html#unload.";
		if (isLikelySafeScenario(ex)) {
			LogLog.debug(msg, ex);
		} else {
			LogLog.error(msg, ex);
		}
	}
	return repositorySelector.getLoggerRepository();
}

       在LogManager的static代码块中通过DefaultRepositorySelector(new Hierarchy(new RootLogger((Level) Level.DEBUG)))创建了DefaultRepositorySelector类型的repositorySelector,上面对于repositorySelector.getLoggerRepository的调用实际上是调用了接口LoggerRepository的实现类Hierarchy的getLoggerRepository方法,其类继承关系图如下:


       继续跟进Hierarchy.java的相关代码,注释如下:

/**
 * 传入Logger名字,获取真对该名字唯一的一个Logger,只会在第一次创建,第二次是直接返回第一次创建的Logger对象
 */
public Logger getLogger(String name) {
	return getLogger(name, defaultFactory);
}

/**
 * 传入Logger名字,获取真对该名字唯一的一个Logger,只会在第一次创建,第二次是直接返回第一次创建的Logger对象
 */
public Logger getLogger(String name, LoggerFactory factory) {
	//CategoryKey是一个对String的包装类,这里传入了name=com.log4jtest.LoggerTest
	CategoryKey key = new CategoryKey(name);

      //Logger只会创建一次,创建完毕立即放入类型为Hashtable的变量ht中存储,后续直接取出返回
	Logger logger;

	//这里防止多线程重复创建,先对类型为Hashtable的变量ht上锁,避免多线程相互竞争
	synchronized (ht) {
		//从类型为Hashtable的变量ht中查找之前是否已经创建过该Logger
		Object o = ht.get(key);
		if (o == null) {//如果从类型为Hashtable的变量ht中找不到名字为com.log4jtest.LoggerTest的Logger,这里会执行
			//调用工厂类DefaultCategoryFactory创建一个全新的Logger对象
			logger = factory.makeNewLoggerInstance(name);
			//设置新建的Logger对象的parent属性指向log4j-1.2.17名字为root的全局根节点RootLogger
			logger.setHierarchy(this);
			//创建完毕立即放入类型为Hashtable的变量ht中存储,后续直接取出返回
			ht.put(key, logger);
			//更新刚才的名字com.log4jtest.LoggerTest的Logger对应的报名,往类型为Hashtable的变量ht中丢入如下信息
			//com.log4jtest.LoggerTest  -->  Logger[name=com.log4jtest.LoggerTest]
			//com.log4jtest                  -->  ProvisionNode{Logger[name=com.log4jtest.LoggerTest]}
			//com                               -->  ProvisionNode{Logger[name=com.log4jtest.LoggerTest]}
			updateParents(logger);
			return logger;
		} else if (o instanceof Logger) {//如果从类型为Hashtable的变量ht中能找到对应名字的Logger,这里会执行,直接返回该Logger
			return (Logger) o;
		} else if (o instanceof ProvisionNode) {
			//如果从类型为Hashtable的变量ht中能找到对应名字ProvisionNode
              //说明之前是子类里创建了Logger,包名的父包名被指向了ProvisionNode,那么这里就创建一个新的Logger,然后把类型为Hashtable的变量ht的记录更新掉
			logger = factory.makeNewLoggerInstance(name);
			//设置新建的Logger对象的parent属性指向log4j-1.2.17名字为root的全局根节点RootLogger
			logger.setHierarchy(this);
			ht.put(key, logger);
              //说明之前是子类里创建了Logger,包名的父包名被指向了ProvisionNode,那么这里就创建一个新的Logger,然后把类型为Hashtable的变量ht的记录更新掉
			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.
		}
	}
}

 

5.Logger.info、Logger.warn和Logger.error代码分析

       还是先通过一张图来诠释这个过程:


       我们就以logger.info("info")作为入口,跟进一下其代码调用。我们还是先回顾一下之前关于Logger组成的关系图如下:


       从上图中我们知道Logger.java的一切行为都转交给Category.java处理,这里我们注释Category.java相关代码如下:

/**
 * 调用Logger.info打印INFO级别日志 
 * Logger.info转交Category.info打印INFO级别日志
 */
public void info(Object message) {
	//如果Hierarchy中的日志级别要大于INFO级别,那么是不会输出的,直接返回
	if (repository.isDisabled(Level.INFO_INT)) return;
	
	//再次取出本Logger的level,如果Logger的level为空,那么就取其属性parent对应的RootLogger对应的level作为本Logger的level
	//如果取出的Logger的level小于INFO,就继续执行,否则直接返回
	if (Level.INFO.isGreaterOrEqual(this.getEffectiveLevel()))
	  //调用forcedLog打印日志
		forcedLog(FQCN, Level.INFO, message, null);
}
/**
 * 调用forcedLog打印日志
 */
protected void forcedLog(String fqcn, Priority level, Object message, Throwable t) {
	//调用logger对应的appender打印日志
  //如果logger对应的appender为空就不打印
	//如果logger的additive=true并且其属性parent对应的appender不为空,就调用parent属性对应的RootLogger的appender打印日志
	callAppenders(new LoggingEvent(fqcn, this, level, message, t));
}
/**
 * 调用logger对应的appender打印日志
 * 如果logger对应的appender为空就不打印
 * 如果logger的additive=true并且其属性parent对应的appender不为空,就调用parent属性对应的RootLogger的appender打印日志
 */
public void callAppenders(LoggingEvent event) {
	int writes = 0;
	
	//调用logger对应的appender打印日志
    //如果logger对应的appender为空就不打印
	//如果logger的additive=true并且其属性parent对应的appender不为空,就调用parent属性对应的RootLogger的appender打印日志
	for (Category c = this; c != null; c = c.parent) {
		synchronized (c) {
			if (c.aai != null) {
				//aai类型为AppenderAttachableImpl
				//AppenderAttachableImpl里通过类型为Vector的变量appenderList维护着Logger对应的appender
				//这里遍历Logger对应的appender,调用appender.doAppend(event)
				writes += c.aai.appendLoopOnAppenders(event);
			}
			
			//如果logger的additive=false,那么不会调用parent属性对应的RootLogger的appender打印日志,直接退出
			if (!c.additive) {
				break;
			}
		}
	}

	//如果找不到Logger,那么只是触发警告事件,这里说明不配置appender首先不会影响正常业务,毕竟日志不能影响业务
	if (writes == 0) {
		repository.emitNoAppenderWarning(this);
	}
}

       我们继续跟踪代码c.aai.appendLoopOnAppenders(event),这里aai类型为AppenderAttachableImpl,AppenderAttachableImpl里通过类型为Vector的变量appenderList维护着Logger对应的appender,这里遍历Logger对应的appender,调用appender.doAppend(event),注释AppenderAttachableImpl.java的相关代码如下:

/**
 *aai类型为AppenderAttachableImpl
 *AppenderAttachableImpl里通过类型为Vector的变量appenderList维护着Logger对应的appender
 *这里遍历Logger对应的appender,调用appender.doAppend(event)
 */
public int appendLoopOnAppenders(LoggingEvent event) {
	int size = 0;
	Appender appender;

	//aai类型为AppenderAttachableImpl
	//AppenderAttachableImpl里通过类型为Vector的变量appenderList维护着Logger对应的appender
	//这里遍历Logger对应的appender,调用appender.doAppend(event)
	if (appenderList != null) {
		size = appenderList.size();
		for (int i = 0; i < size; i++) {
			appender = (Appender) appenderList.elementAt(i);
			appender.doAppend(event);
		}
	}
	return size;
}

       至于logger.warn和logger.error的代码逻辑和logger.info并无太大差别,这里我就不再赘述了,至此我们已经知道了Logger会首先调用自己的appender打印日志,然后如果配置的additive(默认为true)为true,那么会继续调用属性parent对应RootLogger的appender打印日志。我们以一张图来诠释Logger从创建到调用的过程如下:

6.Appender代码分析

       Appender主要解决日志输出到哪里的问题,比如日志输出到操作系统的console,日志输出到数据库中,日志输出到socket接口,不过当前用的最多的是将日志输出到磁盘文件中,首先来看下Appender的类继承关系图:

           从上图看出对于Appender的实现类有十几个,他们有一个共同的抽象父类AppenderSkeleton,工作中最常用的还是ConsoleAppender、FileAppender、DailyRollingFileAppender和RollingFileAppender这4个,接下来会分别分析AppenderSkeleton、WriterAppender、ConsoleAppender、FileAppender 、DailyRollingFileAppende和RollingFileAppender这6个类的源代码,开始查看源代码之前先通过一张类继承关系图了解其关系:

    6.1 AppenderSkeleton.java

       AppenderSkeleton是一个抽象类,同时是所有appender类的父类.AppenderSkeleton提供对于过滤器filter的支持,比如能根据日志级别进行过滤.,注释其代码如下:

package org.apache.log4j;

import org.apache.log4j.spi.Filter;
import org.apache.log4j.spi.ErrorHandler;
import org.apache.log4j.spi.OptionHandler;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.log4j.helpers.OnlyOnceErrorHandler;
import org.apache.log4j.helpers.LogLog;
/**
 * AppenderSkeleton是一个抽象类,同时是所有appender类的父类
 * AppenderSkeleton提供对于过滤器filter的支持,比如能根据日志级别进行过滤
 * */
public abstract class AppenderSkeleton implements Appender, OptionHandler {
    //日志字符串输出格式化类,比如格式话为字符串就直接调用Object.toString(),或者格式化为json
	protected Layout layout;
	//appender名称
	protected String name;
	//日志级别,默认为空
	protected Priority threshold;
	//默认的异常处理类OnlyOnceErrorHandler
	protected ErrorHandler errorHandler = new OnlyOnceErrorHandler();
    //过滤器链条的头结点
	protected Filter headFilter;
    //过滤器链条的尾节点
	protected Filter tailFilter;
    //appender是否被关闭,默认false
	protected boolean closed = false;
	
	/**
	 * 构造方法
	 */
	public AppenderSkeleton() {
		super();
	}

	/**
	 * 构造方法
	 */
	protected AppenderSkeleton(final boolean isActive) {
		super();
	}

	/**
	 * 通常LogManager读取log4j.properties后会通过反射机制设置appender成员变量相关属性
	 * activateOptions是对非成员变量相关属性进行设置
	 */
	public void activateOptions() {
	}

	/**
	 * 过滤器链的末尾添加一个新的过滤器
	 */
	public void addFilter(Filter newFilter) {
		if (headFilter == null) {
			headFilter = tailFilter = newFilter;
		} else {
			tailFilter.setNext(newFilter);
			tailFilter = newFilter;
		}
	}

	/**
	 * appender.doAppend被调用后都会转交AppenderSkeleton.doAppend处理
	 * 而AppenderSkeleton.doAppend会将具体处理细节转交其子类的append方法处理
	 * 这里定义子类必须实现的抽象方法
	 */
	abstract protected void append(LoggingEvent event);

	/**
	 * 清理过滤器链
	 */
	public void clearFilters() {
		headFilter = tailFilter = null;
	}

	/**
	 * appender调用完毕后被执行的方法finalize()
	 */
	public void finalize() {
		// 一个appender可以被java GC关闭,这里进行检查,如果已经被关闭了,就直接返回,放置关闭两次
		if (this.closed)
			return;
		LogLog.debug("Finalizing appender named [" + name + "].");
		close();
	}

	/**
	 * GETSET方法
	 */
	public ErrorHandler getErrorHandler() {
		return this.errorHandler;
	}
	public Filter getFilter() {
		return headFilter;
	}
	public final Filter getFirstFilter() {
		return headFilter;
	}
	public Layout getLayout() {
		return layout;
	}
	public final String getName() {
		return this.name;
	}
	public Priority getThreshold() {
		return threshold;
	}
	public boolean isAsSevereAsThreshold(Priority priority) {
		return ((threshold == null) || priority.isGreaterOrEqual(threshold));
	}

	/**
	 * 通过java的synchronized机制进行加锁,防止多个线程操作同一个appender
	 * 这里调用appender处理相关日志
	 * */
	public synchronized void doAppend(LoggingEvent event) {
		if (closed) {//appender已经被关闭了,直接返回
			LogLog.error("Attempted to append to closed appender named [" + name + "].");
			return;
		}
        //日志级别检查,打印日志的级别为ERROR但appender的日志级别为INFO,那么这里会直接返回
		if (!isAsSevereAsThreshold(event.getLevel())) {
			return;
		}
        //执行过滤器链
		//如果在日志中配置了log4j.appender.appendername.filter=org.apache.log4j.varia.DenyAllFilter
		//那么这里f的类型就是org.apache.log4j.varia.LevelMatchFilter,会调用LevelMatchFilter的decide(event)方法
		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();
			}
		}
        //调用ApenderSkeleton的子类的append方法
		this.append(event);
	}

	/**
	 * 通过java的synchronized机制进行加锁,防止多个线程操作同一个appender
	 * GETSET方法
	 */
	public synchronized void setErrorHandler(ErrorHandler eh) {
		if (eh == null) {
			// We do not throw exception here since the cause is probably a
			// bad config file.
			LogLog.warn("You have tried to set a null error-handler.");
		} else {
			this.errorHandler = eh;
		}
	}

	/**
	 * GETSET方法
	 */
	public void setLayout(Layout layout) {
		this.layout = layout;
	}

	/**
	 * GETSET方法
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * GETSET方法
	 */
	public void setThreshold(Priority threshold) {
		this.threshold = threshold;
	}
}
 

    6.2 WriterAppender.java

       WriterAppender.java通过java的IO流操作类(java.io.Writer或者java.io.OutputStream)来分别对字符流和字节流分别进行处理,其里面2个重要的属性是immediateFlush(IO流是否立即写入磁盘文件,默认为true)和encoding(IO流的编码),以及与文件操作相关的类型为QuietWriter(将java.io.FilterWriter进行扩展包装的QuietWriter)的变量qw,注释其代码如下:

package org.apache.log4j;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;

import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.QuietWriter;
import org.apache.log4j.spi.ErrorHandler;
import org.apache.log4j.spi.LoggingEvent;

/**
 * WriterAppender里实现了java对于IO流的2种不同处理,
 * 一个是java.io.Writer,另一个是java.io.OutputStream
 */
public class WriterAppender extends AppenderSkeleton {

	/**
	 * IO流是否立即清理写入到磁盘文件,默认为true
	 * 这个属性控制着java.io.Writer或者java.io.OutputStream是否每次appender执行时候立即写磁盘
	 */
	protected boolean immediateFlush = true;

	/**
	 * 这个属性控制着java.io.Writer或者java.io.OutputStream的编码类型,默认为null时会读取系统的编码类型
	 */
	protected String encoding;

	/**
	 * 将java.io.FilterWriter进行扩展包装的QuietWriter
	 * 主要扩展了对于ErrorHandler处理
	 */
	protected QuietWriter qw;

	/**
	 * 构造方法,默认LogManager会反射此构造方法构建一个空的WriterAppender对象,然后仍通过反射设置其属性
	 */
	public WriterAppender() {
	}

	/**
	 * 构造方法
	 */
	public WriterAppender(Layout layout, OutputStream os) {
		this(layout, new OutputStreamWriter(os));
	}

	/**
	 * 构造方法
	 */
	public WriterAppender(Layout layout, Writer writer) {
		this.layout = layout;
		this.setWriter(writer);
	}

	/**
	 * LogManager会反射此方法设置WriterAppender的属性immediateFlush
	 */
	public void setImmediateFlush(boolean value) {
		immediateFlush = value;
	}

	/**
	 * GET
	 */
	public boolean getImmediateFlush() {
		return immediateFlush;
	}

	/**
	 * 除了成员变量属性之外的额外设置
	 */
	public void activateOptions() {
	}

	/**
	 * AppenderSkeleton.doAppend会调用此方法处理日志
	 */
	public void append(LoggingEvent event) {
       //对appender进行检查,appender不能已经被关闭,qw不能为空,layout不能为空
		if (!checkEntryConditions()) {
			return;
		}
		//调用subAppend处理日志
		subAppend(event);
	}

	/**
	 * 对appender进行检查,appender不能已经被关闭,qw不能为空,layout不能为空
	 */
	protected boolean checkEntryConditions() {
		if (this.closed) {
			LogLog.warn("Not allowed to write to a closed appender.");
			return false;
		}

		if (this.qw == null) {
			errorHandler.error("No output stream or file set for the appender named [" + name + "].");
			return false;
		}

		if (this.layout == null) {
			errorHandler.error("No layout set for the appender named [" + name + "].");
			return false;
		}
		return true;
	}

	/**
	 * 通过java的synchronized机制进行加锁,防止多个线程操作同一个appender的close
	 */
	public synchronized void close() {
		if (this.closed)
			return;
		this.closed = true;
		//文件中输出日志的尾部标识,比如回车换行
		writeFooter();
		//重置WriterAppender,就是关闭持有的IO流
		reset();
	}

	/**
	 * 关闭java.io.Writer或者java.io.OutputStream对应的IO流
	 * */
	protected void closeWriter() {
		if (qw != null) {
			try {
				qw.close();
			} catch (IOException e) {
				if (e instanceof InterruptedIOException) {
					Thread.currentThread().interrupt();
				}
				LogLog.error("Could not close " + qw, e);
			}
		}
	}

	/**
	 * 创建文件IO流处理类OutputStreamWriter
	 */
	protected OutputStreamWriter createWriter(OutputStream os) {
		OutputStreamWriter retval = null;
        //获取编码
		String enc = getEncoding();
		if (enc != null) {
			try {
				retval = new OutputStreamWriter(os, enc);
			} catch (IOException e) {
				if (e instanceof InterruptedIOException) {
					Thread.currentThread().interrupt();
				}		
				// 如果log4j.properties中配置了log4j.debug=true或者log4j.configDebug=true,其值非空且非false就置默认值true开启log4j-1.2.17自己的日志打印,否则不开启 
				//只有开启了这里的日志才会被打印
				LogLog.warn("Error initializing output writer.");
				LogLog.warn("Unsupported encoding?");
			}
		}
		if (retval == null) {
			retval = new OutputStreamWriter(os);
		}
		return retval;
	}
    //获取编码
	public String getEncoding() {
		return encoding;
	}
    //设置编码
	public void setEncoding(String value) {
		encoding = value;
	}

	/**
	 * 通过java的synchronized机制进行加锁,防止多个线程操作同一个appender的setErrorHandler
	 */
	public synchronized void setErrorHandler(ErrorHandler eh) {
		if (eh == null) {
			LogLog.warn("You have tried to set a null error-handler.");
		} else {
			this.errorHandler = eh;
			if (this.qw != null) {
				this.qw.setErrorHandler(eh);
			}
		}
	}

	/**
	 * 通过java的synchronized机制进行加锁,防止多个线程操作同一个appender的setWriter
	 */
	public synchronized void setWriter(Writer writer) {
		reset();
		this.qw = new QuietWriter(writer, errorHandler);
		//文件中输出日志的头部标识,比如此日志来源的类名,日志产生时间,级别等
		writeHeader();
	}

	/**
	 * 调用subAppend处理日志
	 */
	protected void subAppend(LoggingEvent event) {
		//首先用layout格式化event,然后调用IO流写磁盘文件
		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);
				}
			}
		}
        //是否立即flush日志到磁盘文件
		if (shouldFlush(event)) {
			this.qw.flush();
		}
	}

	/**
	 * WriterAppender必须设置layout
	 */
	public boolean requiresLayout() {
		return true;
	}

	/**
	 * 重置WriterAppender,就是关闭持有的IO流
	 */
	protected void reset() {
		closeWriter();
		this.qw = null;
	}

	/**
	 * 文件中输出日志的尾部标识,比如回车换行
	 */
	protected void writeFooter() {
		if (layout != null) {
			String f = layout.getFooter();
			if (f != null && this.qw != null) {
				this.qw.write(f);
				this.qw.flush();
			}
		}
	}

	/**
	 * 文件中输出日志的头部标识,比如此日志来源的类名,日志产生时间,级别等
	 */
	protected void writeHeader() {
		if (layout != null) {
			String h = layout.getHeader();
			if (h != null && this.qw != null)
				this.qw.write(h);
		}
	}

	/**
	 * 是否立即写磁盘文件
	 */
	protected boolean shouldFlush(final LoggingEvent event) {
		return immediateFlush;
	}
}

    6.3 ConsoleAppender.java

       ConsoleAppender是往console里丢入日志,ConsoleAppender具体调用的是java的System.out和System.err,默认使用System.out,注释其代码如下:

package org.apache.log4j;

import java.io.IOException;
import java.io.OutputStream;
import org.apache.log4j.helpers.LogLog;

/**
 * ConsoleAppender是往console里丢入日志
 * ConsoleAppender具体调用的是java的System.out和System.err,默认使用System.out
 */
public class ConsoleAppender extends WriterAppender {
    //ConsoleAppender具体调用的是java的System.out和System.err,默认使用System.out
	public static final String SYSTEM_OUT = "System.out";
	public static final String SYSTEM_ERR = "System.err";
	protected String target = SYSTEM_OUT;

	/**
	 * Determines if the appender honors reassignments of System.out or
	 * System.err made after configuration.
	 */
	private boolean follow = false;

	/**
	 * 构造方法
	 */
	public ConsoleAppender() {
	}

	/**
	 * 构造方法
	 */
	public ConsoleAppender(Layout layout) {
		this(layout, SYSTEM_OUT);
	}

	/**
	 * 构造方法
	 */
	public ConsoleAppender(Layout layout, String target) {
		setLayout(layout);
		setTarget(target);
		activateOptions();
	}

	/**
	 * 设置使用System.out还是System.err
	 * */
	public void setTarget(String value) {
		String v = value.trim();
		if (SYSTEM_OUT.equalsIgnoreCase(v)) {
			target = SYSTEM_OUT;
		} else if (SYSTEM_ERR.equalsIgnoreCase(v)) {
			target = SYSTEM_ERR;
		} else {
			targetWarn(value);
		}
	}

	/**
	 * 返回当前使用的是System.out还是System.err
	 * */
	public String getTarget() {
		return target;
	}

	/**
	 * 设置变量follow值
	 */
	public final void setFollow(final boolean newValue) {
		follow = newValue;
	}

	/**
	 * 取得变量follow值
	 */
	public final boolean getFollow() {
		return follow;
	}
	
	/**
	 * 调用log4j-1.2.17自身的日志类打印日志,
	 */
	void targetWarn(String val) {
		// 如果log4j.properties中配置了log4j.debug=true或者log4j.configDebug=true,其值非空且非false就置默认值true开启log4j-1.2.17自己的日志打印,否则不开启 
		//只有开启了这里的日志才会被打印
		LogLog.warn("[" + val + "] should be System.out or System.err.");
		LogLog.warn("Using previously set target, System.out by default.");
	}

	/**
	 * 额外的参数设置
	 * 注意这里将SystemErrStream或者SystemOutStream传入了父类构造了QWriter
	 */
	public void activateOptions() {
		if (follow) {
			if (target.equals(SYSTEM_ERR)) {
				setWriter(createWriter(new SystemErrStream()));
			} else {
				setWriter(createWriter(new SystemOutStream()));
			}
		} else {
			if (target.equals(SYSTEM_ERR)) {
				setWriter(createWriter(System.err));
			} else {
				setWriter(createWriter(System.out));
			}
		}
		super.activateOptions();
	}

	/**
	 * 调用父类方法关闭IO流
	 */
	protected final void closeWriter() {
		if (follow) {
			super.closeWriter();
		}
	}

	/**
	 * 定义IO流处理类SystemErrStream
	 */
	private static class SystemErrStream extends OutputStream {
		public SystemErrStream() {
		}
		public void close() {
		}
		public void flush() {
			System.err.flush();
		}
		public void write(final byte[] b) throws IOException {
			System.err.write(b);
		}
		public void write(final byte[] b, final int off, final int len) throws IOException {
			System.err.write(b, off, len);
		}
		public void write(final int b) throws IOException {
			System.err.write(b);
		}
	}

	/**
	 * 定义IO流处理类SystemOutStream
	 */
	private static class SystemOutStream extends OutputStream {
		public SystemOutStream() {
		}
		public void close() {
		}
		public void flush() {
			System.out.flush();
		}
		public void write(final byte[] b) throws IOException {
			System.out.write(b);
		}
		public void write(final byte[] b, final int off, final int len) throws IOException {
			System.out.write(b, off, len);
		}
		public void write(final int b) throws IOException {
			System.out.write(b);
		}
	}
}

       看了上面的代码,我们能知道对于ConsoleAppender类型的Appender,我们可以在log4j.properties中配置其如下属性:

log4j.appender.file=org.apache.log4j.ConsoleAppender
#来自类AppenderSkeleton的属性layout、threshold、errorHandler和closed
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.Threshold=INFO
log4j.appender.console.errorHandler=org.apache.log4j.helpers.OnlyOnceErrorHandler
log4j.appender.console.closed=false
#来自类WriterAppender的属性immediateFlush和encoding
log4j.appender.console.immediateFlush = true
log4j.appender.console.encoding=UTF-8
#来自类ConsoleAppender的属性target和follow
log4j.appender.console.target=System.out或者System.err
log4j.appender.console.follow=false
       这里需要明确一点, 不管是log4j.properties或者log4j.xml,在解析到appender的属性后,都是调用java的反射机制首先通过Appender的空的构造方法创建一个对象,然后通过反射机制继续设置其属性值。ConsoleAppender的可配置属性如上,它们分别来自于类AppenderSkeleton、WriterAppender和ConsoleAppender。
       如果你很纠结于是用log4j.appender.console. target还是log4j.appender.console. Target,这里我们可以看下PropertyConfigurator.java里对于拿到tartget和Target的处理逻辑代码,PropertyConfigurator类里调用了PropertySetter来设置属性值,而PropertySetter则调用java.beans.Introspector的 public static String decapitalize(String name) 方法规范属性名字,其代码如下:
    /**
     * Utility method to take a string and convert it to normal Java variable
     * name capitalization.  This normally means converting the first
     * character from upper case to lower case, but in the (unusual) special
     * case when there is more than one character and both the first and
     * second characters are upper case, we leave it alone.
     * <p>
     * Thus "FooBah" becomes "fooBah" and "X" becomes "x", but "URL" stays
     * as "URL".
     *
     * @param  name The string to be decapitalized.
     * @return  The decapitalized version of the string.
     */
    public static String decapitalize(String name) {
        //名字为空或者长度为0,直接返回name
        if (name == null || name.length() == 0) {
            return name;
        }
        //name长度大于或者等于2,并且name的前2个字符都是大写,直接返回name
        if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
                        Character.isUpperCase(name.charAt(0))){
            return name;
        }
        //否则将name的第一个字符转换为小写字母后进行返回
        char chars[] = name.toCharArray();
        chars[0] = Character.toLowerCase(chars[0]);
        return new String(chars);
    }
        属性转换规则:
  • 属性名为null或者为空白字符,直接返回属性名;
  • 属性名长度大于或者等于2,并且属性名的前2位都是大写,那么直接返回属性名;
  • 非上面两种情况,则直接将首字母转换为小写然后返回属性名。
        设置属性的方法寻找规则:
  • 按照属性转换规则标准化属性名后,将属性名的首字母大写,然后在其前面加上字符串set,反射类里面这个方法进行值设置。

       所以不管属性名字是target还是Target,最后都会通过反射方法setTarget去设置属性值。

    6.4 FileAppender.java

       FileAppender使用java.io.Writer来讲日志写入到磁盘文件,注释其代码如下:

package org.apache.log4j;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.Writer;

import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.QuietWriter;
import org.apache.log4j.spi.ErrorCode;

/**
 * 使用java.io.Writer处理日志到磁盘文件的FileAppender
 * */
public class FileAppender extends WriterAppender {

	/**
	 * FileAppender属性定义
	 */
	protected boolean fileAppend = true;//是否在文件末尾追加内容
	protected String fileName = null;//磁盘文件全路径名称
	protected boolean bufferedIO = false;//是否启用缓冲区
	protected int bufferSize = 8 * 1024;//缓冲区大小

	/**
	 * 构造方法,默认LogManager会反射此构造方法构建一个空的FileAppender对象,然后仍通过反射设置其属性
	 */
	public FileAppender() {
	}

	/**
	 * 构造方法
	 */
	public FileAppender(Layout layout, String filename, boolean append, boolean bufferedIO, int bufferSize)
			throws IOException {
		this.layout = layout;
		this.setFile(filename, append, bufferedIO, bufferSize);
	}

	/**
	 * 构造方法
	 */
	public FileAppender(Layout layout, String filename, boolean append) throws IOException {
		this.layout = layout;
		this.setFile(filename, append, false, bufferSize);
	}

	/**
	 * 构造方法
	 */
	public FileAppender(Layout layout, String filename) throws IOException {
		this(layout, filename, true);
	}

	/**
	 * 设置磁盘文件全路径名
	 */
	public void setFile(String file) {
		String val = file.trim();
		fileName = val;
	}

	/**
	 * 是否在文件末尾追加内容
	 */
	public boolean getAppend() {
		return fileAppend;
	}

	/** 磁盘文件全路径名称 */
	public String getFile() {
		return fileName;
	}

	/**
	 * 额外参数设置
	 */
	public void activateOptions() {
		if (fileName != null) {
			try {
				setFile(fileName, fileAppend, bufferedIO, bufferSize);
			} catch (java.io.IOException e) {
				errorHandler.error("setFile(" + fileName + "," + fileAppend + ") call failed.", e, ErrorCode.FILE_OPEN_FAILURE);
			}
		} else {		
			// 如果log4j.properties中配置了log4j.debug=true或者log4j.configDebug=true,其值非空且非false就置默认值true开启log4j-1.2.17自己的日志打印,否则不开启 
			//只有开启了这里的日志才会被打印
			LogLog.warn("File option not set for appender [" + name + "].");
			LogLog.warn("Are you using FileAppender instead of ConsoleAppender?");
		}
	}

	/**
	 * 关闭文件读写IO流
	 */
	protected void closeFile() {
		if (this.qw != null) {
			try {
				this.qw.close();
			} catch (java.io.IOException e) {
				if (e instanceof InterruptedIOException) {
					Thread.currentThread().interrupt();
				}
				// Exceptionally, it does not make sense to delegate to an
				// ErrorHandler. Since a closed appender is basically dead.
				LogLog.error("Could not close " + qw, e);
			}
		}
	}

	/**
	 * 是否启用缓冲区
	 */
	public boolean getBufferedIO() {
		return this.bufferedIO;
	}

	/**
	 * 缓冲区大小
	 */
	public int getBufferSize() {
		return this.bufferSize;
	}

	/**
	 * 设置是否在文件末尾追加内容
	 */
	public void setAppend(boolean flag) {
		fileAppend = flag;
	}

	/**
	 * 设置父类的bufferedIO,控制IO流是否立即清理写入到磁盘文件
	 */
	public void setBufferedIO(boolean bufferedIO) {
		this.bufferedIO = bufferedIO;
		if (bufferedIO) {
			immediateFlush = false;
		}
	}

	/**
	 * 设置缓冲区大小
	 */
	public void setBufferSize(int bufferSize) {
		this.bufferSize = bufferSize;
	}

	/**
	 * 通过java的synchronized机制进行加锁,防止多个线程操作同一个appender的setFile
	 */
	public synchronized void setFile(String fileName, boolean append, boolean bufferedIO, int bufferSize) throws IOException {
		// 如果log4j.properties中配置了log4j.debug=true或者log4j.configDebug=true,其值非空且非false就置默认值true开启log4j-1.2.17自己的日志打印,否则不开启 
		//只有开启了这里的日志才会被打印
		LogLog.debug("setFile called: " + fileName + ", " + append);

		// 如果设置了缓冲区大小,那么就不要立即刷文件到磁盘
		if (bufferedIO) {
			setImmediateFlush(false);
		}

		//重置IO读写流,创建新的IO读写流
		reset();
		FileOutputStream ostream = null;
		try {
			ostream = new FileOutputStream(fileName, append);
		} catch (FileNotFoundException ex) {
			//如果目录都不存在,那么先创建文件目录,然后再创建文件
			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);//设置文件IO流处理类
		this.fileName = fileName;
		this.fileAppend = append;
		this.bufferedIO = bufferedIO;
		this.bufferSize = bufferSize;
		writeHeader();//文件中输出日志的头部标识,比如此日志来源的类名,日志产生时间,级别等
		// 如果log4j.properties中配置了log4j.debug=true或者log4j.configDebug=true,其值非空且非false就置默认值true开启log4j-1.2.17自己的日志打印,否则不开启 
		//只有开启了这里的日志才会被打印
		LogLog.debug("setFile ended");
	}

	/**
	 * 设置文件IO流处理类
	 */
	protected void setQWForFiles(Writer writer) {
		this.qw = new QuietWriter(writer, errorHandler);
	}

	/**
	 * 重置IO读写流,创建新的IO读写流
	 */
	protected void reset() {
		closeFile();//关闭文件读写IO流
		this.fileName = null;
		super.reset();//重置WriterAppender,就是关闭持有的IO流
	}
}
       看了上面的代码,我们能知道对于FileAppender类型的Appender,我们可以在log4j.properties中配置其如下属性:
log4j.appender.file=org.apache.log4j.FileAppender 
#来自类AppenderSkeleton的属性layout、threshold、errorHandler和closed
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.Threshold=INFO
log4j.appender.file.errorHandler=org.apache.log4j.helpers.OnlyOnceErrorHandler
log4j.appender.file.closed=false
#来自类WriterAppender的属性immediateFlush和encoding
log4j.appender.file.immediateFlush = true
log4j.appender.file.encoding=UTF-8
#来自类FileAppender的属性fileAppend、fileName、bufferedIO和bufferSize
log4j.appender.file.File=file.log 
log4j.appender.file.Append=false 
log4j.appender.file.BufferedIO=false 
log4j.appender.file.BufferSize=8192 
       如果你对上面的配置里的属性比如 File的首字母是否要大写很纠结,请参见6.3环节里的介绍

 

    6.5 DailyRollingFileAppender.java

       DailyRollingFileAppender仍然使用java.io.Writer来讲日志写入到磁盘文件,不同的是它可以控制按照天存储文件、按照小时存储文件、按照分钟存储文件、按照月份存储文件和按照周存储文件,其配置说明如下:

DatePattern文件分割规则举例
'.'yyyy-MM按照月份存储文件At midnight of May 31st, 2002 /foo/bar.log will be copied to /foo/bar.log.2002-05. Logging for the month of June will be output to /foo/bar.log until it is also rolled over the next month.
'.'yyyy-ww按照周存储文件,每周的第一由当前系统的时区决定,比如美国以SUNDAY为一周第一天Assuming the first day of the week is Sunday, on Saturday midnight, June 9th 2002, the file /foo/bar.log will be copied to /foo/bar.log.2002-23. Logging for the 24th week of 2002 will be output to /foo/bar.log until it is rolled over the next week.
'.'yyyy-MM-dd按照天存储文件,每天晚上的12点会变更文件名字At midnight, on March 8th, 2002, /foo/bar.log will be copied to /foo/bar.log.2002-03-08. Logging for the 9th day of March will be output to /foo/bar.log until it is rolled over the next day.
'.'yyyy-MM-dd-a按照半天存储文件,中午的12点和晚上的12点时候会变更文件名字At noon, on March 9th, 2002, /foo/bar.log will be copied to /foo/bar.log.2002-03-09-AM. Logging for the afternoon of the 9th will be output to /foo/bar.log until it is rolled over at midnight.
'.'yyyy-MM-dd-HH按照小时存储文件At approximately 11:00.000 o'clock on March 9th, 2002, /foo/bar.log will be copied to /foo/bar.log.2002-03-09-10. Logging for the 11th hour of the 9th of March will be output to /foo/bar.log until it is rolled over at the beginning of the next hour.
'.'yyyy-MM-dd-HH-mm按照分钟存储文件At approximately 11:23,000, on March 9th, 2001, /foo/bar.log will be copied to /foo/bar.log.2001-03-09-10-22. Logging for the minute of 11:23 (9th of March) will be output to /foo/bar.log until it is rolled over the next minute.

       看了上面的介绍,以前对于DailyRollingFileAppende只能按天存储的想法完全错误,原来能支持这么多的策略来分割文件。

       看了上面的代码,我们能知道对于DailyRollingFileAppende类型的Appender,我们可以在log4j.properties中配置其如下属性:

log4j.appender.file=org.apache.log4j.DailyRollingFileAppende 
#来自类AppenderSkeleton的属性layout、threshold、errorHandler和closed
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.Threshold=INFO
log4j.appender.file.errorHandler=org.apache.log4j.helpers.OnlyOnceErrorHandler
log4j.appender.file.closed=false
#来自类WriterAppender的属性immediateFlush和encoding
log4j.appender.file.immediateFlush = true
log4j.appender.file.encoding=UTF-8
#来自类FileAppender的属性fileAppend、fileName、bufferedIO和bufferSize
log4j.appender.file.File=file.log 
log4j.appender.file.Append=false 
log4j.appender.file.BufferedIO=false 
log4j.appender.file.BufferSize=false 
#来自类DailyRollingFileAppende的属性datePattern
log4j.appender.file.datePattern='.'yyyy-MM-dd

    6.6 RollingFileAppender.java

       RollingFileAppender扩展了FileAppender,支持按照文件大小来分割文件,代码注释如下:

package org.apache.log4j;

import java.io.IOException;
import java.io.Writer;
import java.io.File;
import java.io.InterruptedIOException;

import org.apache.log4j.helpers.OptionConverter;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.CountingQuietWriter;
import org.apache.log4j.spi.LoggingEvent;

/**
 * RollingFileAppender扩展了FileAppender,支持按照文件大小来分割文件
 */
public class RollingFileAppender extends FileAppender {

	/**
	 * 每个文件的最大Bytes大小值,默认10MB
	 */
	protected long maxFileSize = 10 * 1024 * 1024;

	/**
	 * 默认除了在写的日志文件外,保留最多maxBackupIndex个日志文件,默认最多保留1个文件,外加一个在写的日志文件,总计2个日志文件
	 */
	protected int maxBackupIndex = 1;

	private long nextRollover = 0;

	/**
	 * 构造方法,默认LogManager会反射此构造方法构建一个空的DailyRollingFileAppender对象,然后仍通过反射设置其属性
	 */
	public RollingFileAppender() {
		super();
	}

	/**
	 * 构造方法
	 */
	public RollingFileAppender(Layout layout, String filename, boolean append) throws IOException {
		super(layout, filename, append);
	}

	/**
	 * 构造方法
	 */
	public RollingFileAppender(Layout layout, String filename) throws IOException {
		super(layout, filename);
	}

	/**
	 * 返回除了在写的日志文件外,最多允许保留几个备份的日志文件
	 */
	public int getMaxBackupIndex() {
		return maxBackupIndex;
	}

	/**
	 * 返回每个日志文件最大允许的Bytes大小
	 */
	public long getMaximumFileSize() {
		return maxFileSize;
	}

	/**
	 * 开始分割文件,将当前文件变更名字存储为新文件,变更完成后重新创建一个名字为fileName的文件继续写日志
	 */
	public void rollOver() {
		File target;
		File file;

		if (qw != null) {
			//获取当前的在写的日志文件的Bytes值的大小
			long size = ((CountingQuietWriter) qw).getCount();
			// 如果log4j.properties中配置了log4j.debug=true或者log4j.configDebug=true,其值非空且非false就置默认值true开启log4j-1.2.17自己的日志打印,否则不开启 
			// 只有开启了这里的日志才会被打印
			LogLog.debug("rolling over count=" + size);
			nextRollover = size + maxFileSize;
		}
		LogLog.debug("maxBackupIndex=" + maxBackupIndex);

		boolean renameSucceeded = true;
		if (maxBackupIndex > 0) {
			// 现在开始备份在写的日志文件了,那么需要删除编号最大的那个日志文件,因为只有删除了才能留出一个文件位置继续备份文件
			file = new File(fileName + '.' + maxBackupIndex);
			if (file.exists())
				renameSucceeded = file.delete();

			// 最大编号的文件删除了,那么其他的几个备份文件的编号都要变更了
			for (int i = maxBackupIndex - 1; i >= 1 && renameSucceeded; i--) {
				file = new File(fileName + "." + i);
				if (file.exists()) {
					target = new File(fileName + '.' + (i + 1));
					LogLog.debug("Renaming file " + file + " to " + target);
					renameSucceeded = file.renameTo(target);
				}
			}

			if (renameSucceeded) {
				// 上面的操作为编号1的备份文件腾出了一个备份文件位置,这里开始备份当前在写日志文件为编号1的备份文件
				target = new File(fileName + "." + 1);
				// 先关闭在写的日志文件
				this.closeFile(); 

				file = new File(fileName);
				LogLog.debug("Renaming file " + file + " to " + target);
				//开始备份当前在写日志文件为编号1的备份文件
				renameSucceeded = file.renameTo(target);
				//如果变更当前在写日志文件为编号为1的备份文件失败,就恢复当前在写日志文件,继续保证其能正常的追加内容
				if (!renameSucceeded) {
					try {
						this.setFile(fileName, true, bufferedIO, bufferSize);
					} catch (IOException e) {
						if (e instanceof InterruptedIOException) {
							Thread.currentThread().interrupt();
						}
						LogLog.error("setFile(" + fileName + ", true) call failed.", e);
					}
				}
			}
		}

		//如果变更当前在写日志文件为编号为1的备份文件成功,需要新建一个名字为fileName的文件保证正常的日志写入操作能进行
		if (renameSucceeded) {
			try {
				this.setFile(fileName, false, bufferedIO, bufferSize);
				nextRollover = 0;
			} catch (IOException e) {
				if (e instanceof InterruptedIOException) {
					Thread.currentThread().interrupt();
				}
				LogLog.error("setFile(" + fileName + ", false) call failed.", e);
			}
		}
	}

	//利用java的synchronized防止多个线程并发去修改同一个文件的属性值
	public synchronized void setFile(String fileName, boolean append, boolean bufferedIO, int bufferSize)
			throws IOException {
		super.setFile(fileName, append, this.bufferedIO, this.bufferSize);
		if (append) {
			File f = new File(fileName);
			((CountingQuietWriter) qw).setCount(f.length());
		}
	}

	/**
	 * 设置默认除了在写的日志文件外,保留最多maxBackupIndex个日志文件,默认最多保留1个文件,外加一个在写的日志文件,总计2个日志文件
	 */
	public void setMaxBackupIndex(int maxBackups) {
		this.maxBackupIndex = maxBackups;
	}

	/**
	 * 设置每个文件的最大Bytes大小值,默认10MB
	 */
	public void setMaximumFileSize(long maxFileSize) {
		this.maxFileSize = maxFileSize;
	}

	/**
	 * 设置每个文件的最大Bytes大小值,默认10MB
	 */
	public void setMaxFileSize(String value) {
		maxFileSize = OptionConverter.toFileSize(value, maxFileSize + 1);
	}

	protected void setQWForFiles(Writer writer) {
		//一个对于QuietWriter的扩展类,在后端安静的记录着每个文件当前写入了多少Bytes
		this.qw = new CountingQuietWriter(writer, errorHandler);
	}

	/**
	 * 调用subAppend处理日志
	 */
	protected void subAppend(LoggingEvent event) {
		super.subAppend(event);
		if (fileName != null && qw != null) {
			long size = ((CountingQuietWriter) qw).getCount();
			if (size >= maxFileSize && size >= nextRollover) {
				rollOver();
			}
		}
	}
}

       看了上面的代码,我们能知道对于RollingFileAppender类型的Appender,我们可以在log4j.properties中配置其如下属性:

log4j.appender.file=org.apache.log4j.RollingFileAppender 
#来自类AppenderSkeleton的属性layout、threshold、errorHandler和closed
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.Threshold=INFO
log4j.appender.file.errorHandler=org.apache.log4j.helpers.OnlyOnceErrorHandler
log4j.appender.file.closed=false
#来自类WriterAppender的属性immediateFlush和encoding
log4j.appender.file.immediateFlush = true
log4j.appender.file.encoding=UTF-8
#来自类FileAppender的属性fileAppend、fileName、bufferedIO和bufferSize
log4j.appender.file.File=file.log 
log4j.appender.file.Append=false 
log4j.appender.file.BufferedIO=false 
log4j.appender.file.BufferSize=false 
#来自类RollingFileAppender的属性MaxFileSize、MaxBackupIndex
#MaximumFileSize和MaxFileSize都可以设置最大文件的大小
#MaxFileSize可以携带单位KB MB GB,RollingFileAppender会自己解析转换
#MaximumFileSize则直接设置最大文件的大小,不带单位
log4j.appender.file.MaxFileSize=10MB 
log4j.appender.file.MaximumFileSize=10485760 
log4j.appender.file.MaxBackupIndex=10 

       上面我们需要注意的是MaximumFileSize和MaxFileSize都可以设置最大文件的大小,MaxFileSize可以携带单位KB MB GB,RollingFileAppender会自己解析转换,MaximumFileSize则直接设置最大文件的大小,不带单位.

7.Layout代码分析

       Layout主要解决日志格式化输出的问题,类似于C语言对于字符串输出的格式化,其继承关系图如下:

       平时开发环境中使用最多的是PatternLayout,先来看下PatternLayout支持的格式化规则,列表如下:

转换字符作用
cUsed to output the category of the logging event. The category conversion specifier can be optionally followed by precision specifier, that is a decimal constant in brackets.

If a precision specifier is given, then only the corresponding number of right most components of the category name will be printed. By default the category name is printed in full.

For example, for the category name "a.b.c" the pattern %c{2} will output "b.c".

C【请慎用】Used to output the fully qualified class name of the caller issuing the logging request. This conversion specifier can be optionally followed by precision specifier, that is a decimal constant in brackets.

If a precision specifier is given, then only the corresponding number of right most components of the class name will be printed. By default the class name is output in fully qualified form.

For example, for the class name "org.apache.xyz.SomeClass", the pattern %C{1} will output "SomeClass".

WARNING Generating the caller class information is slow. Thus, use should be avoided unless execution speed is not an issue.

注意:使用此种转换字符获取打印日志所在类名会拖慢正常的业务处理,毕竟日志对于正常代码处理还是有影响的,请慎用。

dUsed to output the date of the logging event. The date conversion specifier may be followed by a date format specifier enclosed between braces. For example, %d{HH:mm:ss,SSS} or %d{dd MMM yyyy HH:mm:ss,SSS}. If no date format specifier is given then ISO8601 format is assumed.

The date format specifier admits the same syntax as the time pattern string of the java.text.SimpleDateFormat. Although part of the standard JDK, the performance of SimpleDateFormat is quite poor.

For better results it is recommended to use the log4j date formatters. These can be specified using one of the strings "ABSOLUTE", "DATE" and "ISO8601" for specifying AbsoluteTimeDateFormatDateTimeDateFormat and respectively ISO8601DateFormat. For example, %d{ISO8601} or %d{ABSOLUTE}.

These dedicated date formatters perform significantly better than java.text.SimpleDateFormat.

注意:这里告诉我们其实还有比java的SimpleDateFormat更好的日期格式化工具类,似乎无意中又学习到了新知识

F【请慎用】Used to output the file name where the logging request was issued.

WARNING Generating caller location information is extremely slow and should be avoided unless execution speed is not an issue.

注意:使用此种转换字符获取打印日志所在源代码文件名比如Myclass.java会严重拖慢正常的业务处理,毕竟日志对于正常代码处理还是有影响的,请慎用。

lUsed to output location information of the caller which generated the logging event.

The location information depends on the JVM implementation but usually consists of the fully qualified name of the calling method followed by the callers source the file name and line number between parentheses.

The location information can be very useful. However, its generation is extremely slow and should be avoided unless execution speed is not an issue.

L【请慎用】Used to output the line number from where the logging request was issued.

WARNING Generating caller location information is extremely slow and should be avoided unless execution speed is not an issue.

注意:使用此种转换字符获取源代码里调用打印日志是哪一行代码发起的会严重拖慢正常的业务处理,毕竟日志对于正常代码处理还是有影响的,请慎用。

mUsed to output the application supplied message associated with the logging event.
M【请慎用】Used to output the method name where the logging request was issued.

WARNING Generating caller location information is extremely slow and should be avoided unless execution speed is not an issue.

注意:使用此种转换字符获取源代码里调用打印日志是哪个方法发起的会严重拖慢正常的业务处理,毕竟日志对于正常代码处理还是有影响的,请慎用。

nOutputs the platform dependent line separator character or characters.

This conversion character offers practically the same performance as using non-portable line separator strings such as "\n", or "\r\n". Thus, it is the preferred way of specifying a line separator.

pUsed to output the priority of the logging event.
rUsed to output the number of milliseconds elapsed from the construction of the layout until the creation of the logging event.
tUsed to output the name of the thread that generated the logging event.
xUsed to output the NDC (nested diagnostic context) associated with the thread that generated the logging event.
X

Used to output the MDC (mapped diagnostic context) associated with the thread that generated the logging event. The X conversion character must be followed by the key for the map placed between braces, as in %X{clientNumber} where clientNumber is the key. The value in the MDC corresponding to the key will be output.

See MDC class for more details.

%The sequence %% outputs a single percent sign.

       上面是源代码里关于PatternLayout的参数解释,一些值得我们学习和注意的地方我在上面用特殊颜色进行了标记,其中红色标记的C、F、L、M这四个请慎用,用绿色标记的d使我又学到了更好的关于日期格式化工具类,这里我借用Log4j输出格式控制--log4j的PatternLayout参数含义 里的翻译如下:

       这里仍然以开篇的那张log4j-1.2.17核心类库图来诠释PatternLayout中的组成,也能对log4j-1.2.17整个组成关系有个全面了解,如下图:

       PatternLayout提供了多个预制的表达式让开发者可以在日志中灵活的嵌入想要的信息,比如此日志来源的类名和包名,此日志产生的时间等等,这里开始分析PatternLayout的源代码如下:

public class PatternLayout extends Layout {

	/**
	 * 默认的日志转换表达式,%m代码中的日志事件中的日志字符串,%n是在默认加个换行符
	 */
	public final static String DEFAULT_CONVERSION_PATTERN = "%m%n";

	/**
	 * 日志转换表达式%r [%t] %p %c %x - %m%n,依次含义为日志事件开始值LAOUT创建所花时间 [线程名] 日志优先级 包名 NDC信息 - 日志字符串 换行符
	 */
	public final static String TTCC_CONVERSION_PATTERN = "%r [%t] %p %c %x - %m%n";

	protected final int BUF_SIZE = 256;
	protected final int MAX_CAPACITY = 1024;

	// output buffer appended to when format() is invoked
	private StringBuffer sbuf = new StringBuffer(BUF_SIZE);

	private String pattern;

	private PatternConverter head;

	/**
	 * 构造函数
	 */
	public PatternLayout() {
		this(DEFAULT_CONVERSION_PATTERN);
	}

	/**
	 * 构造函数
	 */
	public PatternLayout(String pattern) {
		this.pattern = pattern;
		head = createPatternParser((pattern == null) ? DEFAULT_CONVERSION_PATTERN : pattern).parse();
	}

	/**
	 * 设置日志转换表达式
	 */
	public void setConversionPattern(String conversionPattern) {
		pattern = conversionPattern;
		head = createPatternParser(conversionPattern).parse();
	}

	/**
	 * 获取日志转换表达式
	 */
	public String getConversionPattern() {
		return pattern;
	}

	/**
	 * 其他属性设置
	 */
	public void activateOptions() {
		// nothing to do.
	}

	/**
	 * 是否忽略异常
	 */
	public boolean ignoresThrowable() {
		return true;
	}

	/**
	 * 创建日志格式化解析器
	 */
	protected PatternParser createPatternParser(String pattern) {
		return new PatternParser(pattern);
	}

	/**
	 * 将用户提供的日志字符串按照预定的格式化进行逐个格式化
	 */
	public String format(LoggingEvent event) {
		// Reset working stringbuffer
		if (sbuf.capacity() > MAX_CAPACITY) {
			sbuf = new StringBuffer(BUF_SIZE);
		} else {
			sbuf.setLength(0);
		}

		PatternConverter c = head;

		while (c != null) {
			c.format(sbuf, event);
			c = c.next;
		}
		return sbuf.toString();
	}
}

       在上面的代码中我们需要关注方法public void setConversionPattern(String conversionPattern),它的作用是创建日志转换表达式解析器,并调用其解析器将日志转换表达式解析成一个链表,链表中每个元素对应一种格式的转换器,后续使用链表转换器逐个转换日志,这里我们继续从这里入手查看PatternParser如何解析日志转换表达式的,这里给出PatternParse.java的核心代码注释:

       查看上面的代码最好心中带着一个被解析的字符串log4j.appender.Info.layout.ConversionPattern=[%-5p] %d{HH:mm:ss:SSS} method:%C{1}.%M%n%m%n去理解,PatternParser其实就是将这个字符串解析成了一个链,后面调用这个链来处理日志事件。

 

8.Filter源代码分析

       最后来聊聊Filter,它主要解决当程序员已经在代码中内置了log4j-1.2.17,突然有一天发现日志打印消耗系统性能想禁用日志功能,那么此时Filter就起到作用。

       Filter的类继承关系如下:

       很多人对于Filter很陌生,那么这里先来几个例子解释下使用:

log4j.appender.Info.filter=org.apache.log4j.varia.LevelMatchFilter
log4j.appender.Info.filter.levelToMatch=WARN
log4j.appender.Info.filter.acceptOnMatch=true

       上面配置针对appender=Info,启用了类型为LevelMatchFilter的过滤器,设置了过滤器LevelMatchFilter的属性levelToMatch=WARN且acceptOnMatch=true,含义是这个appender只打印级别为WARN的日志。

log4j.appender.Info.filter=org.apache.log4j.varia.DenyAllFilter

       上面配置针对appender=Info,启用了类型为DenyAllFilter的过滤器,含义是这个appender对于一切日志都不打印,如果某天你不想打印日志了,那么这个配置能帮上你大忙哦。

log4j.appender.Info.filter=org.apache.log4j.varia.LevelRangeFilter
log4j.appender.Info.filter.levelMin=INFO
log4j.appender.Info.filter.levelMax=ERROR
log4j.appender.Info.filter.acceptOnMatch=true

       上面配置针对appender=Info,启用了类型为LevelRangeFilter的过滤器,设置了过滤器LevelRangeFilter的属性levelMin=INFO且levelMax=ERROR且acceptOnMatch=true,含义是这个appender只打印级别在INFO到ERROR这个范围的日志,也即级别为INFO、WARN和ERROR的日志。

log4j.appender.Info.filter=org.apache.log4j.varia.StringMatchFilter
log4j.appender.Info.filter.stringToMatch=Chinese
log4j.appender.Info.filter.acceptOnMatch=true

       上面配置针对appender=Info,启用了类型为StringMatchFilter的过滤器,设置了过滤器StringMatchFilter的属性stringToMatch=Chinese且acceptOnMatch=true,含义是这个appender只处理日志中包含字符串"Chinese"的日志事件。

9.日志对于业务的影响

       对于log4j-1.2.17的源代码的分析接近尾声了,对于log4j-1.2.17日志的使用有了全新的认识,之前的一些疑问也在本次对于源代码的阅读中一一解答,这些疑问也是许多和我类似的程序员一直想要了解却不知道哪里去了解的痛点问题,这些问题我会在一个单独的博客中采用问答式进行解答,这里先列出如下问题:

  • 为什么就默认加载了log4j.properties文件?为什么不是其它文件?
  • 每个appender的属性有哪些?在哪里能找到关于一个appender的所有可配置属性?比如DailyRollingFileAppender的属性为什么就是File、DatePattern、layout 和Threshold?
  • appender中配置属性File、配置属性FILE、配置属性file,尽管是大小写的区别,log4j会识别吗?log4j是如何去将这些属性设置给appender的呢?
  • log4j中DailyRollingFileAppender中属性DatePattern就只能控制按天输出么?按小时行不行?按月行不行?值配置每周输出一个日志文件行不行?如何配置?
  • 想让日志中输出log4j加载了哪个配置文件和解析配置文件的过程这些日志被打印出来,怎么做?
  • 为什么大部分人上来就配置了log4j.rootLogger这个属性,不配置这个就不打印日志了吗?程序中Logger.getLogger(Test.class)和Logger.getLogger("test")后面到底干了些啥?这两种获取Logger的方式有啥区别么?
  • 配置了log4j.rootLogger,也配置了log4j.logger,这两种方式有区别么?为什么配置log4j.logger里的日志会再次在log4j.rootLogger所在的日志文件中输出导致日志占用磁盘空间翻倍?
  • log4j找不到log4j.properties文件会影响正常业务么?
  • log4j影响整个业务处理的性能么?哪些环节有影响?如何避免对于正常业务逻性能的影响?如何去优化?
  • log4j中Logger.getLogger(Test.class)获取的Logger对象是每个地方都是一个实例对象还是公用一个单例?怎么做到的?
  • 将日志写入文件是使用的java IO的那种流(字符流/字节流)?使用的是FileOutputStream BufferedOutputStream FileWriter这3种java操作文件中的哪种?可配置吗?怎么配置?
  • 日志格式化常用类org.apache.log4j.PatternLayout的各种转换字符的含义?
  • 代码里已经内置了log4j日志输出,如果现在想禁用一切日志输出,该怎么做?是去代码中注释每个输出么?

       上面这些问题,感兴趣的可以研究下,这些问题会在独立的一个博客中采用问答的形式进行解答。

 

 

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐