Log4j是Apache(Apache软件基金会)下的一款开源的日志框架,能够满足我们在项目中对于日志记录的需求。一般来讲,在项目中,我们会结合slf4j和log4j一起使用。Log4j提供了简单的API调用,强大的日志格式定义以及灵活的扩展性。我们可以自己定义Appender来满足我们对于日志输出的需求。
什么是日志框架
我们在系统中对于记录日志的需求并不单纯。首先,我们希望日志要能持久化到磁盘上,最基本的就是要能够保存到文件中;其次,我们希望在开发和生产环境中记录的日志并不相同,明显开发环境的日志记录会更多方便调试,但放到生产环境下大量的日志很容易会撑爆服务器,因此在生产环境我们希望只记录重要信息。
基于不单纯的目的,System.out.println是直接不能满足我们的要求,因此抛弃它,选择功能更强的日志框架。而log4j是apache下一款著名的开源日志框架,log4j满足上面的一切需求。
记录日志的框架并不仅仅只有log4j,比较有名的还有logback等,现在比较火的SpringBoot默认集成的日志就是logback。不管哪种日志框架,一般都能够实现日志的持久化功能。
集成slf4j-log4j到系统
slf4j-log4j的使用也是非常简单的,几个jar包,一个配置文件(log4j.properties)就可以了。把jar包和配置文件都放到classpath下就可以了。那么配置文件都写些什么,一会再说,这是一个重点。
如果使用Maven来管理就更方便了,只需要导入如下依赖即可
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
log4j的日志级别
早就听说有日志级别这么回事,日志级别到底是个什么鬼?刚才提到了两个需求,一个是保存日志到不同的地方,另一个是开发和生产环境打印不同的日志,那么如何在不同的环境打印不同的日志,就是由日志级别来实现的。
日志一般分为5个等级,从低到高分别是 DEBUG INFO WARN ERROR FATAL。对应中文的意思就是 调试信息 一般信息 警告信息 错误信息 严重错误信息。在记录日志的时候,就可以根据不同的日志级别来进行记录。比如,你要监控一下用户提交上来的注册信息,我们认为这是在开发环境才需要的,因此可以使用DEBUG级别记录。再比如一个支付场景,系统抛出了一个未知的异常,这肯定是一个非常严重的问题了,那么可以用FATAL或者ERROR来记录。
了解清楚等级划分之后,再来控制生产环境和开发环境输出的信息不同就很简单了,我可以配置一个最低的日志输出级别,在开发环境我设置为DEBUG,也就是所有的日志信息都能输出。在生产环境我可以设置为WARN,也就是只有WARN及以上级别的日志才会被输出,这样是不是就可以在不同的环境控制不同的日志输出了。
对于开发者,学习如何记录各等级的日志非常简单,日志框架一般都提供了非常人性化的API。比如记录debug信息就是 logger.debug(String msg) 记录 info信息就是 logger.info(String msg) 等等,logger是实例化的日志对象。复杂的在于什么场景下使用哪一个级别的日志信息,关于这个问题,在阿里的《java开发手册》的日志规约部分写的非常好,有兴趣的朋友可以参考一下。
log4j的appender
Appender是log4j的另一大优势,解决的就是日志输出目的地的问题。我们可以自己实现Appender来决定让日志输出到哪里。当然框架默认给我们已经提供了一些常用的Appender,比如输出到控制台的org.apache.log4j.ConsoleAppender,输出到文件的org.apache.log4j.DailyRollingFileAppender等。系统已经默认给我们提供了大量的Appender,基本上可以满足我们的日常需求,当然如果你的需求比较特殊,可以自己实现Appender,也是非常简单的。只需要定义一个类,实现Appender接口就可以了。Appender接口中定义了一系列记录日志的方法,按照自己的规则实现这些方法即可。系统提供的Appender如下
log4j的配置
了解了log4j的两大核心,一个是日志级别,一个是Appender 之后,我们开始看看配置文件到底是什么样子的,下面一个截图,介绍了关于配置的大部分常用内容
注意,这里有一个日志的格式,也就是 ConversionPattern,那么这个值到底该如何配置呢?
%p:输出日志信息的优先级,即DEBUG,INFO,WARN,ERROR,FATAL。
%d:输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,如:%d{yyyy/MM/dd HH:mm:ss,SSS}。
%r:输出自应用程序启动到输出该log信息耗费的毫秒数。
%t:输出产生该日志事件的线程名。
%l:输出日志事件的发生位置,相当于%c.%M(%F:%L)的组合,包括类全名、方法、文件名以及在代码中的行数。例如:test.TestLog4j.main(TestLog4j.java:10)。
%c:输出日志信息所属的类目,通常就是所在类的全名。
%M:输出产生日志信息的方法名。
%F:输出日志消息产生时所在的文件名称。
%L::输出代码中的行号。
%m::输出代码中指定的具体日志信息。
%n:输出一个回车换行符,Windows平台为"/r/n",Unix平台为"/n"。
%x:输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像java servlets这样的多客户多线程的应用中。
%%:输出一个"%"字符。
另外,还可以在%与格式字符之间加上修饰符来控制其最小长度、最大长度、和文本的对齐方式。如:
%20c:指定输出category的名称,最小的长度是20,如果category的名称长度小于20的话,默认的情况下右对齐。
%-20c:"-"号表示左对齐。
%.30c:指定输出category的名称,最大的长度是30,如果category的名称长度大于30的话,就会将左边多出的字符截掉,但小于30的话也不会补空格。
到这里,log4j的配置就完成了。
一个简单的log4j配置文件的案例如下
### 设置###
log4j.rootLogger=DEBUG,stdout,E
log4j.logger.io.netty=WARN
log4j.logger.com.mchange=WARN
### 输出信息到控制抬 ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%p %d{HH:mm:ss} [%c:%L] %m%n
### 输出DEBUG 级别以上的日志到=E://logs/error.log ###
log4j.appender.D=org.apache.log4j.DailyRollingFileAppender
log4j.appender.D.File=E://logs/log.log
log4j.appender.D.Append=true
log4j.appender.D.Threshold=DEBUG
log4j.appender.D.layout=org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
### 输出ERROR 级别以上的日志到=E://logs/error.log ###
log4j.appender.E=org.apache.log4j.DailyRollingFileAppender
log4j.appender.E.File=E://logs/error.log
log4j.appender.E.Append=true
log4j.appender.E.Threshold=ERROR
log4j.appender.E.layout=org.apache.log4j.PatternLayout
log4j.appender.E.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
slf4j日志框架
Slf4j是一款日志记录框架,我们的项目中可能会用到很多的日志,比如commons-logging,又或者是log4j等等,那么不同的日志工具提供了不同的日志接口,我们的项目如果想换一个日志框架,可能需要改动许多的代码,这样slf4j就给我们提供了一种便捷的方式。它提供了一种对日志工具更高一层的抽象,只要导入slf4j的包和slf4j和采用日志工具整合的包,我们就可以使用slf4j来记录日志了,不用去考虑具体的日志工具的接口。那么以后如果要改变日志工具,我们就可以直接将要使用日志工具加到我们的项目中,而不需要改动代码。
日志跟踪(MDC)
高并发环境下,日志输出很容易出现找不到的情况,在传统阻塞模型下,针对每次请求都是一个线程,因此在输出格式的时候,输出线程名就可以实现了,那么跟日志的时候,就可以根据线程的名字进行跟踪。
当然这种跟踪方式有很多的弊端,首先线程是在线程池中的,来来回回也就那么几个线程,所有如果并发量很大,这种方式并不合适。另外对于多机环境,使用线程名的方式也并不靠谱。
为了解决线程名方式带来的问题,可以在当前线程中放置一个变量的形式,而这个变量的值可以是订单号,也可以随机。在日志输出的时候,将这个值输出出来,就可以保证每个线程的每次请求值是相同的。根据这个值就可以跟踪日志了。
这个值的存储可以使用ThreadLocal来存储,但实现还是比较麻烦,而日志又都有这个需求,因此日志框架就给我们提供了这个便捷的操作。MDC!
可以直接通过MDC.set(String key, String value)来设置。比如设置一个uid,值为UUID随机生成。就可以写成MDC.set(“uid”,UUID.randomUUID().toString());
在日志的配置文件中,加上 %uid 就可以输出这个值了。