Apache log4j-1.2.17源码学习笔记
(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依赖:
然后也很自然的在maven的目录src/main/resource下创建了log4j.properties文件,配置了如下内容:
最后很自然的在我们的类里面使用着Logger打印日志:
一切都感觉理所当然,顺理成章,一气呵成,log4j-1.2.17干的事多了,程序员干的事情就相对少了,所以感觉log4j-1.2.17如此的好用,所以log4j-1.2.17有这么广泛的使用。
我想说的是程序员不能只满足于此,在空闲的时候不妨静下心来阅读以下log4j-1.2.17的源代码,研究一下其初始化过程,日志输出操作如何进行,日志格式化如何进行的,这样程序员才会一步步朝着架构师的道路前进,提升自己,提升工作效率,在工作中寻找创新点。
3.log4j-1.2.17初始化(加载配置文件和解析配置文件)代码分析
在打印日志时候我们很自然的添加了这行代码:
这里就可以作为我们查看Apache log4j-1.2.17源代码的一个入口,在官网下载源码log4j-1.2.17.zip,查看Logger.java的代码如下: 我们看到这里Logger类调用了LogManager.java的getLogger方法,那么我们有必要查看下LogManager的代码,LogManager里面有个static代码块,代码如下: 上面的代码已经完全解释了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提供的所有的配置文件读取解析器相关类继承关系图如下:
因为常用的配置文件为log4j.properties,所以这里我以PropertyConfigurator.java类进行讲解,先通过一张处理流程图来对PropertyConfigurator的读取配置过程有个大致了解:
给出PropertyConfigurator.java的相关注释代码如下:
在上面我们也看到关于日志过滤器的配置,这里将日志过滤器的相关类继承关系图绘图如下:
到这里为止,我们完成了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个文件。开篇我们提到了这么一段使用日志的代码:
现在我们来看下Logger.getLogger(LoggerTest.class)干了些啥,先来一张图来诠释:
上图告诉我们创建了一个全新的Logger,这个Logger没有任何的appender,其parent属性指向了起初创建的RootLogger,我们查看Logger.java的相关代码,注释如下:
继续跟进LogManager.java的相关代码,注释如下:
在LogManager的static代码块中通过DefaultRepositorySelector(new Hierarchy(new RootLogger((Level) Level.DEBUG)))创建了DefaultRepositorySelector类型的repositorySelector,上面对于repositorySelector.getLoggerRepository的调用实际上是调用了接口LoggerRepository的实现类Hierarchy的getLoggerRepository方法,其类继承关系图如下:
继续跟进Hierarchy.java的相关代码,注释如下:
5.Logger.info、Logger.warn和Logger.error代码分析
还是先通过一张图来诠释这个过程:
我们就以logger.info("info")作为入口,跟进一下其代码调用。我们还是先回顾一下之前关于Logger组成的关系图如下:
从上图中我们知道Logger.java的一切行为都转交给Category.java处理,这里我们注释Category.java相关代码如下:
我们继续跟踪代码c.aai.appendLoopOnAppenders(event),这里aai类型为AppenderAttachableImpl,AppenderAttachableImpl里通过类型为Vector的变量appenderList维护着Logger对应的appender,这里遍历Logger对应的appender,调用appender.doAppend(event),注释AppenderAttachableImpl.java的相关代码如下:
至于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的支持,比如能根据日志级别进行过滤.,注释其代码如下:
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,注释其代码如下:
6.3 ConsoleAppender.java
ConsoleAppender是往console里丢入日志,ConsoleAppender具体调用的是java的System.out和System.err,默认使用System.out,注释其代码如下:
看了上面的代码,我们能知道对于ConsoleAppender类型的Appender,我们可以在log4j.properties中配置其如下属性:
这里需要明确一点,不管是log4j.properties或者log4j.xml,在解析到appender的属性后,都是调用java的反射机制首先通过Appender的空的构造方法创建一个对象,然后通过反射机制继续设置其属性值。ConsoleAppender的可配置属性如上,它们分别来自于类AppenderSkeleton、WriterAppender和ConsoleAppender。- 属性名为null或者为空白字符,直接返回属性名;
- 属性名长度大于或者等于2,并且属性名的前2位都是大写,那么直接返回属性名;
- 非上面两种情况,则直接将首字母转换为小写然后返回属性名。
- 按照属性转换规则标准化属性名后,将属性名的首字母大写,然后在其前面加上字符串set,反射类里面这个方法进行值设置。
所以不管属性名字是target还是Target,最后都会通过反射方法setTarget去设置属性值。
6.4 FileAppender.java
FileAppender使用java.io.Writer来讲日志写入到磁盘文件,注释其代码如下:
看了上面的代码,我们能知道对于FileAppender类型的Appender,我们可以在log4j.properties中配置其如下属性: 如果你对上面的配置里的属性比如File的首字母是否要大写很纠结,请参见6.3环节里的介绍。
6.5 DailyRollingFileAppender.java
DailyRollingFileAppender仍然使用java.io.Writer来讲日志写入到磁盘文件,不同的是它可以控制按照天存储文件、按照小时存储文件、按照分钟存储文件、按照月份存储文件和按照周存储文件,其配置说明如下:
'.'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中配置其如下属性:
6.6 RollingFileAppender.java
RollingFileAppender扩展了FileAppender,支持按照文件大小来分割文件,代码注释如下:
看了上面的代码,我们能知道对于RollingFileAppender类型的Appender,我们可以在log4j.properties中配置其如下属性:
上面我们需要注意的是MaximumFileSize和MaxFileSize都可以设置最大文件的大小,MaxFileSize可以携带单位KB MB GB,RollingFileAppender会自己解析转换,MaximumFileSize则直接设置最大文件的大小,不带单位.
7.Layout代码分析
Layout主要解决日志格式化输出的问题,类似于C语言对于字符串输出的格式化,其继承关系图如下:
平时开发环境中使用最多的是PatternLayout,先来看下PatternLayout支持的格式化规则,列表如下:
转换字符 | 作用 |
c | Used 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. 注意:使用此种转换字符获取打印日志所在类名会拖慢正常的业务处理,毕竟日志对于正常代码处理还是有影响的,请慎用。 |
d | Used 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 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 These dedicated date formatters perform significantly better than 注意:这里告诉我们其实还有比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会严重拖慢正常的业务处理,毕竟日志对于正常代码处理还是有影响的,请慎用。 |
l | Used 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. 注意:使用此种转换字符获取源代码里调用打印日志是哪一行代码发起的会严重拖慢正常的业务处理,毕竟日志对于正常代码处理还是有影响的,请慎用。 |
m | Used 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. 注意:使用此种转换字符获取源代码里调用打印日志是哪个方法发起的会严重拖慢正常的业务处理,毕竟日志对于正常代码处理还是有影响的,请慎用。 |
n | Outputs 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. |
p | Used to output the priority of the logging event. |
r | Used to output the number of milliseconds elapsed from the construction of the layout until the creation of the logging event. |
t | Used to output the name of the thread that generated the logging event. |
x | Used 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 See |
% | 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 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很陌生,那么这里先来几个例子解释下使用:
上面配置针对appender=Info,启用了类型为LevelMatchFilter的过滤器,设置了过滤器LevelMatchFilter的属性levelToMatch=WARN且acceptOnMatch=true,含义是这个appender只打印级别为WARN的日志。
上面配置针对appender=Info,启用了类型为DenyAllFilter的过滤器,含义是这个appender对于一切日志都不打印,如果某天你不想打印日志了,那么这个配置能帮上你大忙哦。
上面配置针对appender=Info,启用了类型为LevelRangeFilter的过滤器,设置了过滤器LevelRangeFilter的属性levelMin=INFO且levelMax=ERROR且acceptOnMatch=true,含义是这个appender只打印级别在INFO到ERROR这个范围的日志,也即级别为INFO、WARN和ERROR的日志。
上面配置针对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日志输出,如果现在想禁用一切日志输出,该怎么做?是去代码中注释每个输出么?
上面这些问题,感兴趣的可以研究下,这些问题会在独立的一个博客中采用问答的形式进行解答。
上一篇: DolphinDB基础概念理解:Orca
下一篇: 使用数据驱动进行配对交易:简单交易策略