1、基本认识
logback官方文档:http://logback.qos.ch
具体样例:https://www.baeldung.com/logback
从下面依赖关系图可以看见,Springboot的核心启动器spring-boot-stater依赖了spring-boot-starter-looging,而这个就是日志的启动器。
Springboot项目整合了日志框架,它默认使用Slf4j作为日志门面,默认的日志实现使用Logback。用JAVA来理解的话,Slf4j就是接口,而logback就是它的具体实现类。
如果你使用的日志实现不是logback。比如,使用的日志实现是log4j2,在Springboot启动的时候,也会自动通过桥接包《log4j-to-slf4j》,将log4j2切换到slf4j的门面,然后使用logback输出日志到控制台。
2、实际演示
pom文件中引入SpringBoot的Web启动依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.3.7.RELEASE</version> </dependency>
需要注意,需要导入的Logger类是在【org.slf4j】下,不要错误。
①日志工具的logger对象应当用private static final修饰,private保证不会被其他类调用,static final保证在类加载时就被初始化,并且其值不会改变,减少内存消耗。
②打印日志时,尽量不要使用"+"进行拼接,这会额外消耗内存,尽量使用{}占位符。
java
package com.travel.logback.study.web;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
@RestController
public class DemoController {
private static final Logger logger = LoggerFactory.getLogger(DemoController.class);
//该注解的作用是让Springboot启动时,自动执行该方法
@PostConstruct
public void testLogback(){
logger.trace("---trace 级别的日志 ---");
logger.debug("---debug 级别的日志 ---");
logger.info("---info 级别的日志 ---");
logger.warn("---warn 级别的日志 ---");
logger.error("---error 级别的日志 ---");
}
}
启动项目,控制台打印效果如下:
可以发现一个问题,trace级别 和 debug级别的日志并没有出现在控制台。这是因为Springboot启动时,控制台打印的日志级别默认是info级别,低于info级别的日志,就不会打印在控制台上。如果需要打印低级别的日志,就需要修改配置文件内容,指定控制台打印的日志级别。
logback的日志级别
从低到高:TRACE < DEBUG < INFO < WARN < ERROR < FATAL
可以在yml文件中设置日志级别
java
logging:
level:
root: debug
3、配置文件的学习
由于springboot默认使用logback作为日志实现,因此可以在resources目录下直接创建一个【logback.xml】配置文件进行一些个性化的配置。
3.1 完整配置文件内容 和 展示效果
完整配置文件
XML
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- property指定日志输出格式,他有两个属性:
name定义property节点的名称,value属性设置具体的日志输出格式
property节点定义之后,下面的节点可以直接使用"${}"来引用value中定义的日志输出格式
-->
<!--
日志输出格式:
%-5level %level表示日志级别,-5表示占5个字符,如果不足,就向左对齐
%d{yyyy-mm-dd H:mm:ss.sss} %d表示日期,后面是日期的格式
%c 表示 类的完整名称
%M 表示 method
%L 表示 行号
%thread 表示 线程名称
%m或者%msg 表示 信息
%X{key} %X表示输出MDC中特定键的值,key为具体的键名称,值不存在,则不会输出
%logger{36} 表示 使用哪个日志记录器,就会打印那个日志记录器的name,最多显示36个字符
%n 表示 换行
被[]中括号括起来,只是为了方便区分,也可以将中括号去掉,不会有影响
-->
<!--%X{key} %X表示输出MDC中特定键的值,key为具体的键名称,值不存在,则不会输出-->
<property name="NEW_LOG_STYLE"
value="[%-5level] [%thread] [%logger] [%X{traceId-UMR}] [%d{yyyy-mm-dd H:mm:ss.sss}] [%c] [%M] [%L] %m%n"/>
<!--定义日志文件的保存路径-->
<property name="BASE_LOG_HOME" value="/opt/applog"/>
<property name="MAX_FILE_SIZE" value="1MB"/>
<property name="MAX_HISTORY" value="3"/>
<!--项目的名称-->
<property name="SERVICE_NAME" value="ACCOUNT_MANAGE_SYSTEM"/>
<!-- 控制台输出设置 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--encoder指定日志格式,class属性可以不写,默认会将值映射到PatternLayoutEncoder的变量中-->
<!-- <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> -->
<encoder>
<!--使用上文定义的,全局的property配置-->
<pattern>${NEW_LOG_STYLE}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!--定义一个日志文件输出的appender,RollingFileAppender 可以实现日志拆分、和归档压缩 -->
<appender name="LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--file标签指定日志文件保存路径-->
<file>${BASE_LOG_HOME}/${SERVICE_NAME}_rollback.log</file>
<!--滚动策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件的最大限制 -->
<maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
<!-- 日志文件保留时间,默认以天为单位 -->
<maxHistory>${MAX_HISTORY}</maxHistory>
<!-- 日志每超过一次最大限制,则按时间压缩一次,%i是一个计数,从0开始,往上递增-->
<fileNamePattern>${BASE_LOG_HOME}/${SERVICE_NAME}/%d{yyyy-MM-dd}.log%i.log.zip</fileNamePattern>
</rollingPolicy>
<encoder>
<!--指定log文件中的日志格式-->
<pattern>${NEW_LOG_STYLE}</pattern>
</encoder>
</appender>
<!--日志记录器,additivity默认也是true,如果为true,则使用父节点的appender进行输出-->
<logger name="com.travel.logback.study.web" level="INFO" additivity="true" />
<logger name="Wechat-UMR" level="debug" additivity="false">
<appender-ref ref="CONSOLE"/>
</logger>
<!--调整某一个类的日志输出级别,name写入类的全限定名称,level设置日志级别-->
<logger name="org.hibernate.XXXX" level="ERROR" additivity="false">
<appender-ref ref="CONSOLE"/>
</logger>
<root level="info">
<!--匹配到当前日志记录器之后,会执行下面两个appender标签-->
<appender-ref ref="LOG_FILE"/>
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
3.2 各节点介绍
<configuration>根节点
包含的属性如下所示:
scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
<property> 标签
property定义全局的变量,他有两个属性:
name属性定义property节点的名称,
value属性设置具体的日志输出格式
property标签定义之后,能够方便其他标签通过“${name}”来获取value中的值
value可配置的日志输出格式:
%-5level %level表示日志级别,-5表示占5个字符,如果不足,就向左对齐
%d{yyyy-mm-dd H:mm:ss.sss} %d表示日期,后面是日期的格式
%c 表示 类的完整名称
%M 表示 method
%L 表示 行号
%thread 表示 线程名称
%m或者%msg 表示 信息
%X{key} %X表示输出MDC中特定键的值,key为具体的键名称,值不存在,则不会输出
%logger{36} 表示使用哪个日志记录器,就会打印那个日志记录器的name,最多显示36个字符
%n 表示 换行
XML
<property name="UMR_LOG_STYLE" value="[%-5level] [%thread] [%d{yyyy-mm-dd H:mm:ss.sss}] [%c] [%M] [%L] %m%n"/>
用 " 中括号[ ] " 括起来,只是为了方便查看,可以将中括号去掉,不会有任何影响
<appender>标签
appender用来指定日志输出方式,有两个属性name和class。由class决定使用哪种输出策略,常用就是控制台输出策略 和文件输出策略 。ConsoleAppender 是将日志输出到控制台。FileAppender 是将日志输出到一个文件中。
使用控制台输出策略
XML
<property name="UMR_LOG_STYLE" value="[%-5level] [%thread] [%d{yyyy-mm-dd H:mm:ss.sss}] [%c] [%M] [%L] %m%n"/>
<!-- 控制台输出设置 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--encoder标签指定日志的格式-->
<encoder>
<!--引用指定property配置,获取它的value值-->
<pattern>${UMR_LOG_STYLE}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!--日志记录器,使用类的全限定名称进行匹配,表示只捕获这一个类的日志-->
<logger name ="com.travel.logback.study.web.DemoController" level="INFO" additivity="false">
<appender-ref ref="CONSOLE"/>
</logger>
使用文件输出策略
将日志输出到文件比较麻烦,需要考虑输出到哪里,一个log文件最大是多少,保存多少天,文件超过最大又该如何处理。
针对这些问题,可以使用RollingFileAppender,它里面的属性能够帮助我们控制文件大小、根据时间将日志信息进行压缩等等。
ch.qos.logback.core.rolling.RollingFileAppender这个类有一个rollingPolicy成员变量
而RollingPolicy只是一个接口,所以我们使用SizeAndTimeBasedRollingPolicy这个实现类,这个实现类的源码如下:
类图如下
观察类图可以发现TimeBasedRollingPolicy继承于RollingPolicyBase。RollingPolicyBase有个fileNamePattern变量,能够帮助我们按照某种规则拆分日志。
XML
<!--项目存放路径-->
<property name="BASE_LOG_HOME" value="/opt/applog" />
<property name="MAX_FILE_SIZE" value="1MB" />
<property name="MAX_HISTORY" value="3" />
<!--项目的名称-->
<property name="SERVICE_NAME" value="ACCOUNT_MANAGE_SYSTEM" />
<property name="NEW_LOG_STYLE"
value="[%-5level] [%thread] [%logger] [%X{traceId-UMR}] [%d{yyyy-mm-dd H:mm:ss.sss}] [%c] [%M] [%L] %m%n"/>
<!-- 定义一个日志文件输出的appender,RollingFileAppender 可以实现日志拆分、和归档压缩 -->
<appender name="LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--
会根据给定的路径创建对应文件夹以及log文件
【/opt/applog】会从根路径创建文件夹。如果是windows系统,会默认从当前项目所在磁盘的根路径开始,如果处于D盘,则会创建 D:/opt/applog目录
【opt/applog】是在当前项目下创建一个opt/applog目录
-->
<!--file标签指定日志文件保存路径-->
<file>${BASE_LOG_HOME}/${SERVICE_NAME}_rollback.log</file>
<!--滚动策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件的最大限制 -->
<maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
<!-- 日志文件保留时间,默认以天为单位 -->
<maxHistory>${MAX_HISTORY}</maxHistory>
<!-- 日志文件每超过一次最大限制,则按时间压缩一次,%i是一个计数,从0开始,往上递增-->
<fileNamePattern>${BASE_LOG_HOME}/${SERVICE_NAME}/%d{yyyy-MM-dd}.log%i.log.zip</fileNamePattern>
</rollingPolicy>
<encoder>
<!--指定log文件中的日志格式-->
<pattern>${NEW_LOG_STYLE}</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="LOG_FILE" />
<appender-ref ref="CONSOLE" />
</root>
我在logback文件中写的是超过1M就进行压缩,通过for循环模拟1w次请求。(生产环境,日志文件肯定不要限制这么小,这里只是为了方便测试)
java
@RestController
public class DemoController {
private Logger logger = LoggerFactory.getLogger(DemoController.class);
@PostConstruct
public void testLogback() {
for (int i = 0; i < 10000; i++) {
logger.info("---info 级别的日志 ---");
logger.warn("---warn 级别的日志 ---");
logger.error("---error 级别的日志 ---");
}
}
}
最外层的log,因为我们限制文件大小为1MB,所以肯定不会超过
压缩包是经过压缩之后形成的,肯定没有1M,但解压之后,他的文件大小就是1M左右了
为什么有些appender节点下面存在<file>、<rollingPolicy>等子节点,有些又不存在?
这取决于class属性中定义的是什么类型。项目启动的时候,会创建class指定类型的对象,并且子节点的名称并不是随便起的,是class中定义的这个类的变量名。子节点设置的值会通过类的set()方法进行属性的赋值。
能够看见,项目启动时调用了set方法,并将配置文件中的值赋值给指定对象。
<logger>标签
一个logger标签就表示一个日志记录器,在同一个配置文件中可以有多个。他有三个属性 name、level 和 additivity,以及一个子节点<appender-ref>。
一、level可以设置日志级别。
二、additivity默认为true,当logger匹配成功后,调用父logger的appender进行日志输出;
如果为false,则使用当前logger的appender进行日志输出。当additivity为true的时候,不要在当前logger引用<appender-ref>标签,否则会输出2次一样的日志。
三、name的值可以是类的全限定名称,也可以是包名,也可以是自定义的名称。
①上面的例子就是使用类的全限定名称。
②使用包名,这个包下所有类的日志都会被这个logger记录,并输出到控制台或者文件中。
③name使用自定义的名称,这个也是我工作中经常使用的。
定义一个客户端,并打印日志
java
package com.travel.logback.study.web;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class WechatTerminalDemo {
private Logger wechatLogger = LoggerFactory.getLogger("Wechat-UMR");
@PostConstruct
public void payAmount(){
wechatLogger.debug("---远程接口调用成功--");
}
}
添加日志记录器
XML
<logger name ="Wechat-UMR" level="debug" additivity="false">
<appender-ref ref="CONSOLE"/>
</logger>
<root>标签
root标签也可以指定日志的级别,它是特殊的<logger>标签,是所有<logger>的祖先节点,等于<logger name="ROOT">。
一个配置文件中只能有一个root标签,且root标签只有一个属性level。因为name已经被命名为root,而且他是所有logger的祖先节点,所以也就没有additivity。
一个配置文件需要用<root>标签来进行兜底,避免日志出现丢失的情况。JAVA中定义的一个Logger对象,会向上遍历,直到在配置文件中找到合适的logger。如果一个logger都没找到,最终都会达到根logger,也就是root标签,所以,如果配置文件中没有声明root标签,那这个日志就会出现丢失。
比如,定义一个PayService,并且在一个方法中打印日志信息
java
package com.travel.logback.study.web;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
@Service
public class PayService {
private final Logger logger = LoggerFactory.getLogger(PayService.class);
//该注解的作用是让Springboot启动时,自动执行该方法
@PostConstruct
private void pay() {
logger.info("----支付成功----");
}
}
logback.xml配置文件如下,logger日志记录器只配置了一个,使用全限定名称,没有root标签。
XML
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<property name="UMR_LOG_STYLE" value="[%-5level] [%thread] [%d{yyyy-mm-dd H:mm:ss.sss}] [%c] [%M] [%L] %m%n" />
<!-- 控制台输出设置 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!--使用上文定义的,全局的property配置-->
<pattern>${UMR_LOG_STYLE}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<logger name ="com.travel.logback.study.web.DemoController" level="INFO" additivity="false">
<appender-ref ref="CONSOLE"/>
</logger>
</configuration>
启动就会发现,没有root标签进行兜底,少了非常多的日志,只保留了 DemoController的日志。
3.3 排除某些依赖包中的日志
有些第三方的依赖包会打出许多我们不需要知道的INFO日志,不方便我们排查问题。
说是排除,实际就是使用类的全限定名称,创建一个日志记录器,将日志级别提高,不让他打印INFO级别的日志。
XML
<!--name写入类的全限定名称,level调整日志级别,additivity默认为true-->
<logger name ="org.hibernate.boot.internal.SessionFactoryOptionsBuilder" level="ERROR" />
4、MDC的使用
MDC是 logback 提供的,用于日志跟踪的工具。多线程或者是并发量很大的情况下,请求没有唯一标识的话,不好排查问题,不清楚哪些日志对应的是哪一次请求,这个时候就需要用到MDC。
MDC 内部使用的是 ThreadLocal ,只有本线程才有效,如果在当前线程中,又开启了一个异步线程,那么异步线程的MDC就会丢失,异步线程获取指定key的时候就会是null。通常这种情况,都是请求传来的时候,携带一个requestId,用来表示唯一的请求,当前线程和异步线程就使用这唯一的requestId。
一般MDC是定义在拦截器中,等到一次完整的请求结束之后,再去清除掉,这里只是做一个Demo,所以我就不创建拦截器了。
java
package com.travel.logback.study.web;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
import java.util.Date;
@RestController
public class DemoController {
private Logger logger = LoggerFactory.getLogger(DemoController.class);
//该注解的作用是让Springboot启动时,自动执行该方法
@PostConstruct
public void testLogback() {
//往当前线程存入一个键值对
MDC.put("traceId-UMR", "traceId" + new Date().getTime());
logger.info("当前线程的traceId为:" + MDC.get("traceId-UMR"));
logger.info("---info 级别的日志 ---");
logger.warn("---warn 级别的日志 ---");
//清除当前线程的traceId
logger.warn("---清除当前线程的MDC ---");
MDC.clear();
logger.error("---error 级别的日志 ---");
}
}
logback中,使用%X{key}来接收MDC中的值,key为存入MDC中的键
java
<!--%X{key} %X表示输出MDC中特定键的值,key为具体的键名称,值不存在,则不会输出-->
<property name="NEW_LOG_STYLE" value="[%-5level] [%thread] [%logger] [%X{traceId-UMR}] [%d{yyyy-mm-dd H:mm:ss.sss}] [%c] [%M] [%L] %m%n" />
<!-- 控制台输出设置 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!--使用上文定义的,全局的property配置-->
<pattern>${NEW_LOG_STYLE}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<logger name ="com.travel.logback.study.web" level="INFO" additivity="false">
<appender-ref ref="CONSOLE"/>
</logger>
由于打印error级别日志之前,我在代码中已经执行了clear()方法,将当前线程的所有key给清除掉了,所以,输出error日志到控制台的时候,显示是空白的。
5、注意事项
①logback单独使用,也可以通过logback.xml文件来完成一些个性化的配置,并不一定需要是Springboot项目。单独使用时,只需要引入slf4j和logback的包就可以了。
②日志工具的logger对象应当用private static final修饰,private保证不会被其他类调用,static final保证在类加载时就被初始化,并且其值不会改变,减少内存消耗。
③打印日志时,尽量不要使用"+"进行拼接,这会额外消耗内存,尽量使用{}占位符。