本周为自己的系统加入了日志记录功能。java的日记框架比较多,而且各有特点,选哪个框架来为系统记录日志实在是很难选择,选择的日志框架现在适合我的系统,随着系统的发展,这个框架还能适合吗?到时候要更换更适合的怎么做。真的是很纠结。。。 之前,我通常会自己写一些类来做中间转换,在业务代码中调用自己的日志类,而日志类来调用日记框架完成日志记录(民生银行项目就是这么做的),方案有点拙劣,不过至少在后续需要更换日志实现时,不用去每个业务代码中做修改,在生产环境中改一行代码,走流程可是要花N倍时间的啊,如果所有业务代码改一遍,想要提交到生产环境是完全不可能的。一般情况,日志实现应该是不会变的,可以如果你使用的日志实现存在性能问题,这时候你可能需要考虑更换性能更换的实现了,一切皆有可能嘛,未雨绸缪还是好的,毕竟应用系统要使用挺久的。。。
这次在搭建自己的新系统时,实在不想用之前拙劣的方案了,可以也没有时间去自己实现一套完整解决方案。当然为了找到更好的日志解决方案,而不是眼里只有log4j,所以我求助了互联网。经过查询发现了SLF4J,即简单日志门面(Simple Logging Facade for Java),这不正是我想要实现的东西吗,竟然有现成的,看来看使用指南,简单易用,这就是我想要的,决定了使用它。目前我的系统不需要考虑性能问题,未来用户量非常有限,那就还是使用熟悉的log4j吧。
SLF4J+log4j,下面就来看一下这个方案的具体实现吧,尽量详细,我是个不愿意记忆东西的人,所以写想来下次在建新系统的时候,按照步骤做就好,回忆和重新研究学习一样让人痛苦,尽量少的伤害脑细胞,才能长久保持好状态嘛。。。 好了,废话少说,开始。
首先,在工程中引入SLF4J的相应jar包,2个哦,slf4j-api-1.6.4.jar(api,你懂得),slf4j-log4j12-1.6.4.jar(看名字,你也懂),当然你想要和其他的日志框架组合就是用
slf4j-XXX-1.6.4.jar包,XXX代表你要用的框架名字。好了你可以用SLF4J记录日志了
接着,在代码中使用SLF4J日志啦,具体代码如下:
在类中创建日志实例:
private static final Logger logger = LoggerFactory.getLogger(AbstractXmlTag.class);
开始记日志:
logger.error("标签({}) 即不是叶子节点,也没有子标签",this.getTagName());
记录结果:
标签(head) 即不是叶子节点,也没有子标签
标签(body) 即不是叶子节点,也没有子标签
细心的你应该看到,花括号被后面的参数替换了,没错,这样你在代码中,在也不用使用String+value+String的方法去输出相要的日志了,而且不要纠结这个string+会创建多少个对象的问题,每当这个时候我都会纠结,难受啊,代码洁癖+完美主义 无语啊。。。 纠结啊。。。
继续,其实在上一步做完,是不可能看到输出结果的,因为这步还没做呢,这步想往常一样配置log4j就好了。
引入log4j-1.2.15.jar包,类路径下配置log4j.properties完事了就可以看到结果了。
我一般喜欢看到代码的具体实现,那么SLF4J是怎么做的呢,一句门面我可无法接受,不搞清楚,我就会很纠结的,而且会很容易把它遗忘,纠结男。。。当然搞清楚的好处是很多的。那么SLF4J是如何去关联log4j的呢,跟下代码吧,
好了在调用api中LoggerFactory.getLogger(AbstractXmlTag.class);时,会调用StaticLoggerBinder.getSingleton();,而StaticLoggerBinder类在slf4j-XXX-1.6.4.jar包中,包名org.slf4j.impl.StaticLoggerBinder,所以你引用两个不同的slf4j-XXX-1.6.4.jar包时,可是会类冲突的,他们类名完全一样,所以一定只能引用一个。接下来看看StaticLoggerBinder里面这么实现的吧, loggerFactory = new org.slf4j.impl.Log4jLoggerFactory.Log4jLoggerFactory();,对这里就是这么直白,不再需要委婉了,Log4jLoggerFactory又怎么干的?它就直接调用了log4j的代码了
public Logger getLogger(String name) {
Logger slf4jLogger = null;
// protect against concurrent access of loggerMap
synchronized (this) {
slf4jLogger = (Logger) loggerMap.get(name);
if (slf4jLogger == null) {
org.apache.log4j.Logger log4jLogger;
if(name.equalsIgnoreCase(Logger.ROOT_LOGGER_NAME)) {
log4jLogger = LogManager.getRootLogger();
} else {
log4jLogger = LogManager.getLogger(name);
}
slf4jLogger = new Log4jLoggerAdapter(log4jLogger);
loggerMap.put(name, slf4jLogger);
}
}
return slf4jLogger;
}
那么在运行时,你拿到的日志实例,其实就是log4j的,这就是动态绑定?好吧,勉强算吧 ,不过运行过程中你换一个其他框架试试。。。
很简单啊,不过我的水平总是很难理解为什么代码实现里有那么其它代码,好吧,他应该考虑很全面,我还没到这水平,我只能关心主要的逻辑了
我发现很多程序员完成不懂log4j的配置意思,至少我遇到很多吧,当然我也没真正去完整学习过这些配置都什么意思,完全靠自己悟和零星的学习,那么借这个机会好好学一下吧,上网搜索是最快的方法。
先贴一个我系统的配置,然后再逐条来说明,
log4j.rootCategory=DEBUG, STDOUT, R
# logging console.
log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender
log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout
log4j.appender.STDOUT.layout.ConversionPattern=[EPS]%d %p [%t] %C.%M(%L) | %m%n
# logging file
log4j.appender.R=org.apache.log4j.FileAppender
log4j.appender.R.File=D:\logs\EPS.log
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=[EPS]%d %p [%t] %C.%M(%L) | %m%n
log4j.logger.com.infohold=INFO
log4j.logger.org.springframework=INFO
下面说明大部分搬自网络,懒的写了,而且我觉得这个人写挺好
log4j.rootCategory=DEBUG, STDOUT, R
此句为将等级为INFO的日志信息输出到stdout和R这两个目的地,stdout和R的定义在下面的代码,可以任意起名。等级可分为OFF、 FATAL、ERROR、WARN、INFO、DEBUG、ALL,如果配置OFF则不打出任何信息,如果配置为INFO这样只显示INFO, WARN, ERROR的log信息,而DEBUG信息不会被显示,具体讲解可参照第三部分定义配置文件中的logger。
log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender
log4j.appender.R=org.apache.log4j.FileAppender
此句为定义名为stdout的输出端是哪种类型,可以是
org.apache.log4j.ConsoleAppender(控制台),
org.apache.log4j.FileAppender(文件),
org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件),
org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)
当然你可以自己实现自己的适配器,让日志按你的想法去输出
log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout
此句为定义名为stdout的输出端的layout是哪种类型,可以是
org.apache.log4j.HTMLLayout(以HTML表格形式布局),
org.apache.log4j.PatternLayout(可以灵活地指定布局模式),
org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串),
org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)
通常我们都使用org.apache.log4j.PatternLayout
log4j.appender.STDOUT.layout.ConversionPattern=[EPS]%d %p [%t] %C.%M(%L) | %m%n
通常你配置了自定义布局,那么这条就是定义你想要的布局的
如果使用pattern布局就要指定的打印信息的具体格式ConversionPattern,打印参数如下:
%m 输出代码中指定的消息
%p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL
%r 输出自应用启动到输出该log信息耗费的毫秒数
%c 输出所属的类目,通常就是所在类的全名
%t 输出产生该日志事件的线程名
%n 输出一个回车换行符,Windows平台为“rn”,Unix平台为“n”
%d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921
%l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。
[QC]是log信息的开头,可以为任意字符,一般为项目简称。
我的输出结果:
[EPS]2013-08-18 17:58:43,882 ERROR ["http-bio-8080"-exec-4] com.infohold.becs.message.AbstractXmlTag.getInnerXml(48) | 标签(head) 即不是叶子节点,也没有子标签
[EPS]2013-08-18 17:58:43,883 ERROR ["http-bio-8080"-exec-4] com.infohold.becs.message.AbstractXmlTag.getInnerXml(48) | 标签(body) 即不是叶子节点,也没有子标签
从该日志中我们能看出那些信息呢,这些信息在生产问题定位上很有用的哦
EPS 这个是项目名,这个真没用,出发你多个项目日志输出到一个文件中,
时间:生产问题出现是,我们会关心问题发生的时间,这样你能快速的找到发生问题时间段附近的日志,缩小你搜寻日志的范围,要知道,通常日志文件很大的,没时间可无从找起。
日志级别:这个其实用处也不大
线程id:["http-bio-8080"-exec-4] ,在多线程环境下,通常都是多线程的,多个线程的混在一次,你要根据线程id,按顺序看代码的执行过程。从而定位文档发生的代码区域。因为日志并不总是能清晰记录每一个问题的原因,有些情况,就需要你根据日志来缩小错误发生的代码范围,然后猜测发生问题的代码。
类名+方法名+代码行数:com.infohold.becs.message.AbstractXmlTag.getInnerXml(48) ,很明显的好处,你能非常快速定位到代码与日志的对应
日志信息:这是你自己输出的信息。有没有用,完全取决与你记录了什么信息。如例子中不记录具体哪个标签(如body)出来问题,那它的用处小了很多有没有
好了,写完了