Java日志框架详解
前言
日志的输出对于项目的运行重要性是不言而喻的,一方面日志可以帮助我们在复杂的场景中快速定位到问题,另一方面操作日志还可以协助我们完成对用户行为的收集,帮助我们后续进行分析。现在市面上存在很多日志框架,一开始笔者接触日志框架的时候也经常对各个日志框架的关系和使用产生了混淆。所以打算对日志框架进行一次梳理,以后再使用的时候才可以做到心中有数。
本篇文章将涉及日志框架的功能、发展历程、核心组件和思想等进行讲解,内容较长,也希望各位读者耐心阅读,相信可以有所收获。
一、日志的概念和作用
(一)什么是日志?
在计算机中,日志文件是记录在操作系统或其他软件运行中发生的事件或在通信软件的不同用户之间的消息的文件。记录是保持日志的行为。简单理解的话,日志就是对信息的记录。
(二)日志的作用
日志可以说是每个项目都应该具备的功能模块,其功能主要体现在以下三点:
-
调试
在Java项目调试时,通过查看日志信息我们可以方便地知道当前程序的运行状态是否符合预期。
-
错误定位
项目能正确跑起来并不能让我们高枕无忧,稳定性同样重要。项目在运行一段时候后,可能由于数据问题,网络问题,内存问题等出现异常。这时日志可以帮助开发或者运维人员快速定位错误位置,提出解决方案。要知道,生产环境往往是不允许debug的,我们一般都是通过日志来分析和排查生产环境的问题的。
-
数据分析
大数据的兴起,使得大量的日志分析成为可能,ELK也让日志分析门槛降低了很多。日志中蕴含了大量的用户数据,包括点击行为,兴趣偏好等,用户画像对于公司下一步的战略方向有一定指引作用。
尤其需要注意,合理输出日志在运维过程是相当重要的一件事情,也是让运维人员/开发人员在生产环境中能够快速准确定位到代码问题的关键。所以我们在开发过程中,一定要格外注意在一些必要的位置加上日志输出,方便后续问题的排查。在文章的第七章中,也列举了阿里日志规范,有兴趣的读者可以学习一下。
二、日志的使用和框架介绍
(一)传统日志输出的局限性
在使用日志框架之前,我们使用最多日志输出方式往往是通过System.out
和System.err
等方式来直接在控制台进行输出,在简单的应用调试中,这种做法当然十分便捷也能满足需要,但局限性也相当明显:
-
日志缺乏持久化手段。
System.out
和System.err
等方式都是只能在控制台上输出,但实际部署项目的时候我们不可能一直盯着控制台的输出来观察是否有错误产生,所以这种缺乏持久化的日志输出方式是不符合我们实际的运维需求的。 - 日志输出源、输出方式单一。上述方式只能满足我们简单的控制台输出,实际上我们还希望能对日志进行文件的持久化,数据库的保存或者是通过JMS进行推送等其他操作。
- 日志信息缺少级别分层。没有根据信息的重要程度进行分级,不便于我们日后对日志进行分析。
(二)市面主流日志框架介绍
面对直接控制台输出的局限性,我们需要更为强大的日志工具来辅助我们实现项目运行的日志需要。当前市面上的日志框架众多,我们根据是否为具体实现这个标准,可以大致将主流的日志框架分为以下两类:
- 日志实现(日志实现框架):JUL(java util logging)、logback、log4j、log4j2
- 日志门面(日志标准接口框架):JCL(Jakarta Commons Logging)、slf4j(Simple Logging Facade for Java)
日志门面通过接口(和抽象类)的方式,定义了日志框架应该具备的各种规范,而日志实现则实现了日志门面的规范,是具体日志输出的执行者。现在比较主流的日志组合是SLF4F
+log4j2
。
但是,在早期的时候其实并没有所谓的日志门面这种东西,大家都是直接使用已经开发好的日志实现,直到后来为了更好地统一项目中的日志规范,才有日志门面这个概念,这也就使得我们在一些比较旧的项目中切换日志框架还需要额外加多一些依赖。了解日志框架的变更历史,可以让我们对其有更加深刻的认识(见最后的参考资料:Java日志系统变迁历史)。
三、以log4j为例,聊聊日志框架使用和实现
log4j是最早大规模推广应用的日志框架,其核心的组件思想基本上也都有被后面新的日志框架所参考和借鉴,所以我们对日志框架的学习可以从Log4j开始,只要掌握了Log4j框架中的核心思想,以后接触其他日志框架也都可以快速的上手。
(一)Log4j的核心组件
无论是什么日志框架,我们对日志的输出需求基本就是可定义的日志级别、可定义的输出源、可定义的日志格式。所以Log4j提供的核心组件如下:
Loggers(日志输出的入口类,可以指定日志级别)
Loggers组件在此系统中被分为五个级别:DEBUG、INFO、WARN、ERROR和FATAL。这五个级别是有顺序的,DEBUG<INFO<WARN<ERROR<FATAL,分别用来指定这条日志信息的重要程度
Log4J中有一个特殊的logger叫做“root”,他是所有logger的根,也就意味着其他所有的logger都会直接或者间接地继承自root。rootlogger可以用Logger.getRootLogger()方法获取。JUL是不是也有一个名为.的根。
Appenders(指定数据源)
Appenders用来指定日志输出到哪个地方,可以同时指定日志的输出目的地。常使用的类如下:
- org.apache.log4j.ConsoleAppender(控制台)
- org.apache.log4j.FileAppender(文件)
- org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)
- org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
- org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)
Layouts(指定日志输出格式)
Log4j可以在Appenders的后面附加Layouts来完成这个功能。Layouts提供四种日志输出样式,如根据HTML样式、自由指定样式、包含日志级别与信息的样式和包含日志时间、线程、类别等信息的样式。
常使用的类如下:
- org.apache.log4j.HTMLLayout(以HTML表格形式布局)
- org.apache.log4j.PatternLayout(可以灵活地指定布局模式)
- org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串)
- org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等信息)
(二)使用Log4j框架做一个小案例
步骤一:引入Log4j坐标依赖
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
步骤二:在代码中使用核心组件进行日志输出
@Test
public void normalTest(){
//传入当前类名,获取一个Logger对象,我们将使用这个对象来进行日志输出
Loggerlogger=Logger.getLogger(Log4jTest.class);
//设置logger对象的日志级别,为ALL/OFF表示开启/关闭所有级别的日志
logger.setLevel(Level.ALL);
//定义一个appender数据源,我们这里选择的是控制台
ConsoleAppenderconsoleAppender=newConsoleAppender();
//设置输出格式,具体占位符的含义将在下文给出
consoleAppender.setLayout(newPatternLayout("%d[%t]%m%n"));
//设置一个outputstream流
consoleAppender.setWriter(newPrintWriter(System.out));
//为logger对象设置appender输出源,让logger知道将日志输出到哪里
logger.addAppender(consoleAppender);
//以下为日志输出
logger.error("error");
logger.warn("warn");
logger.info("info");
logger.trace("trace");
logger.debug("debug");
logger.fatal("fatal");
}
步骤三:运行上述代码,结果如下:
我们可以看到,上述的日志都被正常的打印了出来。通过上面的案例,我们已经可以通过编码的方式实现日志输出了,但是当项目中的类逐渐变多,难道我们每次都要在类中手动定义数据源和其他配置吗?这显然是不合理的。针对这种情况,Log4j提供了Logger的继承性来帮助我们解决这个问题。
(三)Logger的继承性
大家可能发现了,我们在上一个小节的案例中,传入了类的全路径名作为入参来获取Logger
对象,这种做法也是实际项目开发中我们经常使用的一种做法。这样做的原因是因为Logger对象具有“继承机制”,例如name为com.log4j.service的logger会继承name为com.log4j的logger身上的配置,也就是说当后者定义好数据源等配置后,前者会自动继承父logger的配置,不再需要自己配置了,这样就可以大大减少我们的代码量。
同时,Log4J中有一个特殊的logger叫做“root”,他是所有logger的根,也就意味着其他所有的logger都会直接或者间接地继承自root。rootlogger可以用Logger.getRootLogger()方法获取。
通过Logger对象的继承性,我们可以来简单用一个案例来演示一下
@Test
public void parentLoggerTest(){
LoggerparentLogger=Logger.getLogger("com.log4j");
parentLogger.addAppender(newConsoleAppender(newPatternLayout("%d[%t]%m%n")));
parentLogger.setLevel(Level.WARN);
LoggersonLogger=Logger.getLogger("com.log4j.sonLogger");
parentLogger.warn("parentLoggerwarn");
parentLogger.info("parentLoggerinfo");
sonLogger.warn("sonLoggerwarn");
sonLogger.info("sonLoggerinfo");
//判断是否真的继承了
System.out.println("sonLogger的父Logger对象"+sonLogger.getParent());
System.out.println("parentLogger对象输出:"+parentLogger);
}
上述代码的输出结果如下:
我们可以看到sonLogger
并没有设置任何数据源和级别,但实际上却自动获得了和parentLogger
一样的数据源和输出级别。同时,从对象的输出结果来看,也更加印证了sonLogger
继承了parentLogger
对象的结果。需要注意的是,如果子类也定义了一套数据源的话,那么此时就会发生叠加的数据源输出,所以一般这种情况下,我们会手动地关闭子类对父类的继承。
有了Logger继承机制,大大简化了我们对日志的编码配置了,但是对于数据源等配置我们都是通过硬编码的方式来进行实现的,一旦要修改就必然要改动代码,重新编译、部署才行。有没有更加方便的方式呢?
答案自然是有的,Log4j允许我们通过配置文件的方式,来配置我们的Logger。具体使用可见下一小节。
(四)通过配置文件来加载日志参数
(1)了解配置文件的组成
我们先来简单看一眼Log4j的源码(见下图),我们可以发现Log4j默认会读取resources目录下的log4j.xml
作为配置文件,如果找不到该配置文件,则读取log4j.properties
作为配置文件。
两种配置文件除了格式略有区别外,其实内容上都是大同小异的。下面我们以log4j.properties
为例,讲一下一些基本的配置:
1、配置根Logger(后续所有Logger对象都会继承根Logger的配置)
log4j.rootLogger=[level],appenderName1,appenderName2
1.level:设定日志记录的最低级别,可设的值有OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL或者自定义的级别,Log4j建议只使用中间四个级别。通过在这里设定级别,您可以控制应用程序中相应级别的日志信息的开关,比如在这里设定了INFO级别,则应用程序中所有DEBUG级别的日志信息将不会被打印出来。
2.appenderName:就是指定日志信息的输出源。可以同时指定多个输出源,多个输出源之间用逗号隔开。
例如:log4j.rootLogger=INFO,A1,B2,C3
2、配置日志信息输出目的地(appender)
log4j.appender.appenderName=className
appenderName:自定义appderName,在log4j.rootLogger设置中使用;
className:可设值如下:
(1)org.apache.log4j.ConsoleAppender(控制台)
(2)org.apache.log4j.FileAppender(文件)
(3)org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)
(4)org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
(5)org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)
3、配置日志信息的输出格式(Layout)
log4j.appender.appenderName.layout=className
className:可设值如下:
(1)org.apache.log4j.HTMLLayout(以HTML表格形式布局)
(2)org.apache.log4j.PatternLayout(可以灵活地指定布局模式)
(3)org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串)
(4)org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)
对于PatternLayout,我们可以通过下面的占位符来格式化日志:
%p:输出日志信息的优先级,即DEBUG,INFO,WARN,ERROR,FATAL。
%d:输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,
如:%d{yyyy/MM/ddHH: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(嵌套诊断环境),尤其用到像javaservlets这样的多客户多线程的应用中。
%%:输出一个"%"字符。
(2)使用配置文件的方式实现一个小案例
根据上面的讲解,我们可以来做一个简单的案例:
步骤一:在resources目录下配置log.properties
文件
#指定日志的输出级别与输出端
log4j.rootLogger=INFO,Console,sizeRollFile
#自定义Appender,控制台输出配置
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d[%t]%-5p[%c]-%m%n
步骤二:在代码中输出日志:
@Test
public void simpleLog(){
Loggerlogger=Logger.getLogger(Log4jTest.class);
logger.fatal("fatal");
logger.error("error");
logger.warn("warn");
logger.info("info");
logger.debug("debug");
logger.trace("trace");
}
步骤三:运行代码,观察输出
我们可以看到,即使我们的Logger对象没有在代码中进行任何配置,也已经可以根据需要来定义输出了。上面的代码,其实已经很接近于我们实际项目的应用了:在类中获取Logger对象,使用logger对象进行输出,logger对象的配置统一抽离到配置文件进行配置。
正因为Log4j提供了便捷而强大的日志功能,所以当年Log4j一面世就受到广泛欢迎和使用,但随着时间的推移,
JUL
(JDK提供的日志实现)、JCL
(Apache提供的日志门面)和SLF4J
等规范和实现的推出,Log4j也逐渐退出了历史的舞台,但是其核心思想仍然被其他框架所吸收。
四、什么是日志门面
当我们的系统变的复杂的之后,难免会集成其他的系统,不同的系统之间可能会使用不同的日志实现。也就是说一个系统中可能出现多个日志实现,这样并不利于项目的日志管理,而且随着时间的发展,可能会出现新的效率更高的日志系统,如果我们想切换代价会非常的大。如果我们的日志系统能和jdbc一样,有一套自己的规范,其他实现均按照规范去实现,就能很灵活的使用日志框架了。
日志门面就是为了解决这个问题而出现的一种技术,日志门面是规范,其他的实现按照规范实现各自的日志框架即可,我们程序员基于日志门面编程即可。
- 常见的日志门面:JCL、SLF4J
- 常见的日志实现:JUL、log4j、logback、log4j2
上述日志框架出现的历史顺序: log4j -->JUL-->JCL--> slf4j --> logback --> log4j2
五、SLF4J日志门面介绍
我们从第四章可以知道,当前使用比较多的日志门面是
JCL
和SLF4J
,其中SLF4J
是当前更加主流的日志门面选择,所以我们第五章主要来介绍SLF4J
的使用和核心思想。
简单日志门面(SimpleLoggingFacadeForJava)SLF4J主要是为了给Java日志访问提供一套标准、规范的API框架,其主要意义在于提供接口,具体的实现可以交由其他日志框架,例如log4j和logback等。当然slf4j自己也提供了功能较为简单的实现,但是一般很少用到。对于一般的Java项目而言,日志框架会选择slf4j-api作为门面,配上具体的实现框架(log4j、logback等)。
SLF4J官方网站:https://www.slf4j.org/
(一)使用SLF4J来做一个小案例
步骤一:引入坐标
<!--slf4j门面依赖-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.27</version>
</dependency>
<!--slf4j自带的简单日志实现-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.27</version>
</dependency>
步骤二:编写代码进行测试
@Test
public void normalTest(){
//和log4j类似,传入类名获取logger对象
Loggerlogger=LoggerFactory.getLogger(Slf4jTest.class);
//slf4j默认只有五个等级,调用logger对象进行各个等级的输出
logger.error("error");
logger.warn("warn");
logger.info("info");
logger.debug("debug");
logger.trace("trace");
//使用占位符来进行日志输出
logger.warn("{}今年{}岁了","小明",15);
//slf4j也支持我们将异常作为参数传入
logger.error("出现异常:",newNullPointerException());
}
步骤三:运行代码,观察结果
我们可以看到,我们的代码可以成功输出日志,且SLF4J
默认的日志级别为INFO
(二)SLF4J绑定其他日志实现
SLF4J的出现时间其实是相对较晚的,所以即使SLF4J
提出了很好的接口规范,但问题在于以前的日志实现并没有实现后面才提出来的日志规范,在这种情况下,SLF4J
使用适配器模式的设计思想来解决了这个问题。我们可以来简单了解一下SLF4J的解决方案。
(1)使用适配器模式解决接口和实现不对应的问题(了解)
假如存在LogImpl类,类中方法如下:
class LogImpl{
public void mylog(){
System.out.println("logImplislogging...");
}
}
现在存在一个接口Logable
interface Logable{
void log();
}
我们希望能够使用Logable
接口的方法调用LogImpl
方法可以怎么办呢?其实很简单,引入一个适配器即可:
class LogAdaptor extends LogImpl implements Logable{
public void log(){
mylog();
}
}
通过上面这种方式,我们就可以实现在我们的接口中调用其他方法实现了,SLF4J
正是使用的这种方法来解决原先的日志框架没有实现其接口规范的问题。
(2)使用SLF4J的绑定来做一个小案例
步骤一:引入坐标依赖
<!--slf4j门面依赖-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.27</version>
</dependency>
<!--log4j适配log4j的依赖包-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.27</version>
</dependency>
<!--log4j具体实现-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
步骤二:在resources目录下配置log4j.properties
文件
#指定日志的输出级别与输出端
log4j.rootLogger=WARN,Console
#控制台输出配置
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d[%t]%-5p[%c]-%m%n
步骤三:编写测试代码
@Test
public void normalTest(){
Loggerlogger=LoggerFactory.getLogger(Slf4jTest.class);
logger.error("error");
logger.warn("warn");
logger.info("info");
logger.debug("debug");
logger.trace("trace");
//使用占位符来进行日志输出
logger.warn("{}今年{}岁了","小明",15);
//slf4j也支持我们将异常作为参数传入
logger.error("出现异常:",newNullPointerException());
}
步骤四:运行代码,观察结果
我们可以看到,此时的日志输出已经根据我们的配置文件定义,将输出的级别改为了WARN
,即使我们现在使用的是log4j的配置文件,slf4j的接口方法!
PS:实际上,上面案例中的适配器还比较简单,SLF4J还提供适配JCL
接口,再由JCL接口调用具体实现的适配器。Anyway,了解思想就行了...
(三)使用桥接来维护以前的日志框架
我们在开发过程中可能会遇到这样一种情况,依赖的某些组件使用了SLF4J以外的日志记录API,且这些组件在不久的将来也不会切换到SLF4J。为了解决这种情况,SLF4J附带了几个桥接模块,这些模块将对log4j,JCL和java.util.loggingAPI的调用重定向,就好像它们是对SLF4J的API一样。
简单来说就是,对于项目的依赖包使用到了其他日志框架这种情况,很明显我们在没有源码的情况下无法手动将他们的日志框架进行切换,所以SLF4J
使用了桥接的设计方式来解决这种问题,我们可以来简单了解一下SLF4J的解决方案。
(1)使用桥接模式解决旧接口和新接口不一致的问题
假如现在A项目中存在类LogImpl(注意包名)
package com.bridge;
public class LogImpl{
public void mylog(){
System.out.println("logImplislogging...");
}
}
且该方法被B项目中的service类正常调用了
package com.service;
import com.bridge.LogImpl;
public class UserService{
public void login(){
newLogImpl().mylog();
//省略具体方法
}
}
很明显,对于上面这种代码,输出结果会是:logImplislogging...
如果我们在不更改B项目service方法的基础上,想要将上述输出改成C项目中MyLog
实现的输出,可以怎么办呢?
package com.slf4j;
public interface Logable{
void log();
}
class MyLog implementsLogable{
public void log(){
System.out.println("MyLogislogging...");
}
}
答案是可以使用桥接来解决这个问题。我们在C项目中同样创建com.bridge
目录,然后在该目录下创建一个同名的LogImpl
类,我们在这个新建的类中再调用C项目中的MyLog
类方法。
package com.bridge;
public class LogImpl{
public void mylog(){
Logablelogger=newMyLog();
logger.log();
}
}
这样移除掉A项目的依赖后,B项目不需要改动任何代码,即对于B项目而言我们的改动对它是无感知的,我们还是可以用原来的api,但实际上调用的方法早已不是一开始A项目中的方法了。
(2)使用SLF4J的桥接包来做一个小案例
步骤一:引入依赖
<!--slf4j提供log4j的桥接包-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.27</version>
</dependency>
<!--slf4j的门面依赖-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.27</version>
</dependency>
<!--slf4j接口的实现包-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.27</version>
</dependency>
步骤二:编写测试代码
需要注意的是,我们的代码使用的是Log4j的代码,但实际上我们并没有引入任何Log4j的依赖。
import org.apache.log4j.Logger;
import org.junit.Test;
public class Slf4jBridgeTest{
@Test
public void bridgeTest(){
Loggerlogger=Logger.getLogger(Slf4jBridgeTest.class);
logger.error("error");
logger.warn("warn");
logger.info("info");
logger.trace("trace");
logger.debug("debug");
logger.fatal("fatal");
}
}
步骤三:运行代码,观察结果
我们可以看到,代码可以正常运行,且底层的日志实现是我们所引入的slf4j-simple
依赖。
(四)使用日志门面的优势
- 面向接口编程。无论底层实现怎么改变,我们都不需要更改任何项目中的日志代码,比如项目一开始是使用log4j作为日志实现,处于性能考虑想要将项目中的日志框架改为log4j2或者logback,那么此时我们只需要改一下pom文件的依赖就行,其他代码完全不需要改变。
- 对于历史代码有较好的适配依赖进行处理。
(五)使用SLF4J的注意事项
- SLF4J不依赖于任何特殊的类装载。实际上,每个SLF4J绑定在编译时都是硬连线的,使用一个且只有一个特定的日志记录框架。
- 同对象的桥接包不可以和绑定包一起引入(例如jcl-over-slf4j.jar和slf4j-jcl.jar不能同时部署),否则会出现循环依赖
- 所有的桥接都只对Logger日志记录器对象有效,如果程序中调用了内部的配置类或者是Appender,Filter等对象,将无法产生效果。
六、Log4j2介绍
(一)什么是Log4j2
我们在上一小节中提到了Log4j2这个日志框架,这里来对这个框架做一个简单的介绍:
ApacheLog4j2是对Log4j的升级版,参考了logback的一些优秀的设计,并且修复了一些问题,因此带来了一些重大的提升,主要有:
异常处理,在logback中,Appender中的异常不会被应用感知到,但是在log4j2中,提供了一些异常处理机制。
性能提升,log4j2相较于log4j和logback都具有很明显的性能提升,后面会有官方测试的数据。
自动重载配置,参考了logback的设计,当然会提供自动刷新参数配置,最实用的就是我们在生产上可以动态的修改日志的级别而不需要重启应用。
需要注意的是,Log4j2其实也是日志门面,而且也提供了性能优越的日志实现,但因为目前市面上SLF4J
已经成为了市面的主流门面,所以大家一般还是将Log4j2作为日志实现来用。
官网:https://logging.apache.org/log4j/2.x/
(三)代码使用
我们简单来用代码进行演示一下:
没错,这里的代码和之前演示SLF4J的代码基本一样,因为我们使用了SLF4J来做日志门面的话,那么基本上我们的API是不需要进行任何改变的,面向接口编程就是这么好!
@Test
public void normalTest() {
Logger logger = LoggerFactory.getLogger(Slf4jTest.class);
logger.error("error");
logger.warn("warn");
logger.info("info");
logger.debug("debug");
logger.trace("trace");
// 使用占位符来进行日志输出
logger.warn("{}今年{}岁了", "小明", 15);
// slf4j也支持我们将异常作为参数传入
logger.error("出现异常:", new NullPointerException());
}
@SLF4J注解的应用
我们会发现,我们在每个需要打印日志的类中,都需要传入当前类来获取Logger对象,这样显得代码十分的繁琐。我们可以引入Lombok
依赖来简化我们的代码。Lombok
提供了@Slf4j
注解来帮我们省略获取Logger对象的代码。
所以上面的代码可以简化为下面这样:
使用@Slf4j
注解之后,直接用log对象就行
@Slf4j
public class Slf4jTest {
@Test
public void normalTest() {
Logger logger = LoggerFactory.getLogger(Slf4jTest.class);
logger.error("error");
logger.warn("warn");
logger.info("info");
logger.debug("debug");
logger.trace("trace");
// 使用占位符来进行日志输出
logger.warn("{}今年{}岁了", "小明", 15);
// slf4j也支持我们将异常作为参数传入
logger.error("出现异常:", new NullPointerException());
}
(四)了解log4j2配置文件
(1)了解log4j2.xml配置文件和基本的标签含义
我们以下面的配置文件为例来做一个简单的标签介绍
<Configuration status="info"
monitorInterval="30">
<Appenders>
<RollingFile name="fileDefaultLog" fileName="logs/dailyLog.log" filePattern="logs/dailyLog.log.%i">
<PatternLayout pattern="%d %-5p - %X{requestId} - {%c{2}} - %m%n"/>
<Policies>
<SizeBasedTriggeringPolicy size="25MB"/>
</Policies>
<DefaultRolloverStrategy max="10"/>
</RollingFile>
<RollingFile name="filePerfLog" fileName="logs/dailyLog_perf.log" filePattern="logs/dailyLog_perf.log.%i">
<PatternLayout pattern="%d %c [%X{requestId}] %m%n"/>
<Policies>
<SizeBasedTriggeringPolicy size="25MB"/>
</Policies>
<DefaultRolloverStrategy max="10"/>
</RollingFile>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="%d %-5p - %X{requestId} - {%c{1}} - %X{indent}%m%n"/>
</Console>
</Appenders>
<Loggers>
<!-- Performance, Step and Request logger -->
<Logger name="perf" level="warn" additivity="false">
<AppenderRef ref="filePerfLog"/>
</Logger>
...
<!-- root logger -->
<Root level="info">
<AppenderRef ref="fileDefaultLog"/>
<AppenderRef ref="console"/>
</Root>
</Loggers>
</Configuration>
-
最外层的Configuration标签,标签内可以有多个
Appenders
和Loggers
子节点- error属性表示log4j默认的打印级别(会被Logger的级别覆盖)
- monitorInterval属性表示log4j自动重新配置的监测间隔时间,单位是s,最小是5s。(是的,<font color="red" >log4j2可以不用重启应用,动态读取日志配置文件!</font>)
-
Appenders标签是
Configuration
标签的子标签,我们可以在里面配置多个输出源,常见的有三种子节点:Console、RollingFile、File。-
Console节点用来定义输出到控制台的Appender.
- name:指定Appender的名字.
- target:
SYSTEM_OUT
或SYSTEM_ERR
,一般只设置默认:SYSTEM_OUT
. - PatternLayout:输出格式,不设置默认为:%m%n.
-
File节点用来定义输出到指定位置的文件的Appender.
- name:指定Appender的名字.
- fileName:指定输出日志的目的文件带全路径的文件名.
- PatternLayout:输出格式,不设置默认为:%m%n.
-
RollingFile节点用来定义超过指定大小自动删除旧的创建新的的Appender.
- filePattern:指定新建日志文件的名称格式.
- Policies:指定滚动日志的策略,就是什么时候进行新建日志文件输出日志.
- TimeBasedTriggeringPolicy:Policies子节点,基于时间的滚动策略。(interval属性用来指定多久滚动一次)
- SizeBasedTriggeringPolicy:Policies子节点,基于指定文件大小的滚动策略。(size属性用来定义每个日志文件的大小)
- DefaultRolloverStrategy:用来指定同一个文件夹下最多有几个日志文件时开始删除最旧的,创建新的。(max属性指定数量)
-
Console节点用来定义输出到控制台的Appender.
-
Loggers标签也是
Configuration
标签的子标签,常见的有两种:Root和Logger。-
Root节点用来指定项目的根日志,如果没有单独指定Logger,那么就会默认使用该Root日志输出
- level:日志输出级别,共有8个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF.
- AppenderRef:Root的子节点,用来指定该日志输出到哪个Appender.
-
Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。
- level:日志输出级别,共有8个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF.
- name:用来指定该Logger所适用的类或者类所在的包全路径,继承自Root节点.
- AppenderRef:Logger的子节点,用来指定该日志输出到哪个Appender,如果没有指定,就会默认继承自Root。如果指定了,那么会在指定的这个Appender和Root的Appender中都会输出,此时我们可以设置Logger的additivity="false"只在自定义的Appender中进行输出。
-
Root节点用来指定项目的根日志,如果没有单独指定Logger,那么就会默认使用该Root日志输出
通过上面的标签介绍,我们现在就可以很轻松地读懂上面的配置文件了:我们在文件中定义了两个Appender数据源,并把它们分配给了根Logger,也就是说以后所有级别在INFO以上日志都会在控制台和日志文本中进行输出,同时我们还单独配置了name=perf
的Logger并为其指定了Appender
的数据源,所以所有包前缀(严格来说应该是逻辑包前缀)为pref
下的类都会将日志滚动输出到dailyLog_perf.log文件中。由于该Logger默认继承了父Logger(Logger具有继承性),为了避免出现重复的日志输出,所以我们使用additivity="false"
来让这个Logger只在自定义的Appender中进行输出。
七、阿里日志规约(了解)
应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架SLF4J中的API。使用门面模式的日志框架,有利于维护和各个类的日志处理方法统一。
日志文件推荐至少保存15天,因为有些异常具备以“周”为频次发生的特点。
应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:appName_logType_logName.log。logType为日志类型,推荐分类有stats/monitor/visit等;
logName为日志描述。这种命名的好处:通过文件名就可以知道日志文件属于哪个应用,哪种类型,有什么目的,这也有利于归类查找。
对trace/debug/info级别的日志输出,必须使用条件输出形式或者占位符的方式。
避免重复打印日志,否则会浪费磁盘空间。务必在日志配置文件中设置additivity=false。
异常信息应该包括两类:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字向上抛出。
谨慎地记录日志。生产环境禁止输出debug日志;有选择地输出info日志;如果使用warn记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免吧服务器磁盘撑爆,并及时删除这些观察日志。
可以使用warn日志级别记录用户输入参数错误的情况,避免当用户投诉时无所适从。
八、总结
本篇文章基于日志框架的作用和使用方式进行了介绍,选择了最早出现的日志框架Log4j来分析日志框架主要的核心组件:(Logger对象、Appender输出源、Layouts格式化器)和核心思想(Logger对象的继承性和配置文件的抽离),由于各个系统/依赖之间可能引用了不同的日志框架,为了达到统一的效果,我们又介绍了当前最为主流的SLF4J门面框架,除了演示该框架的基本API之外,我们还简要介绍了SLF4J绑定日志实现以及桥接其他日志框架的实现原理。然后我们对当前主流的日志实现Log4j2的使用和配置进行介绍。在掌握了上述内容后,文章的最后分享了阿里日志规范。
个人感觉日志的重要性在越大的项目中就越是明显,因为复杂的项目往往除了面临的(运维层面)不稳定因素更多之外,代码逻辑的复杂度也相对较高,一旦没有合理的日志来及时进行反馈,将会大大的提升问题的排查成本。所以日志本身和代码的注释一样,是十分值得重视的。同时由于有日志框架的存在,实际上是大大降低了我们日志收集的成本的,在日常开发中我们只要掌握常用的配置项,可以读懂日志配置的含义即可,对于底层实现原理可以无需过多涉及。
参考文档:
log4j详情配置可参考:https://www.cnblogs.com/zhangguangxiang/p/12007924.html
log4j2详情配置可参考:https://www.cnblogs.com/hafiz/p/6170702.html
Java日志系统变迁历史:https://segmentfault.com/a/1190000021121882
SLF4J官方地址:http://www.slf4j.org/index.html
Log4j2官方地址:https://logging.apache.org/log4j/2.x/