可以说是相当的简便与灵活。下面一段代码简单地说明了后者的实现。
Logger MY_LOG = Logger.getLogger("MY_LOG");
//文件形式的输出方式实例化
DailyRollingFileAppender appender = new DailyRollingFileAppender();
appender.setName(name);
appender.setAppend(true);
appender.setEncoding("GBK");
//文本的输出格式采用PatternLayout
appender.setLayout(new PatternLayout(pattern));
appender.setFile(new File(getLogPath(), fileName).getAbsolutePath());
appender.activateOptions();
//将appender加入到MY_LOG的appender集合
MY_LOG.addAppender(appender);
//设定日志输出级别为INFO
MY_LOG.setLevel(Level.INFO);
Log4j初始化的代码是在LogManager的静态块里面,这个静态块无论如何都会实例化一个日志级别为DEBUG的RootLogger,并且初始化一个以这个RootLogger为根节点的级联结构,然后检查有没有用户指定重写这个日志系统的初始化工作,如果没有,那么先去找log4j.xml,如果没有找到,那么再去找log4j.properties(也就是log4j.xml的优先级高于log4j.properties), 只有找到这两个配置文件的其中一个,再初始化文件里面内容,主要是一些配置文件中指定的Logger初始化,以及各个Logger的Appender列表设定,各个Appender的具体实例,Layout等。其实,没找到任何log4j配置文件也没什么关系,因为已经有RootLogger,日志系统骨架已经完成了,所以也可以通过如前面的代码添加Logger。
//初始化以RootLogger为根的级联结构,rootLogger默认DEBUG级别
Hierarchy h = new Hierarchy(new RootLogger((Level) Level.DEBUG));
//有没指定log4j.configuration(内部使用,不知出于什么目的,未探究)
if(configurationOptionStr == null) {
//加载classpath下log4j.xml
url = Loader.getResource(DEFAULT_XML_CONFIGURATION_FILE);
if(url == null) {
//如果log4j.xml没有,加载log4j.properties
url = Loader.getResource(DEFAULT_CONFIGURATION_FILE);
}
} else {
try {
url = new URL(configurationOptionStr);
} catch (MalformedURLException ex) {
url = Loader.getResource(configurationOptionStr);
}
}
if(url != null) {
LogLog.debug("Using URL ["+url+"] for automatic log4j configuration.");
//开始真正解析配置文件
OptionConverter.selectAndConfigure(url, configuratorClassName,LogManager.getLoggerRepository());
} else {
LogLog.debug("Could not find resource:["+configurationOptionStr+"].");
}
简要介绍完Log4J的初始化后,我们有必要来看下从Logger myLogger =LogManager. getLogger(“MY_ LOG”),到 myLogger.debug(“some message”)结束之后,Log4j到底为我们做了些什么。
上面这幅序列图主要描述的过程就是从LoggerManager取得一个Logger的过程,其中最重要的操作在Hierarchy类中,这个类说白了就是存储Logger的仓库,其内部使用一个HashMap ht来存储Logger和ProvisionNode。
这里需要解释下ProvisionNode,这个类是一个Vector的实现,之前我们谈到过初始化的一开始,以RootLogger为根节点的级联结构(就是Hierarchy实例),那么这个ProvisionNode想当于这个级联结构中的树节点,比如我在定义了一个名字为a.b.c的Logger,那么总共会生成”a”,”a.b”两个ProvisionNode,以及一个名字为”a.b.c”的Logger。Hierarchy并没有一个链表来维护他们之间的顺序,ProvisionNode会通过其本身就是向量的特性将属于它的Logger进行有序的排列,而Logger本身则通过parent属性记住他们的日志属性可以从哪里继承。
这样做的好处有两点,第一点就是只要名字相同,从LogManager中取出来的Logger就是同一个实例,第二点好处就是级联结构,可以定义出差异化的Logger,特别是其以a.b.c.d类似包结构的拆分日志节点,使得包级别的日志差异化输出更加的容易,并且这种特性提供了日志节点属性继承的功能。
举个例子,我们一般取得一个Logger实例是这样的, Logger log=LogManager.getLogger(Class class), Log4j处理方式为getLogger(class.getName()),这也就是说,这个Logger的名字是带包名的完全限定名字,所以如果我们通过名为a.b.c.aclass,a.b.c.d.bclass这么两个类取得Logger实例,然后定义名为a.b.c和a.b.c.d 2个Logger,那么总共会生成如下的节点
ProvisionNode: a,a.b
Logger: a.b.c(加入a,a.b 2个ProvisionNode中并且parent为RootLogger)
a.b.c.d(加入a,a.b 2个ProvisionNode中,并且parent为a.b.c)
a.b.c.aclass (加入到a,a.b 2个ProvisionNode中,并且parent为a.b.c),
a.b.c.d.bclass(加入到a,a.b 2个ProvisionNode中,并且parent为a.b.c.d)
其中ProvisionNode可能升级为Logger,当同名的Logger加入时,该ProvisionNode中所有以该ProvisionNode为父节点的子节点修改parent指向新的Logger.
final private void updateChildren(ProvisionNode pn, Logger logger) {
final int last = pn.size();
for(int i = 0; i < last; i++) {
Logger l = (Logger) pn.elementAt(i);
//有可能子节点的父节点指向更低一级的节点,比如孙节点。这个应该不难理解。
if(!l.parent.name.startsWith(logger.name)) {
logger.parent = l.parent;
l.parent = logger;
}
}
}
这样Logger a.b.c除了自身的日志输出设置之外,还享受rootLogger的输出(输出级别,Appenders),Logger a.b.c.d同时享受rootLogger和a.b.c的设置,a.b.c.d.bclass享受rootLogger,a.b.c,a.b.c.d的日志输出设定,也就是可以分别定义一批Logger的输出级别和输出形式以及其他属性,当然也有控制开关控制这种继承。下图说明了一些问题。
取得Logger之后,随后就是需要按指定形式输出日志内容,首先需要在LoggerRepository中判定当前Level是否可以做日志输出,包括与全局的thresholdInt进行判定(相当于总开关,这个级别通不过直接返回,不做任何事情),通过后,再与其自身Logger日志级别比较,如果没有设定,查其父节点的日志级别,仍然没有设定,那么查父节点的父节点,直到查到有日志级别设定或者最终到RootLogger(其默认为Debug级别),比较通过后,就可以调用callDependers进行日志输出了。
日志级别为 OFF>FATAL>ERROR>WARN>INFO>DEBUG>ALL,只有输出级别大于等于Logger自身级别才能进行输出,比如 logger.debug,那么只有该Logger的级别(也可能是其先辈节点的日志级别)在DEBUG或者ALL才能允许被输出。全局的thresholdInt如果不设定是保持在ALL级别。
一般Logger会通过AppenderAttachableImpl的实例来维护多个Appender,并且可以共享父节点的Appender List(包括RootLogger里面定义的appenders),所以我们一般在log4j.xml或者log4j.properties里面定义一个某个包下的Logger,然后挂有几个Appender,而程序中通过完全限定的类名(这个类属于前面指定的包)取得Logger,那么当这些Logger输出日志的时候,其本身并没有任何Appender,但是却通过先辈节点定义的Appenders得以输出。这里需要注意的是,如果在直系节点间定义相同的appender,似乎会多次重复输出,因为其会遍历自身以及所有先辈节点的Appender list并且逐一进行doAppend调用,而不会去排重,并且addAppender的时候也没有遍历先辈节点排重。
Appender持有一个Filter链表,doAppend的时候首先走一遍过滤器,结果有3种,Filter.DENY(直接拒绝返回),Filter.ACCEPT(接收输出请求,并且不再走之后的Filter),Filter.NEUTRAL(继续执行下一个Filter),顺利通过Filter链之后,进入真正的输出日志过程,这边以WriteAppender为例,首先将message通过持有的Layout进行格式化(format),然后调用输出流输出日志到目标文件(FileAppender)或者屏幕(ConsoleAppender)。
至此,整个日志输出过程结束。
下面两幅图分别是Log4j的整体类图,不是非常完整,但是大概能够了解到整个结构。
总结下,log4J出来已经很多年了,以前只是使用下,并没有去探究里面机制,但其某些机制还是相当不错的。文中可能出现一些错误,请各位能够指出。