?
目录
了解日子框架体系
分析日志框架如何转换
logback日志的集成
SpringBoot日志使用
? ?常见的一些日志框架如,commons-logging.jar、log4j.jar、sl4j-api.jar等,他们之间是存在一定关系的,在实际开发中可能就存在日志框架替换的问题,所以只有了解这些框架之间的关系,才能去解决问题。
了解日子框架体系
? ? ?在以前我们只是纯粹使用System.out.println("")来打印输出,这不仅影响性能,在实际线上遇到问题时还很不方便定位问题位置。后来为了定位问题方便出现了日志框架,最初的开源框架log4j 也是收到广大开发者欢迎,之后jdk也推出日志框架jui (java.util.logging),后面又开发了一个日志门面slf4j,(它不实现日志功能,只是用于整合日志的,需要适配去,桥接器去转换)。
早期的log4j框架写的代码是这样的
import?org.apache.log4j.Logger;
\\省略
Logger?logger?=?Logger.getLogger(Test.class);
logger.trace("trace");
\\省略
之后sun公司对于log4j的出现内心隐隐表示嫉妒。于是在jdk1.4版本后,增加了一个包为java.util.logging,简称为jul,用以对抗log4j。如下所示?
import?java.util.logging.Logger;
\\省略
Logger?loggger?=?Logger.getLogger(Test.class.getName());?
logger.finest("finest");
\\省略
可以看出,api完全是不同的。那有没有办法,将这些api抽象出接口,这样以后调用的时候,就调用这些接口就好了呢?
? ? ? 这个时候jcl(Jakarta Commons Logging)出现了,说jcl可能大家有点陌生,那么commons-logging-xx.jar组件,大家总有印象吧。JCL 只提供 log 接口,具体的实现则在运行时动态寻找。这样一来组件开发者只需要针对 JCL 接口开发,而调用组件的应用程序则可以在运行时搭配自己喜好的日志实践工具。JCL可以实现的集成方案如下图所示
jcl默认的配置:如果能找到Log4j 则默认使用log4j 实现,如果没有则使用jul(jdk自带的) 实现,再没有则使用jcl内部提供的SimpleLog 实现。于是,你在代码里变成这么写了
import?org.apache.commons.logging.Log;
import?org.apache.commons.logging.LogFactory;
\\省略
Log?log?=LogFactory.getLog(Test.class);
log.trace('trace');
\\省略
? ? ?至于这个Log具体的实现类,JCL会在ClassLoader中进行查找。这么做,有三个缺点,缺点一是效率较低,二是容易引发混乱,三是在使用了自定义ClassLoader的程序中,使用JCL会引发内存泄露。JCL动态查找机制进行日志实例化,执行顺序为:commons-logging.properties---->系统环境变量------->log4j--->jul--->simplelog---->nooplog
? ? ? 于是log4j的作者觉得jcl不好用,自己又写了一个新的接口api,那么就是slf4j。关于slf4j的集成图如下所示
????????理解slf4j日志门面了吗,它跟jcl机制不一样。 它只提供一个门面,至于你要使用什么日志框架,需要引入对应的实现日志框架。JCL 背靠JDK 自带jul .
Slf4j与其他各种日志组件的桥接说明
各种日志组件的桥接说明
jar包名 | 说明 | slf4j-log4j12-1.7.13.jar | Log4j1.2版本的桥接器,你需要将Log4j.jar加入Classpath。 | log4j-slf4j-impl.jar | Log4j2版本的桥接器,还需要log4j-api.jar log4j-core.jar | slf4j-jdk14-1.7.13.jar | java.util.logging的桥接器,Jdk原生日志框架。 | slf4j-nop-1.7.13.jar | NOP桥接器,默默丢弃一切日志。 | slf4j-simple-1.7.13.jar | 一个简单实现的桥接器,该实现输出所有事件到System.err. 只有Info以及高于该级别的消息被打印,在小型应用中它也许是有用的。 | slf4j-jcl-1.7.13.jar | Jakarta Commons Logging 的桥接器. 这个桥接器将Slf4j所有日志委派给Jcl。 | logback-classic-1.0.13.jar(requires logback-core-1.0.13.jar) | Slf4j的原生实现,Logback直接实现了Slf4j的接口,因此使用Slf4j与Logback的结合使用也意味更小的内存与计算开销 |
?如图所示,应用调了sl4j-api,即日志门面接口。日志门面接口本身通常并没有实际的日志输出能力,它底层还是需要去调用具体的日志框架API的,也就是实际上它需要跟具体的日志框架结合使用。由于具体日志框架比较多,而且互相也大都不兼容,日志门面接口要想实现与任意日志框架结合可能需要对应的桥接器,上图红框中的组件即是对应的各种桥接器!
我们在代码中需要写日志,变成下面这么写
import?org.slf4j.Logger;
import?org.slf4j.LoggerFactory;
//省略
Logger?logger?=?LoggerFactory.getLogger(Test.class);
//?省略
logger.info("info");
?????????在代码中,并不会出现具体日志框架的api。程序根据classpath中的桥接器类型,和日志框架类型,判断出logger.info应该以什么框架输出!注意了,如果classpath中不小心引了两个桥接器,那会直接报错的!因此,在阿里的开发手册上才有这么一条:
强制:应用中不可直接使用日志系统(log4j、logback)中的 API ,而应依赖使用日志框架 SLF4J 中的 API 。使用门面模式的日志框架,有利于维护和各个类的日志处理方式的统一。
分析日志框架如何转换
一个项目,一个模块用log4j,另一个模块用slf4j+log4j2,如何统一输出?
? ? ? 这里就要用上slf4j的适配器,slf4j提供了各种各样的适配器,用来将某种日志框架委托给slf4j。其最明显的集成工作方式有如下:
? ? ? ?进行选择填空,将我们的案例里的条件填入,根据题意应该选log4j-over-slf4j适配器,于是就变成下面这张图
再比如:如何让spring以log4j2的形式输出?
spring默认使用的是jcl输出日志,由于你此时并没有引入Log4j的日志框架,jcl会以jul做为日志框架。此时集成图如下
?而你的应用中,采用了slf4j+log4j-core,即log4j2进行日志记录,那么此时集成图如下
那我们现在需要让spring以log4j2的形式输出?怎么办?
OK,第一种方案,走jcl-over-slf4j适配器,此时集成图就变成下面这样了
在这种方案下,spring框架中遇到日志输出的语句,就会如上图红线流程一样,最终以log4J2的形式输出!
OK,有第二种方案么?有,走jul-to-slf4j适配器,此时集成图如下
ps:这种情况下,记得在代码中执行
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
假设,我们在应用中调用了sl4j-api,但是呢,你引了四个jar包,slf4j-api-xx.jar,slf4j-log4j12-xx.jar,log4j-xx.jar,log4j-over-slf4j-xx.jar,于是你就会出现如下尴尬的场面
?如上图所示,在这种情况下,你调用了slf4j-api,就会陷入死循环中!slf4j-api去调了slf4j-log4j12,slf4j-log4j12又去调用了log4j,log4j去调用了log4j-over-slf4j。最终,log4j-over-slf4j又调了slf4j-api,陷入死循环!
logback日志的集成
总结:
- SpringBoot底层也是使用slf4j+logback的方式进行日志记录
- logback桥接:logback-classic
- SpringBoot也把其他的日志都替换成了slf4j;
- log4j 适配: log4j-over-slf4j
- jul适配:jul-to-slf4j
- 这两个适配器都是为了适配Spring的默认日志:jcl
SpringBoot日志使用
可以设置TRACE,DEBUG,INFO,WARN,ERROR,FATAL或OFF之一
logging:
level:
root: INFO
com.fairy.log: ERROR
org.springframework.web: ERROR
org.hibernate: DEBUG
pattern:
dateformat: '%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){red} %clr(${LOG_LEVEL_PATTERN:-%10p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}'
file:
name: ./logs/faity-info.log
max-size: 10KB
可以使用logging.pattern.console 修改默认的控制的日志格式:
? ? ? ? ? ?默认情况下,Spring Boot仅记录到控制台,不写日志文件。如果除了控制台输出外还想写日志文件,则需要设置一个logging.file.name或logging.file.path属性(例如application.properties)。下表显示了如何logging.*一起使用这些属性:
logging.file.name | logging.file.path | 实例 | 描述 | (没有) | (没有) | | 仅控制台记录。 | 指定文件名 | (没有) | my.log | 写入指定的日志文件。 | (没有) | 具体目录 | /var/log | 写入spring.log指定的目录。 |
- logging.file.name
- 可以设置文件的名称, 如果没有设置路径会默认在项目的相对路径下
- 还可以指定路径+文件名:name: D:/xushu.log
- logging.file.path
- 不可以指定文件名称, 必须要指定一个物理文件夹路径,会默认使用spring.log
? ? ? 如果你使用的是Logback,则可以使用application.properties或application.yaml文件微调日志轮播设置。对于所有其他日志记录系统,您需要直接自己配置轮转设置(例如,如果使用Log4J2,则可以添加log4j.xml文件)
名称 | 描述 | logging.logback.rollingpolicy.file-name-pattern | 归档的文件名 | logging.logback.rollingpolicy.clean-history-on-start | 如果应在应用程序启动时进行日志归档清理。 | logging.logback.rollingpolicy.max-file-size | 归档前日志文件的最大大小。 | logging.logback.rollingpolicy.total-size-cap | 删除日志档案之前可以使用的最大大小。 | logging.logback.rollingpolicy.max-history | 保留日志存档的天数(默认为7) |
- logging.logback.rollingpolicy.file-name-pattern
- ${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz
- ${LOG_FILE} 对应 logging.file.name
- %d{yyyy-MM-dd} 日期 年-月-日
- %i 索引, 当文件超出指定大小后进行的文件索引递增
可以通过在类路径中包含适日志配置文件来激活各种日志记录系统或使用logging.config
Logging System | Customization | Logback | logback-spring.xml,?logback-spring.groovy,?logback.xml, or?logback.groovy | Log4j2 | log4j2-spring.xml?or?log4j2.xml | JDK (Java Util Logging) | logging.properties |
注意:
- 如果使用自定义日志配置文件 会使用springboot中全局配置文件的logging相关配置失效
- 结合SpringBoot提供Profile来控制日志的生效
- 注意: 一定要将日志配置文件的文件名改成logback-spring.xml, 因为?logback.xml 会在Springboot容器加载前先被logback给加载到, 那么由于logback无法解析springProfile 将会报错:
- ERROR in ch.qos.logback.core.joran.spi.Interpreter@4:71 - no applicable action for [springProperty], current ElementPath is [[configuration][springProperty]] ERROR in ch.qos.logback.core.joran.spi.Interpreter@4:71 - no applicable action for [springProfile], current ElementPath is [[configuration][springProfile]]
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 格式 -->
<springProfile name="dev">
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{100} ======= %msg%n</pattern>
</springProfile>
<springProfile name="!dev">
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{100} ++++++ %msg%n</pattern>
</springProfile>
</encoder>
</appender>
- 将logback的场景启动器排除(slf4j只能运行有1个桥接器)
- 添加log4j2的场景启动器
- 添加log4j2的配置文件
<dependencies>
<dependency>
<!--starter-web里面自动添加starter-logging 也就是logback的依赖-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!--排除starter-logging 也就是logback的依赖-->
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!--Log4j2的场景启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
</dependencies>
- 要将logback的桥接器排除
<dependency>
<!--starter-web里面自动添加starter-logging 也就是logback的依赖-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
</exclusion>
</exclusions>
</dependency>
- 添加log4j的桥接器
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
- 添加log4j的配置文件
log4j.properties
#trace<debug<info<warn<error<fatal
log4j.rootLogger=trace, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
举一个logback.xml的例子:
<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration scan="true" scanPeriod="10 seconds">
<contextName>logback</contextName>
<!--定义日志文件的存储地址, 这里设置为项目的根目录-->
<property name="log.path" value="/logs" />
<!--日志文件名称:这里spring.application.name表示工程名称-->
<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
<!--日志文件名称:这里spring.application.name表示工程名称-->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!--配置控制台(Console)-->
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<!--输出到文件-->
<!-- 时间滚动输出 level为 DEBUG 日志 -->
<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_debug.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 归档的文件名 -->
<fileNamePattern>${log.path}/debug/log-debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>30</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录debug级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 时间滚动输出 level为 INFO 日志 -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_info.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天日志归档路径以及格式 -->
<fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>30</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录info级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 时间滚动输出 level为 ERROR 日志 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_error.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<totalSizeCap>3GB</totalSizeCap>
<maxFileSize>100MB</maxFileSize>
<!--日志文件保留天数-->
<maxHistory>30</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录ERROR级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--Mq消费者日志-->
<appender name="MQ_RECEIVE_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/mqLog/info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${log.path}/mqLog/log-info-%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<MaxHistory>15</MaxHistory>
<maxFileSize>100MB</maxFileSize>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>utf8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<!--控制所有的日志级别-->
<root level="INFO">
<!-- 将当前日志级别输出到哪个追加器上面 -->
<appender-ref ref="CONSOLE"/>
<appender-ref ref="DEBUG_FILE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
<logger name="com.fairy.mbg.mapper" level="DEBUG"/>
<logger name="com.fairy.user" level="ERROR">
<appender-ref ref="ERROR_LOG"/>
</logger>
<logger name="org.springframework.data.mongodb.core" level="DEBUG">
</logger>
<logger name="org.apache.ibatis" level="DEBUG">
</logger>
</configuration>
|