Skip to content

五、日志配置

在开发中,我们经常使用 System.out.println() 来打印一些信息,但是这样不好,因为大量的使用 System.out 会增加资源的消耗。我们实际项目中使用的是 slf4j 的 logback 来输出日志,效率挺高的,Spring Boot 提供了一套日志系统,logback 是最优的选择。

1、日志级别

几种常见的日志级别由低到高分为: TRACE < DEBUG < INFO < WARN < ERROR < FATAL 。 如何理解这个日志级别呢?很简单,如果项目中的日志级别设置为 INFO ,那么比它更低级别的日志信息就看不到了,即是 TRACEDEBUG 日志将会不显示。

2、Spring Boot 日志框架

Spring Boot默认的日志框架是 logback ,既然Spring Boot能够将其纳入的默认的日志系统,肯定是有 一定的考量的,因此实际开发过程中还是不要更换。

原则上需要使用logback,需要添加以下依赖,但是既然是默认的日志框架,当然不用重新引入依赖了。

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-logging</artifactId>
    <version>2.6.7</version>
</dependency>

Spring Boot中默认的日志级别是 INFO ,启动项目日志打印如下:

image-20220504192941323

从上图可以看出,输出的日志的默认元素如下:

  1. 时间日期:精确到毫秒
  2. 日志级别:ERROR, WARN, INFO, DEBUG , TRACE
  3. 进程ID
  4. 分隔符:— 标识实际日志的开始
  5. 线程名:方括号括起来(可能会截断控制台输出)
  6. Logger名:通常使用源代码的类名
  7. 日志内容

3、slf4j 介绍

引用百度百科里的一段话:

SLF4J,即简单日志门面(Simple Logging Facade for Java),不是具体的日志解决方案,它只服务于各种各样的日志系统。按照官方的说法,SLF4J是一个用于日志系统的简单Facade,允许最终用户在部署其应用时使用其所希望的日志系统。

这段的大概意思是:你只需要按统一的方式写记录日志的代码,而无需关心日志是通过哪个日志系统,以什么风格输出的。因为它们取决于部署项目时绑定的日志系统。例如,在项目中使用了 slf4j 记录日志,并且绑定了 log4j(即导入相应的依赖),则日志会以 log4j 的风格输出;后期需要改为以 logback 的风格输出日志,只需要将 log4j 替换成 logback 即可,不用修改项目中的代码。这对于第三方组件的引入的不同日志系统来说几乎零学习成本,况且它的优点不仅仅这一个而已,还有简洁的占位符的使用和日志级别的判断。

正因为 sfl4j 有如此多的优点,阿里巴巴已经将 slf4j 作为他们的日志框架了。在《阿里巴巴Java开发手册(正式版)》中,日志规约一项第一条就强制要求使用 slf4j:

1.【强制】应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架SLF4J中的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。

“强制”两个字体现出了 slf4j 的优势,所以建议在实际项目中,使用 slf4j 作为自己的日志框架。使用 slf4j 记录日志非常简单,直接使用 LoggerFactory 创建即可。

java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Test {
    private static final Logger log = LoggerFactory.getLogger(Test.class);
    
   	@Test
	public void test(){ 
        log.debug("输出DEBUG日志.......");
	}
}

使用lombok

xml
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

使用也是很简单,只需要在类上标注一个注解 @Slf4j 即可,如下:

java
@Slf4j
public class Test {
    
   	@Test
	public void test(){ 
        log.debug("输出DEBUG日志.......");
	}
}

4、application.yml、properties 中对日志的配置

1、如何指定日志级别

Spring Boot中默认的日志级别是INFO,但是可以自己定制日志级别,如下:

properties
logging.level.root=warn

# 指定包的日志级别
logging.level.com.xue=info

2、日志如何输出到文件中

其中有两个重要配置如下:

  1. logging.file.path:指定日志文件的路径
  2. logging.file.name:日志的文件名,默认为 spring.log

注意:官方文档说这两个属性不能同时配置,否则不生效,因此只需要配置一个即可。

properties
# springboot会每天打个包
logging.file.name=/Users/xueqimiao/springboot-test.log

3、如何定制日志格式

默认的日志格式在第一张图已经看到了,有时我们需要定制自己需要的日志输出格式,这样在排查日志 的时候能够一目了然。 定制日志格式有两个配置,分别是控制台的输出格式和文件中的日志输出格式,如下:

  1. logging.pattern.console:控制台的输出格式
  2. logging.pattern.file :日志文件的输出格式

例如配置如下:

properties
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n 
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n

上面的配置编码的含义如下:

java
#%d{HH:mm:ss.SSS}——日志输出时间
#%thread——输出日志的进程名字,这在Web应用以及异步任务处理中很有用
#%-5level——日志级别,并且使用5个字符靠左对齐
#%logger- ——日志输出者的名字
#%msg——日志消息
#%n——平台的换行符

Spring Boot 对 slf4j 支持的很好,内部已经集成了 slf4j,一般我们在使用的时候,会对slf4j 做一下配置。application.yml 文件是 Spring Boot 中唯一一个需要配置的文件,一开始创建工程的时候是 application.properties 文件,个人比较细化用 yml 文件,因为 yml 文件的层次感特别好,看起来更直观,但是 yml 文件对格式要求比较高,比如英文冒号后面必须要有个空格,否则项目估计无法启动,而且也不报错。用 properties 还是 yml 视个人习惯而定,都可以。

我们看一下 application.yml 文件中对日志的配置:

yaml
logging:
  config: logback.xml
  level:
    com.xue.dao: trace

logging.config 是用来指定项目启动的时候,读取哪个配置文件,这里指定的是日志配置文件是根路径下的 logback.xml 文件,关于日志的相关配置信息,都放在 logback.xml 文件中了。logging.level 是用来指定具体的 mapper 中日志的输出级别,上面的配置表示 com.xue.dao 包下的所有 mapper 日志输出级别为 trace,会将操作数据库的 sql 打印出来,开发时设置成 trace 方便定位问题,在生产环境上,将这个日志级别再设置成 error 级别即可。

5、logback.xml 配置文件解析

在上面 application.yml 文件中,我们指定了日志配置文件 logback.xmllogback.xml 文件中主要用来做日志的相关配置。在 logback.xml 中,我们可以定义日志输出的格式、路径、控制台输出格式、文件大小、保存时长等等。下面来分析一下:

1、定义日志输出格式和存储路径

xml
<configuration>
	<property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" />
	<property name="FILE_PATH" value="D:/logs/course03/demo.%d{yyyy-MM-dd}.%i.log" />
</configuration>

我们来看一下这个定义的含义:首先定义一个格式,命名为 “LOG_PATTERN”,该格式中 %date 表示日期,%thread 表示线程名,%-5level 表示级别从左显示5个字符宽度,%logger{36} 表示 logger 名字最长36个字符,%msg 表示日志消息,%n 是换行符。

然后再定义一下名为 “FILE_PATH” 文件路径,日志都会存储在该路径下。%i 表示第 i 个文件,当日志文件达到指定大小时,会将日志生成到新的文件里,这里的 i 就是文件索引,日志文件允许的大小可以设置,下面会讲解。这里需要注意的是,不管是 windows 系统还是 Linux 系统,日志存储的路径必须要是绝对路径。

2、定义控制台输出

xml
<configuration>
	<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
            <!-- 按照上面配置的LOG_PATTERN来打印日志 -->
			<pattern>${LOG_PATTERN}</pattern>
		</encoder>
	</appender>
</configuration>

使用 <appender> 节点设置个控制台输出(class="ch.qos.logback.core.ConsoleAppender")的配置,定义为 “CONSOLE”。使用上面定义好的输出格式(LOG_PATTERN)来输出,使用 ${} 引用进来即可。

3、定义日志文件的相关参数

xml
<configuration>
	<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<!-- 按照上面配置的FILE_PATH路径来保存日志 -->
			<fileNamePattern>${FILE_PATH}</fileNamePattern>
			<!-- 日志保存15天 -->
			<maxHistory>15</maxHistory>
			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
				<!-- 单个日志文件的最大,超过则新建日志文件存储 -->
				<maxFileSize>10MB</maxFileSize>
			</timeBasedFileNamingAndTriggeringPolicy>
		</rollingPolicy>

		<encoder>
			<!-- 按照上面配置的LOG_PATTERN来打印日志 -->
			<pattern>${LOG_PATTERN}</pattern>
		</encoder>
	</appender>
</configuration>

使用 <appender> 定义一个名为 “FILE” 的文件配置,主要是配置日志文件保存的时间、单个日志文件存储的大小、以及文件保存的路径和日志的输出格式。

4、定义日志输出级别

xml
<configuration>
	<logger name="com.itcodai.course03" level="INFO" />
	<root level="INFO">
		<appender-ref ref="CONSOLE" />
		<appender-ref ref="FILE" />
	</root>
</configuration>

有了上面那些定义后,最后我们使用 <logger> 来定义一下项目中默认的日志输出级别,这里定义级别为 INFO,然后针对 INFO 级别的日志,使用 <root> 引用上面定义好的控制台日志输出和日志文件的参数。这样 logback.xml 文件中的配置就设置完了。

5、示例配置

logback-spring.xml(放入resource目录下即可,不需要额外的配置,只针对springboot默认的日志框架)

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/base.xml"/>
    <logger name="org.springframework.web" level="info"/>
    <!-- 定义日志文件 输入位置 -->
    <property name="logDir" value="./logs/project-name/" />
    <!-- 日志最大的历史 30天 -->
    <property name="maxHistory" value="30"/>

    <!-- 控制台输出日志 -->
    <!--<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger -%msg%n</pattern>
            <charset class="java.nio.charset.Charset">UTF-8</charset>
        </encoder>
    </appender>-->

    <!-- ERROR级别日志 -->
    <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logDir}\%d{yyyyMMdd}\error.log</fileNamePattern>
            <maxHistory>${maxHistory}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
            <charset class="java.nio.charset.Charset">UTF-8</charset>
        </encoder>
        <append>false</append>
        <prudent>false</prudent>
    </appender>

    <!-- WARN级别日志 -->
    <!--<appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>WARN</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logDir}\%d{yyyyMMdd}\warn.log</fileNamePattern>
            <maxHistory>${maxHistory}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
            <charset class="java.nio.charset.Charset">UTF-8</charset>
        </encoder>
        <append>false</append>
        <prudent>false</prudent>
    </appender>-->

    <!-- INFO级别日志 -->
    <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logDir}\%d{yyyyMMdd}\info.log</fileNamePattern>
            <maxHistory>${maxHistory}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
            <charset class="java.nio.charset.Charset">UTF-8</charset>
        </encoder>
        <append>false</append>
        <prudent>false</prudent>
    </appender>

    <!-- DEBUG级别日志 -->
    <!--<appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logDir}\%d{yyyyMMdd}\debug.log</fileNamePattern>
            <maxHistory>${maxHistory}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
            <charset class="java.nio.charset.Charset">UTF-8</charset>
        </encoder>
        <append>false</append>
        <prudent>false</prudent>
    </appender>-->

    <!-- root级别  DEBUG -->
    <root level="DEBUG">
        <!-- 控制台输出 -->
        <!--<appender-ref ref="STDOUT" />-->
        <!-- 文件输出 -->
        <appender-ref ref="ERROR" />
        <appender-ref ref="INFO" />
        <!--<appender-ref ref="WARN" />
        <appender-ref ref="DEBUG" />-->
    </root>
</configuration>

6、使用Logger在项目中打印日志

在代码中,我们一般使用 Logger 对象来打印出一些 log 信息,可以指定打印出的日志级别,也支持占位符,很方便。

java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {

    private final static Logger logger = LoggerFactory.getLogger(TestController.class);

    @RequestMapping("/log")
    public String testLog() {
        logger.debug("=====测试日志debug级别打印====");
        logger.info("======测试日志info级别打印=====");
        logger.error("=====测试日志error级别打印====");
        logger.warn("======测试日志warn级别打印=====");

        // 可以使用占位符打印出一些参数信息
        String str1 = "xue.com";
        String str2 = "小薛博客";
        logger.info("======个人博客:{};小程序:{}", str1, str2);

        return "success";
    }
}

启动该项目,在浏览器中输入 localhost:8080/test/log 后可以看到控制台的日志记录:

======测试日志info级别打印===== =====测试日志error级别打印==== ======测试日志warn级别打印===== ======个人博客:xue.com;小程序:小薛博客

因为 INFO 级别比 DEBUG 级别高,所以 debug 这条没有打印出来,如果将 logback.xml 中的日志级别设置成 DEBUG,那么四条语句都会打印出来,这个大家自己去测试了。同时可以打开 D:\logs\course03\ 目录,里面有刚刚项目启动,以后后面生成的所有日志记录。在项目部署后,我们大部分都是通过查看日志文件来定位问题。

7、切换日志框架

1、如何做到无感知切换

SLF4j是日志门面,无论什么日志框架都是基于SLF4j的API实现, 因此无论是代码打印日志还是 Lombok注解形式打印日志,都要使用的SLF4j的API,而不是日志框架的API ,这样才能解耦,做到无 感知。因为最终切换的框架只是对于SLF4j的实现,并不是切换SLF4j。

其实这一条在阿里开发手册中也是明确指出了,如下:

image-20220504215937219

2、如何切换

Spring Boot默认是 Logback 日志框架,如果需要切换到其他的日志框架应该如何做?

首先我们先看官网的一张图,一切都在图中,如下:

image-20220504220047480SLF4j只是一个门面,共有两大特性。一是静态绑定、二是桥接。

什么是静态绑定?:我们以 log4j为例。首先我们的application中会使用slf4j的api进行日志记录。我 们引入适配层的jar包 slf4j-log412.jar 及底层日志框架实现 log4j.jar 。简单的说适配层做的事情就是 把 slf4j的api转化成 log4j的api。通过这样的方式来屏蔽底层框架实现细节。

什么是桥接?:比如你的application中使用了 slf4j ,并绑定了 logback 。但是项目中引入了一个 A.jarA.jar 使用的日志框架是 log4j 。那么有没有方法让 slf4j 来接管这个 A.jar 包中使用 log4j 输出的日志呢?这就用到了桥接包。你只需要引入 log4j-over-slf4j.jar 并删除log4j.jar就可以实现 slf4j A.jarlog4j的接管.听起来有些不可思议。你可能会想如果删除log4j.jar那 A.jar 不会报编译错误嘛?答案是不会。因为 log4j-over-slf4j.jar 实现了 log4j 几乎所有publicAPI。但关键方法都被改写了。不再是简单的输出日志,而是将日志输出指令委托给 slf4j

下面就以 log4j2 为例,切换Spring Boot的日志框架为 Log4j2

Spring Boot 默认是Logback日志框架,如果想要切换 log4j2 肯定是要将 Logback 的依赖移除,只需要 排除 web 模块中的 日志启动器 即可,如下:

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <!-- 去掉springboot默认日志框架logback -->
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>

排除了默认的logback依赖,肯定是需要引入 log4j2 的依赖,其实 log4j2 为了与Spring Boot适配也做 了个启动器,不需要在引入其他的jar包了,只需要添加如下依赖即可:

xml
<!-- 引入log4j2依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>