(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.x和log4j-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依赖:
log4j log4j 1.2.17
然后也很自然的在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.java的 getLogger方法,那么我们有必要查看下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如何初始化的核心过程,总结如下:
- 首先如果操作系统设置了环境变量log4j.defaultInitOverride=false或者没有设置,这里的初始化过程才会执行,否则不执行;
- 接着读取操作系统配置的环境变量log4j.configuration的值,该值告诉log4j框架加载哪个配置文件,如果没有配置该环境变量,那么先读取名字为log4j.xml的配置文件,如果log4j.xml也不存在,就读取默认的配置文件log4j.properties;
- 试图将步骤2中读取的配置文件转换为URL形式;
- 如果步骤3中配置文件不是URL的形式,那么就从工程的classpath路径加载配置文件;
- 如果仍然无法找到配置文件,那么就放弃出花花过程。
用一张流程图解释如下:
在上面的代码中我们看到配置文件加载的优先顺序为:
接下来通过工具类OptionConverter.java的static 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。