愿你如阳光,明媚不忧伤。
目録
- 1. 日志系统SLF4J
- 1.1 主流的日志框架
- 2. 日志级别
- 3. 使用SLF4J
- 3.1 使用占位符 { }
- 4. Spring Boot 中的 Log
- 4.1 添加依赖
- 4.2 配置日志文件
- 4.3 logback-spring.xml 分解
- 5. 日志进阶优化
- 5.1 继承优化
- 5.2 方法内优化
1. 日志系统SLF4J
slf4j(Simple Logging Facade for Java)即简单日志门面,不是具体的日志解决方案,它只服务于各种各样的日志系统。按照官方的说法,SLF4J是一个用于日志系统的简单Facade,允许最终用户在部署其应用时使用其所希望的日志系统。SLF4J提供了统一的记录日志的接口,只要按照其提供的方法记录即可,最终日志的格式、记录级别、输出方式等通过具体日志系统的配置来实现,因此可以在应用中灵活切换日志系统。
1.1 主流的日志框架
- JUL(java.util.logging) java内置的日志模块。
- JCL(Jakarta Commons Logging) 也叫Apache Commons Logging其中spring就是用的这个实现日志记录的。
- log4j(Log4j是Apache的一个开源项目)是一个日志功能比较强大的日志记录器。
- logback是log4j的升级版,由三个模块组成:logback-core, logback-classic and logback-access。 其中logback-core为另外两个模块提供了基础,logback-classic是 log4j的改进版和实现slf4j。Logback的定制性更加灵活,同时也是spring boot的内置日志框架。
- log4j2 是现在市面上功能最强大的日志记录器,和log4j是两个框架并不是log4j的升级版。只是借用了一下log4j的名称而已。
- Jboss-logging(用得很少忽略)
2. 日志级别
- trace (the least serious)
健康级别,一般不会使用,在日志里边也不会打印出来(开发时会将操作数据库的 sql 打印出来) - debug
一般放于程序的某个关键点的地方,用于打印一个变量值或者一个方法返回的信息之类的信息 - info
一般处理业务逻辑的时候使用,就跟 system.out打印一样,用于说明此处是干什么的。slf4j使用的时候是可以动态的传参的,使用占位符 {} 。后边一次加参数,会挨个对应进去。 - warn
警告,不会影响程序的运行,但是值得注意。 - error
用户程序报错,必须解决的时候使用此级别打印日志。 - fatal (the most serious)
致命级别
- 实例
package com.it.god.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTestController {
Logger logger = LoggerFactory.getLogger(LogTestController.class);
public static void main(String[] args) {
LogTestController lc = new LogTestController();
Logger log = lc.logger;
try {
lc.fire("手动点火");
lc.clutch();
lc.gear(2);
lc.looseBrake();
} catch (Exception e) {
log.error("run failed !", e.getCause());
}
log.info("car is ready to run faster !");
}
// 点火
public void fire(String fire) {
logger.info("fire start");
logger.trace("fire");
logger.debug(fire);
logger.info("fire end");
}
// 踩离合
public void clutch() {
logger.info("clutch start");
logger.trace("clutch");
logger.info("clutch end");
}
// 挂档
public void gear(int gear) {
logger.info("gear start");
logger.debug(String.valueOf(gear));
logger.warn("Warning!!!gear" + gear + "is too high!!!");
logger.info("gear end");
}
// 松刹车
public void looseBrake() {
logger.info("looseBrake start");
logger.trace("looseBrake");
logger.error("制动失败");
logger.info("looseBrake end");
}
}
................................................................
・【CONSOLE】実行結果
16:02:01.278 [main] INFO com.it.god.controller.LogTestController - fire start
16:02:01.281 [main] DEBUG com.it.god.controller.LogTestController - 手动点火
16:02:01.281 [main] INFO com.it.god.controller.LogTestController - fire end
16:02:01.281 [main] INFO com.it.god.controller.LogTestController - clutch start
16:02:01.281 [main] INFO com.it.god.controller.LogTestController - clutch end
16:02:01.282 [main] INFO com.it.god.controller.LogTestController - gear start
16:02:01.282 [main] DEBUG com.it.god.controller.LogTestController - 2
16:02:01.282 [main] WARN com.it.god.controller.LogTestController - Warning!!!gear2is too high!!!
16:02:01.282 [main] INFO com.it.god.controller.LogTestController - gear end
16:02:01.282 [main] INFO com.it.god.controller.LogTestController - looseBrake start
16:02:01.282 [main] ERROR com.it.god.controller.LogTestController - 制动失败
16:02:01.282 [main] INFO com.it.god.controller.LogTestController - looseBrake end
16:02:01.282 [main] INFO com.it.god.controller.LogTestController - car is ready to run faster !
3. 使用SLF4J
启用SLF4J的库意味着仅添加一个强制性依赖项,即slf4j-api.jar
。如果在类路径上未找到绑定,则SLF4J将默认为无操作实现。我们只把当前类.class 作为参数传给LoggerFactory,由日志工厂给我们生产出一个日志记录器 Logger ,然后就可以通过logger.info(),logger.error()来实现日志记录功能。
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
在这个图片中我们可以大概的了解到通过slf4j日志记录,不同日志实现有不同日志实现架构(api → slf4j 直接实现 / 适配器层 → 第三方日志记录器)。其中slf4j直接实现和适配层是最为关键的一步。
- logback
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
- log4j
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.30</version>
</dependency>
- java.util.logging
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.30</version>
</dependency>
- simple
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.30</version>
</dependency>
- no-operation
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>2.0.0-alpha1</version>
</dependency>
3.1 使用占位符 { }
打印参数必须使用{},可以优化提高运行速度。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTestController {
Logger logger = LoggerFactory.getLogger(LogTestController.class);
public static void main(String[] args) {
LogTestController lc = new LogTestController();
Logger log = lc.logger;
try {
lc.gear(2, "Ouseki");
} catch (Exception e) {
log.error("run failed !", e.getCause());
}
log.info("car is ready to run faster !");
}
// 挂档
public void gear(int gear, String args) {
logger.info("gear start");
logger.debug(String.valueOf(gear));
logger.warn("Warning!!!gear{};args{}" + gear + "is too high!!!", gear, args);
logger.info("gear end");
}
................................................................
・【CONSOLE】実行結果
17:01:18.261 [main] INFO com.it.god.controller.LogTestController - gear start
17:01:18.265 [main] DEBUG com.it.god.controller.LogTestController - 2
17:01:18.265 [main] WARN com.it.god.controller.LogTestController - Warning!!!gear2;argsOuseki2is too high!!!
17:01:18.267 [main] INFO com.it.god.controller.LogTestController - gear end
17:01:18.267 [main] INFO com.it.god.controller.LogTestController - car is ready to run faster !
4. Spring Boot 中的 Log
4.1 添加依赖
maven依赖中添加 spring-boot-starter-logging
但是呢,实际开发中我们不需要直接添加该依赖,你会发现spring-boot-starter
其中包含了 spring-boot-starter-logging
,该依赖内容就是 Spring Boot 默认的日志框架 Logback+SLF4J
。而 spring-boot-starter-web 包含了spring-boot-starter,所以我们只需要引入web组件即可:
- pom.xml
<!--
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
4.2 配置日志文件
默认情况下Spring Boot将日志输出到控制台,不会写到日志文件。如果要编写除控制台输出之外的日志文件,则需在application.yml中设置logging.file或logging.path属性。
spring:
# file和path二者不能同时使用,如若同时使用,则只有logging:file生效:
logging:
# 指定log配置文件位置,默认为 logback-spring.xml,要想改名,必须设置此项
config: classpath:config/logback.xml
# 设置文件,可以是绝对路径,也可以是相对路径。如:logging:file: my.log
file: my.log
# 设置目录,会在该目录下创建spring.log文件,并写入日志内容,如:logging:path: /var/log
path: /var/log
level:
# 包名: 指定包下的日志级别
# 开发时设置成 trace 会将操作数据库的 sql 打印出来,方便定位问题,在生产环境上,将这个日志级别再设置成 error 级别即可
com.yg.example.dao: trace
pattern:
console: 日志打印规则
上面这种方式配置简单,但是能实现的功能也非常有限,如果想要更复杂的需求,就需要下面的定制化配置了。将xml放至 src/main/resource
下面。
- logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<!-- appender是configuration的子节点,是负责写日志的组件。 -->
<!-- ConsoleAppender:把日志输出到控制台 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- 默认情况下,每个日志事件都会立即刷新到基础输出流。 这种默认方法更安全,因为如果应用程序在没有正确关闭appender的情况下退出,则日志事件不会丢失。
但是,为了显着增加日志记录吞吐量,您可能希望将immediateFlush属性设置为false -->
<!--<immediateFlush>true</immediateFlush>-->
<encoder>
<!-- %37():如果字符没有37个字符长度,则左侧用空格补齐 -->
<!-- %-37():如果字符没有37个字符长度,则右侧用空格补齐 -->
<!-- %15.15():如果记录的线程字符长度小于15(第一个)则用空格在左侧补齐,如果字符长度大于15(第二个),则从开头开始截断多余的字符 -->
<!-- %-40.40():如果记录的logger字符长度小于40(第一个)则用空格在右侧补齐,如果字符长度大于40(第二个),则从开头开始截断多余的字符 -->
<!-- %msg:日志打印详情 -->
<!-- %n:换行符 -->
<!-- %highlight():转换说明符以粗体红色显示其级别为ERROR的事件,红色为WARN,BLUE为INFO,以及其他级别的默认颜色。 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) --- [%15.15(%thread)] %cyan(%-40.40(%logger{40})) : %msg%n</pattern>
<!-- 控制台也要使用UTF-8,不要使用GBK,否则会中文乱码 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- info 日志-->
<!-- RollingFileAppender:滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
<!-- 以下的大概意思是:1.先按日期存日志,日期变了,将前一天的日志文件名重命名为XXX%日期%索引,新的日志仍然是project_info.log -->
<!-- 2.如果日期没有发生变化,但是当前日志的文件大小超过10MB时,对当前日志进行分割 重命名-->
<appender name="info_log" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志文件路径和名称-->
<File>logs/project_info.log</File>
<!--是否追加到文件末尾,默认为true-->
<append>true</append>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>DENY</onMatch><!-- 如果命中ERROR就禁止这条日志 -->
<onMismatch>ACCEPT</onMismatch><!-- 如果没有命中就使用这条规则 -->
</filter>
<!--有两个与RollingFileAppender交互的重要子组件。 第一个RollingFileAppender子组件,即RollingPolicy:负责执行翻转所需的操作。
RollingFileAppender的第二个子组件,即TriggeringPolicy:将确定是否以及何时发生翻转。 因此,RollingPolicy负责什么和TriggeringPolicy负责什么时候.
作为任何用途,RollingFileAppender必须同时设置RollingPolicy和TriggeringPolicy,但是,如果其RollingPolicy也实现了TriggeringPolicy接口,则只需要显式指定前者。-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件的名字会根据fileNamePattern的值,每隔一段时间改变一次 -->
<!-- 文件名:logs/project_info.2017-12-05.0.log -->
<!-- 注意:SizeAndTimeBasedRollingPolicy中 %i和%d令牌都是强制性的,必须存在,要不会报错 -->
<fileNamePattern>logs/project_info.%d.%i.log</fileNamePattern>
<!-- 每产生一个日志文件,该日志文件的保存期限为30天, ps:maxHistory的单位是根据fileNamePattern中的翻转策略自动推算出来的,例如上面选用了yyyy-MM-dd,则单位为天
如果上面选用了yyyy-MM,则单位为月,另外上面的单位默认为yyyy-MM-dd-->
<maxHistory>30</maxHistory>
<!-- 每个日志文件到10mb的时候开始切分,最多保留30天,但最大到20GB,哪怕没到30天也要删除多余的日志 -->
<totalSizeCap>20GB</totalSizeCap>
<!-- maxFileSize:这是活动文件的大小,默认值是10MB,测试时可改成5KB看效果 -->
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>
<!--编码器-->
<encoder>
<!-- pattern节点,用来设置日志的输入格式 ps:日志文件中没有设置颜色,否则颜色部分会有ESC[0:39em等乱码-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level --- [%15.15(%thread)] %-40.40(%logger{40}) : %msg%n</pattern>
<!-- 记录日志的编码:此处设置字符集 - -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- error 日志-->
<!-- RollingFileAppender:滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
<!-- 以下的大概意思是:1.先按日期存日志,日期变了,将前一天的日志文件名重命名为XXX%日期%索引,新的日志仍然是project_error.log -->
<!-- 2.如果日期没有发生变化,但是当前日志的文件大小超过10MB时,对当前日志进行分割 重命名-->
<appender name="error_log" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志文件路径和名称-->
<File>logs/project_error.log</File>
<!--是否追加到文件末尾,默认为true-->
<append>true</append>
<!-- ThresholdFilter过滤低于指定阈值的事件。 对于等于或高于阈值的事件,ThresholdFilter将在调用其decision()方法时响应NEUTRAL。 但是,将拒绝级别低于阈值的事件 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level><!-- 低于ERROR级别的日志(debug,info)将被拒绝,等于或者高于ERROR的级别将相应NEUTRAL -->
</filter>
<!--有两个与RollingFileAppender交互的重要子组件。 第一个RollingFileAppender子组件,即RollingPolicy:负责执行翻转所需的操作。
RollingFileAppender的第二个子组件,即TriggeringPolicy:将确定是否以及何时发生翻转。 因此,RollingPolicy负责什么和TriggeringPolicy负责什么时候.
作为任何用途,RollingFileAppender必须同时设置RollingPolicy和TriggeringPolicy,但是,如果其RollingPolicy也实现了TriggeringPolicy接口,则只需要显式指定前者。-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 活动文件的名字会根据fileNamePattern的值,每隔一段时间改变一次 -->
<!-- 文件名:logs/project_error.2017-12-05.0.log -->
<!-- 注意:SizeAndTimeBasedRollingPolicy中 %i和%d令牌都是强制性的,必须存在,要不会报错 -->
<fileNamePattern>logs/project_error.%d.%i.log</fileNamePattern>
<!-- 每产生一个日志文件,该日志文件的保存期限为30天, ps:maxHistory的单位是根据fileNamePattern中的翻转策略自动推算出来的,例如上面选用了yyyy-MM-dd,则单位为天
如果上面选用了yyyy-MM,则单位为月,另外上面的单位默认为yyyy-MM-dd-->
<maxHistory>30</maxHistory>
<!-- 每个日志文件到10mb的时候开始切分,最多保留30天,但最大到20GB,哪怕没到30天也要删除多余的日志 -->
<totalSizeCap>20GB</totalSizeCap>
<!-- maxFileSize:这是活动文件的大小,默认值是10MB,测试时可改成5KB看效果 -->
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>
<!--编码器-->
<encoder>
<!-- pattern节点,用来设置日志的输入格式 ps:日志文件中没有设置颜色,否则颜色部分会有ESC[0:39em等乱码-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level --- [%15.15(%thread)] %-40.40(%logger{40}) : %msg%n</pattern>
<!-- 记录日志的编码:此处设置字符集 - -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!--给定记录器的每个启用的日志记录请求都将转发到该记录器中的所有appender以及层次结构中较高的appender(不用在意level值)。
换句话说,appender是从记录器层次结构中附加地继承的。
例如,如果将控制台appender添加到根记录器,则所有启用的日志记录请求将至少在控制台上打印。
如果另外将文件追加器添加到记录器(例如L),则对L和L'子项启用的记录请求将打印在文件和控制台上。
通过将记录器的additivity标志设置为false,可以覆盖此默认行为,以便不再添加appender累积-->
<!-- configuration中最多允许一个root,别的logger如果没有设置级别则从父级别root继承 -->
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
<!-- 指定项目中某个包,当有日志操作行为时的日志记录级别 -->
<!-- 级别依次为【从高到低】:FATAL > ERROR > WARN > INFO > DEBUG > TRACE -->
<logger name="com.sailing.springbootmybatis" level="INFO">
<appender-ref ref="info_log" />
<appender-ref ref="error_log" />
</logger>
<!-- 利用logback输入mybatis的sql日志,
注意:如果不加 additivity="false" 则此logger会将输出转发到自身以及祖先的logger中,就会出现日志文件中sql重复打印-->
<logger name="com.sailing.springbootmybatis.mapper" level="DEBUG" additivity="false">
<appender-ref ref="info_log" />
<appender-ref ref="error_log" />
</logger>
<!-- additivity=false代表禁止默认累计的行为,即com.atomikos中的日志只会记录到日志文件中,不会输出层次级别更高的任何appender-->
<logger name="com.atomikos" level="INFO" additivity="false">
<appender-ref ref="info_log" />
<appender-ref ref="error_log" />
</logger>
</configuration>
4.3 logback-spring.xml 分解
- 定义日志输出格式和存储路径
<configuration>
<!-- 首先定义一个格式,命名为 “LOG_PATTERN”,该格式中 %date 表示日期, %thread 表示线程名, %-5level
表示级别从左显示5个字符宽度, %logger{36} 表示 logger 名字最长36个字符, %msg 表示日志消息, %n 是换行符。 -->
<property name="LOG_PATTERN"
value="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" />
<!-- 再定义一下名为 “FILE_PATH” 文件路径,日志都会存储在该路径下。 %i 表示第 i 个文件,当日志 文件达到指定大小时,会将日志生成到新的文件里,这里的
i 就是文件索引,日志文件允许的大小可以 设置,下面会讲解。这里需要注意的是,不管是 windows 系统还是 Linux 系统,日志存储的路径必须
要是绝对路径。 -->
<property name="FILE_PATH"
value="D:/logs/course03/demo.%d{yyyy-MM- dd}.%i.log" />
</configuration>
- 定义控制台输出
<configuration>
<appender name="CONSOLE"
class="ch.qos.logback.core.ConsoleAppender">
<encoder> <!-- 按照上面配置的LOG_PATTERN来打印日志 -->
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
</configuration>
- 定义日志文件的相关参数
<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>
- 定义日志输出级别
<configuration>
<!-- 有了上面那些定义后,最后我们使用 <logger> 来定义一下项目中默认的日志输出级别,这里定义级别为 INFO,然后针对 INFO 级别的日志,
使用 <root> 引用上面定义好的控制台日志输出和日志文件的参数。这样 logback.xml 文件中的配置就设置完了。 -->
<logger name="com.it.god.controller" level="INFO" />
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>
5. 日志进阶优化
5.1 继承优化
当我们想要在一个类中打日志的时候,每次都必须要使用 LoggerFactory 进行创建,每一个类都要创建一次,重复代码太多了,而且还容易出错,这时候我们可以引入继承。
- BaseController.java
public class BaseController {
protected Logger log;
public BaseController() {
log = LoggerFactory.getLogger(this.getClass());
}
}
- TestController.java
@RestController
@RequestMapping("/test")
public class TestController extends BaseController {
@RequestMapping("/log")
public void testLog() {
log.info("测试开始");
System.out.println("通过继承,每次打log不用每次都使用 LoggerFactory 进行创建了。");
log.info("测试结束");
}
}
浏览器访问 http://localhost:8080/test/log
5.2 方法内优化
通过继承我们只创建一次log,继承的类就不用自己再去创建了,那还有什么需要优化的呢,仔细观察我们打的log,会发现方法开头处的log,是以方法名+开始
为后缀的,方法尾端处的log是以方法名+结束
为后缀的。而且要是想展现额外的log每个方法中都需要重复的设置。
- LogBean.java
public class LogBean {
protected Logger log;
public LogBean() {
log = LoggerFactory.getLogger(this.getClass());
}
protected String startLog(String systemName, String methodName) {
return "SYSTEM:" + systemName + " METHOD:" + methodName + " START。";
}
protected String endLog(String systemName, String methodName) {
return "SYSTEM:" + systemName + " METHOD:" + methodName + " END。";
}
}
- BaseController.java
public class BaseController extends LogBean {
private String systemName;
public BaseController() {
String className = this.getClass().getSimpleName();
systemName = className;
}
protected String startLog(String methodName) {
return super.startLog(systemName, methodName);
}
protected String endLog(String methodName) {
return super.endLog(systemName, methodName);
}
}
- TestController.java
@RestController
@RequestMapping("/test")
public class TestController extends BaseController {
@RequestMapping("/log")
public void testLog() {
log.info(startLog("testLog"));
System.out.println("通过继承,每次打log不用每次都使用 LoggerFactory 进行创建了。");
// 也可以通过线程信息获取方法名
log.info(endLog(Thread.currentThread() .getStackTrace()[1].getMethodName()));
}
}
浏览器访问 http://localhost:8080/test/log