研究了许久,作为半路出家的码农,还是记下学习的成果。(本文针对的版本是1.2.17)
初学者在记录某段程序结果或者某段记录时,最常用的就是:
System.out.println("test String...");
但是这个输出方法是系统控制台的输出,在实际生产环境会被埋没在海量的日志中。所以有了很多日志框架,这里结合自己学习的Apache的log4j日志框架,从源码阶段分享下该框架的实现和使用!(第一次写博客,忘批评指正!)
开始前,请思考以下几个问题:
- 输出的内容有格式要求吗?
- 日志输出到哪里?控制台?页面?还是文件中?
- 可以选择有些日志输出,有些日志不输出吗?
- 系统日志那么多,我怎么找到我要的日志?
针对这些疑问,很容易想到:
- 原来的test String...可能会在不同的场景中会有不同的格式要求,——log4j抽象出Layout来描述日志的格式
- 日志的输出位置也不仅仅限制于控制台,——log4j抽象出Appender来描述日志的输出形式
- 在开发时,可以debug打日志调试,但是在生产环境就不需要,——log4j抽象出Level来描述日志的级别
- 给一个Logger名字是不是更好管理他们?————log4j抽象出Category和LoggerRepository来描述日志和日志管理容器
那么,log4j的工作流程以及它工作的源码细节又是如何呢?我们通过debug追溯他的执行轨迹。
先简述下流程:
- 入口:Logger log = Logger.getLogger(AppTest.class);
- Logger.getLogger(Class clazz);实际上启动LoggerManager日志管理器,该管理器主要工作是加载log4j配置文件,并进行初始化;
- 配置文件解析,初始化rootLogger,添加Appender,Layout等配置;
- 注册logger实例到LoggerRepository,更新层级节点关系;
- 执行日志记录行为,debug?info?error?
- 判别级别后,让所有日志输出器执行输出;
- 输出前会进行过滤和样式渲染;
- 指定形式输出。
到这里流程大体就是这样了,看起来不清晰,而且流程中的处理似乎和开头问题中出现的抽象的类关联不大,没关系,我们先大体宏观上清楚log4j是如何工作的。 接下来,将重点结合源码逐步解析。
一、首先啰嗦之前出现的所有的类:
- Logger——日志行为类,debug,info等日志记录行为,
- Category——Logger的父类
- LoggerRepository——日志容器,是管理Logger的容器,内部维护一个Hashtable,储存所有的Logger
- LoggerManager——日志管理,日志框架的入口,由他开始启动日志系统,加载配置,初始化根日志
- Appender——日志输出器
- Layout——日志形式
- Level——日志级别
- rootLogger——根日志,每个Logger都有父级Logger,是层级关系,如果没有,则根日志就是父级,根日志是最后节点
二、详解:
1、Logger
public class Logger extends Category { // 继承Category
protected Logger(String name) {
super(name);
}
}
Logger在Category的基础上扩展了一个trace()行为,低于debug,重点看Category类
public class Category implements AppenderAttachable { // 实现AppenderAttachable接口,改接口是日志输出器的管理接口
protected String name; // logger的名字
volatile protected Level level; // logger的级别
volatile protected Category parent; // 父logger,log4j支持a.b.c这样的命名logger,但是会继续解析a.b和a为临时节点
private static final String FQCN = Category.class.getName(); // 默认命名
protected ResourceBundle resourceBundle; // 资源绑定对象
protected LoggerRepository repository; // logger容器,每个日志也知道自己属于哪个容器
AppenderAttachableImpl aai; // 日志输出器的默认管理接口实现类,每一个Logger都会把自己的Appender交给aai来管理,Logger在记录日志行为实际上都是aai在管理输出器的输出
protected boolean additive = true; // 继承标记,如果为false,表示不能使用父类Logger的Appender
protected Category(String name) {
this.name = name;
}
}
由源码可以看出,每一个logger实际上都是一个层级的有行为的日志输出器管理者,只是他的所有输出器都交由aai来管理,如下源码:
public void debug(Object message, Throwable t) { // 所有日志记录行为,都是判断级别后,调用forceLog方法
if(repository.isDisabled(Level.DEBUG_INT))
return;
if(Level.DEBUG.isGreaterOrEqual(this.getEffectiveLevel()))
forcedLog(FQCN, Level.DEBUG, message, t);
}
// 而forceLog则是调用callAppenders方法,同时把日志内容封装为LoggerEvent对象
protected void forcedLog(String fqcn, Priority level, Object message, Throwable t) {
callAppenders(new LoggingEvent(fqcn, this, level, message, t));
}
,在执行日志行为时,实际上都是aai管理的Appender在输出
public void callAppenders(LoggingEvent event) { // loggerEvent是对日志内容的封装,把要输出的内容封装成该对象
int writes = 0;
// 遍历logger及父级,每个logger的aai输出管理器输出
for(Category c = this; c != null; c=c.parent) {
synchronized(c) {
if(c.aai != null) {
writes += c.aai.appendLoopOnAppenders(event); // 每级logger的aai执行appendLoopOnAppenders
}
if(!c.additive) { //如果父级的additive为false,则不再执行父级logger的Appender
break;
}
}
}
// 如果Appender数为0,监听警告
if(writes == 0) {
repository.emitNoAppenderWarning(this);
}
}
由上可以看出,总结如下:
- Logger是有层级的,内部维护aai,管理所有的Appender,并且可以调用父级的Appender,aai后面讲Appender时详解。
- Logger可以用父类的Appender,通过设置父类的additive为false,如果不让子级用本级的Appender,则本级设为false
- Logger的日志内容是经过包装的,包装后的内容是不是可以添加样式Layout,是不是便于Appender输出呢?
- Logger的日志行为是有级别的,级别下篇详解。