log4j源码分析(版本1.2.17)
liyakun.hit
2017-12-04
1. Log4j使用方式
1.1 引入jar包
在pom文件中添加如下依赖:
1.2 代码集成
在源码中集成Log4j的使用示例如下:
1 | import org.apache.log4j.Logger; |
1.3 修改配置
上面的源码中的修改,只是代表了日志会进入到log4j的系统中,但是日志最终会以什么格式输出到哪些地方还没有被指定,这些信息是通过修改配置文件log4j.properties来指定的,先看一个log4j.properties的配置示例:
log4j.rootLogger=INFO, file, databus
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.file=${log.file}
log4j.appender.file.append=false
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p %-60c - %m%n
log4j.appender.databus=com.data.databus.DatabusAppender
log4j.appender.databus.channel=container_level_log
log4j.appender.databus.layout=org.apache.log4j.PatternLayout
log4j.appender.databus.threshold=INFO
log4j.appender.databus.layout.ConversionPattern=databus4j %X{ip} %X{user} %X{pid} %X{cid} %d{yyyy-MM-dd_HH:mm:ss} %p %c{1}: %m
简单介绍一下:
- 首先是要配置log4j.rootLogger,其值的第一个字段INFO代表着rootLogger的日志级别,这个字段是可选项,如果不配置,则默认为所有日志。后面的值每一个都代表一个log处理器(appender),每个appender之间是并列的,每条日志都会经过所有appender的处理。
- 然后是为每个appender写出它们各个的相关配置,这些相关配置主要包括以下四种:
- log4j.appender.$name是这个处理器载入的类
- log4j.appender.$name.$others是要传递给这个类的参数,可以任意多种,只要相应的appender的类能处理就行。
- log4j.appender.$name.layout是日志的行格式的处理类
- log4j.appender.$name.layout.$others是要传递给行格式处理类的参数,可以任意多种。
重点介绍一下PatternLayout的参数ConversionPattern,对于格式的控制主要包含两类内容:
- 字段类型
- %p: 输出日志信息优先级,即DEBUG,INFO,WARN,ERROR,FATAL,
- %d: 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyyy-MM-dd HH:mm:ss,SSS},输出类似:2011-10-18 22:10:28,921
- %r: 输出自应用启动到输出该log信息耗费的毫秒数
- %c: 输出日志信息所属的类目,通常就是所在类的全名
- %t: 输出产生该日志事件的线程名
- %l: 输出日志事件的发生位置,相当于%C.%M(%F:%L)的组合,包括类目名、发生的线程,以及在代码中的行数。
- %x: 输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像java servlets这样的多客户多线程的应用中。
- %%: 输出一个”%”字符
- %F: 输出日志消息产生时所在的文件名称
- %L: 输出代码中的行号
- %m: 输出代码中指定的消息,产生的日志具体信息
- %n: 输出一个回车换行符,Windows平台为”\r\n”,Unix平台为”\n”输出日志信息换行
- 字段控制
- %c{2} : 保留类名值的最后两个部分,比如类名是a.b.c.d,那么%c{2}的值为c.d
- %20c : 指定类名最小的宽度是20,如果类名小于20的话,默认的情况下右对齐。
- %-20c : 指定类名最小的宽度是20,如果类名小于20的话,”-“号指定左对齐。
- %.30c : 指定类名最大的宽度是30,如果类名大于30的话,就会将左边多出的字符截掉,但小于30的话也不会有空格。
- %20.30c : 如果类名小于20就补空格,并且右对齐,如果长于30字符,就把左边的字符截掉
- %-20.30c : 若名字空间长度小于20,则右边用空格填充;若名字空间长度超过30,截去多余字符
下面解读一下上面的例子中配置文件:
- 第一个appender名字叫做file,类型是由log4j官方实现的org.apache.log4j.FileAppender
- 这个FileAppender要处理一个名为file的参数,这个参数的值是从jvm中读取的变量log.file的值
- 这个FileAppender还要处理一个名为append的参数,这个参数代表在第一交写入前是否需要把原文件清空
- 这个FileAppender使用的行格式方法为log4j中实现的PatternLayout,这个格式允许用户灵活地自定义日志格式
- 这个FileAppender中的PatternLayout接收一个ConversionPattern的参数,这个参数的值代表要使用的行格式,在这个例子中是:
- [以yyyy-MM-dd HH:mm:ss,SSS为格式的时间] [日志级别(至少5位长度,左对齐)] [类名(至少60位长度,左对齐)] - [消息内容][换行]
- 第二个appender的名字叫做databus,类型是由用户自己实现(后面介绍如何自定义appender)的com.data.databus.DatabusAppender
- 这个appender会处理自己定义好的参数channel,threshold
- 这个appender使用的行格式方法也是PatternLayout
- 这个appender使用的PatternLayout的ConversionPattern参数有所不同,并且包含一些不存在上面介绍的字段类型,比如%X{ip} %X{user} %X{pid} %X{cid}这些,这些是用户自定义的,后面会介绍。
2. 源码分析
用户直接使用的代码一共只有两行,分别是:
- 初始化方法:private static Logger logger = Logger.getLogger(YK.class);
- 添加日志方法:logger.info(“你的消息”);
下面分别沿着这两条线进行分析。
2.1 初始化方法
从private static Logger logger = Logger.getLogger(YK.class);这行代码入手。从Import中可以看到Logger类的位置是:org.apache.log4j.Logger。找到它的getLogger(Class)方法:
1 | //位置:log4j/src/main/java/org/apache/log4j/Logger.java |
跟进到LogManager里面,找到getLogger(String)方法:
1 | //位置:log4j/src/main/java/org/apache/log4j/LogManager.java |
到这里先暂停不进入getLogger(name)方法的内部,先要找到静态成员变量repositorySelector初始化的位置如下:
1 | //位置:log4j/src/main/java/org/apache/log4j/LogManager.java |
- 首先是创建了一个Hierarchy的实例h,它实现了LoggerRepository接口,是一个Logger的容器。
- 然后使用h创建了DefaultRepositorySelector(默认的Logger容器选择器)的实例repositorySelector。
- 接下来是找配置文件的过程,先找log4j.configuration,如果没有,再找log4j.xml,如果还没有,再找log4j.properties
- 最后,调用OptionConverter.selectAndConfigure(url, configuratorClassName, LogManager.getLoggerRepository());方法,对Logger容器进行初始化。
下面继续跟进到OptionConverter类中:
1 | //位置:log4j/src/main/java/org/apache/log4j/helpers/OptionConverter.java |
首先根据参数选择合适的解析类,这里假设使用了log4j.properties,那么会使用PropertyConfigurator来进行解析,下面进入它的doConfigure方法:
1 | //位置:log4j/src/main/java/org/apache/log4j/PropertyConfigurator.java |
这个方法主要是读取了配置文件,放到props对象里面,然后调用doConfigure(props, hierarchy)方法,继续跟进:
1 | //位置:log4j/src/main/java/org/apache/log4j/PropertyConfigurator.java |
- 首先配置是否打开自己的调试(打日志)功能,然后找log4j.threshold来设置最低的日志处理级别。
- configureRootCategory是用来处理配置文件中的rootLogger的。这个下面会详细介绍。
- configureLoggerFactory是用来配置logger的工厂类
- parseCatsAndRenderers是用来配置一些非rootLogger这些配置的其它配置。比如打印对象时的render方法,String类型不支持render,只能使用layout控制。
下面仔细看一下configureRootCategory方法里面:
1 | //位置:log4j/src/main/java/org/apache/log4j/PropertyConfigurator.java |
先找”log4j.rootLogger”,再找”log4j.rootCategory”,这个rootCategory是历史遗留问题,可以忽略。看到这个同步锁,就知道关键点要来了,下面进入到parseCategory()方法里面。
1 | //位置:log4j/src/main/java/org/apache/log4j/PropertyConfigurator.java |
- 首先为rootlogger设置LEVEL
- 然后调用removeAllAppenders方法清除所有的appender
- 然后开始依次添加appender。具体来说,就是先在rootLogger那一行找到一个名字,然后调用parseAppender(props, appenderName)看能否实例化一个Appender,如果可以实例化成功,那就把它添加到logger中。这里不再继续跟进到logger.addAppender(appender)方法内部了,但是要提一点,就是这个方法中,在logger对象中创建了一个AppenderAttachableImpl的对象aai,然后调用aai.addAppender(newAppender)方法,把这个appender放入到aai中了,后面打印日志的时候还需要使用这个对象。
下面再跟进到parseAppender(props, appenderName)方法中:
1 | //位置:log4j/src/main/java/org/apache/log4j/PropertyConfigurator.java |
- 先看是否已经添加了,如果已经添加,就返回
- 然后找到appenderName对应的类,通过反射进行实例化,设置appendername。
- 再找到appenderName使用的layout对应的类,通过反射进行实例化。
- 最后返回这个实例化的appender。
下面回到LogManager类里面,沿着一开始跟进的路线继续:
repositorySelector.getLoggerRepository()这个方法返回的对象是Hierarchy的实例,下面进入它的getLogger(name)方法内部:
1 | //位置:log4j/src/main/java/org/apache/log4j/Hierarchy.java |
这里的defaultFactory是在构造函数里面创建的DefaultCategoryFactory类的实例,下面继续跟进到getLogger(name, defaultFactory)方法内部:
1 | //位置:log4j/src/main/java/org/apache/log4j/Hierarchy.java |
先对全局的Hashtable的对象ht进行同步锁,然后从里面根据key名取出logger对象,根据logger对象的不同,有以下三种情况:
- logger对象为null,此时调用DefaultCategoryFactory类中的makeNewLoggerInstance方法创建一个Logger的实例
- logger对象为一个Logger类的实例,那么直接返回它
- logger对象为一个ProvisionNode类的实例,那么也通过DefaultCategoryFactory类中的makeNewLoggerInstance方法创建一个Logger的实例,并重新梳理一下各个logger的父子关系。再返回这个Logger类的实例。
这里的的ProvisionNode类是什么含义呢?其实Log4j对于logger的管理是树形的,比如loger(“x.y.z”)的父亲节点是loger(“x.y”),如果只创建了loger(“x.y.z”)而没有实例化loger(“x.y”),就会先放置一个ProvisionNode类(其实就是一个Vector)的实例来充当父亲节点。
无论如何,最终是返回了一个Logger类(log4j/src/main/java/org/apache/log4j/Logger.java)的实例。
2.2 添加日志方法
从用户使用的这一行代码logger.info(“你的消息”)入手。
根据上面的初始化方法的分析,可以得知,这里的logger方法其实是一个Logger类的实例。但是log4j/src/main/java/org/apache/log4j/Logger.java这个类中并没有声明info(String)这个方法,是因Logger类继承了Category类,下面看一下Category类中的Info(String)方法:
1 | //位置:log4j/src/main/java/org/apache/log4j/Category.java |
先看一下是否禁用了INFO这个Level,然后确认一下是否比当前有效的level要大,就调用forcedLog方法,下面继续跟进:
1 | //位置:log4j/src/main/java/org/apache/log4j/Category.java |
这个方法比较简单,先是创建了一个LoggingEvent对象,然后继续跟进到callAppenders方法中:
1 | //位置:log4j/src/main/java/org/apache/log4j/Category.java |
这里的外层循环,是由于log4j的树形结构管理的功能,一个logger要处理的日志,它的父亲节点也要处理。
循环里面首先是以当前logger对象进行同步锁,然后把输入的LoggingEvent对象放到类AppenderAttachableImpl的appendLoopOnAppenders方法中。如果一个也没有放入,就打印一个warning。
下面跟进到appendLoopOnAppenders(LoggingEvent)方法内部:
1 | //位置:log4j/src/main/java/org/apache/log4j/helpers/AppenderAttachableImpl.java |
这个方法内部,就是简单的把每个appender遍历了一下,然后依次调用了各自的doAppend(LoggingEvent)方法。
这里的appender的具体实现可能会有很多种,现在为了介绍方便,下面就以org.apache.log4j.FileAppender这个类来进行介绍,同时,使用org.apache.log4j.PatternLayout来作为介绍的layout。
下面进入到FileAppender类的doAppend(LoggingEvent)方法,这个类继承了WriterAppender类,然后WriterAppender类继承了AppenderSkeleton抽象类,在AppenderSkeleton这个抽象类中定义了doAppend(LoggingEvent)方法,下面跟进到这个类中:
1 | //位置:log4j/src/main/java/org/apache/log4j/AppenderSkeleton.java |
- 这是一个类级别同步的方法
- 首先看是否已经关闭,再看是否能通过过滤,最后调用自己的append(LoggingEvent)方法。
由于FileAppender中依然没有实现append方法,看WriterAppender中的append方法:
1 | //位置:log4j/src/main/java/org/apache/log4j/WriterAppender.java |
这个方法中,只是检查了一些条件,然后调用了subAppend()方法,继续跟进:
1 | //位置:log4j/src/main/java/org/apache/log4j/WriterAppender.java |
- 首先是调用了layout的format方法,来把LoggingEvent格式化为一个字符串,这个不再详细介绍了,详细的内容可以参考:log4j/src/main/java/org/apache/log4j/helpers/PatternParser.java。
- 然后判断layout是否忽略了抛出的异常信息(PatternLayout中会忽略异常信息),如果在layout中忽略了,那么就在WriterAppender中把异常栈信息一行一行的拿出来处理一下。
- 最后看一下是否需要立即刷新到文件,默认是true,即每次调用都会刷新到文件中。
然后,在这个方法里面通过this.qw.write(String)来处理日志,这个qw是在FileAppender的构造函数中实例化的,过程如下:
1 | //位置:log4j/src/main/java/org/apache/log4j/FileAppender.java |
然后进入到setFile方法中:
1 | //位置:log4j/src/main/java/org/apache/log4j/FileAppender.java |
这里主要是负责创建目录,然后创建一个OutputStreamWriter对象fw,再调用setQWForFiles方法:
1 | //位置:log4j/src/main/java/org/apache/log4j/FileAppender.java |
这样,qw对象就实例化出来,qw.write(String)方法,就可以把日志写入到相应的文件中。
以上就是Log4j的工作原理,深入了解一下,还是挺有意思的。
后面有时间的时候,争取再写一篇如何定制化实现自己的Appender的文章。