背景
Java 中比较经常使用的日志框架:html
- log4j(
Log for Java
):Apache 的一个开源项目,七种日志级别:OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE - logback:是一个很成熟的日志框架,其实 logBack 和 log4j 出自一我的之手,这我的就是 Ceki Gülcü。logback 比 log4j 大约快 10 倍、消耗更少的内存,迁移成本也很低,自动压缩日志、支持多样化配置、不须要重启就能够恢复 I/O 异常等优点
- log4j2:做者认为,log4j2已经不只仅是 log4j 的一个升级版本了,而是从头至尾被重写的,这能够认为这其实就是彻底不一样的两个框架
Spring Boot 默认使用 logback,但相比较而言,log4j2 在性能上面会更好。SpringBoot 高版本都再也不支持 log4j,而是支持 log4j2。log4j2,在使用方面与 log4j 基本上没什么区别,比较大的区别是 log4j2 再也不支持 properties 配置文件,支持 xml、json 格式的文件。java
《阿里巴巴Java开发手册》,其中有一条规范作了「强制」要求:git
应用中不可直接使用日志系统(Log4j Logback)中的 API,而应依赖使用日志框架 SLF4J 中的 API,使用日志门面模式的日志框架,有利于维护和各个类的日志处理方式统一。web
Java 简易日志门面(Simple Logging Facade for Java
,缩写 SLF4J),它并非真正的日志框架,他是对全部日志框架制定的一种规范、标准、接口,并非一个框架的具体的实现,由于接口并不能独立使用,须要和具体的日志框架实现配合使用。能够在软件部署的时候决定要使用的 Logging 框架,目前主要支援的有 Java logging API、log4j 及 logback 等框架。spring
理解 SLF4J
接口用于定制规范,能够有多个实现,使用时是面向接口的(导入的包都是 slf4j 的包而不是具体某个日志框架中的包),即直接和接口交互,不直接使用实现,因此能够任意的更换实现而不用更改代码中的日志相关代码。sql
好比:slf4j 定义了一套日志接口,项目中使用的日志框架是logback,开发中调用的全部接口都是 slf4j 的,不直接使用 logback,调用是 本身的工程调用 slf4j 的接口,slf4j 的接口去调用 logback 的实现,能够看到整个过程应用程序并无直接使用 logback,当项目须要更换更加优秀的日志框架时(如log4j2)只须要引入 log4j2 的 jar 和 Llg4j2 对应的配置文件便可,彻底不用更改 Java 代码中的日志相关的代码 logger.info(“xxx”)
,也不用修改日志相关的类的导入的包( import org.slf4j.Logger; import org.slf4j.LoggerFactory;
)shell
总结:使用日志接口便于更换为其余日志框架。apache
One More Thing:上面的这几段话是参考文章中截取的,也让我确实理解了为什么推荐使用 SLF4J 的缘由。这种作法感受就是有点「面向接口编程」的思想,今天也查阅了一些这方面的资料,也让我想起了为什么项目中写 Service 代码时,每每是先写个接口、而后在写个该接口的实现类。待有时间好好研究一些这块的优势!编程
性能分析
!性能评测](https://gitee.com/michael_xiang/images/raw/master/BBsUn0.jpg)json
能够看到在同步日志模式下, Logback的性能是最糟糕的。
log4j2的性能不管在同步日志模式仍是异步日志模式下都是最佳的!那本文就介绍的是使用 log4j2 做为 slf4j 的具体实现。
log4j2 依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <!-- 去掉logback配置 --> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <!-- 引入log4j2依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> </dependencies>
log4j2 使用
import org.slf4j.Logger; import org.slf4j.LoggerFactory; // 这了两种写法都 OK,推荐第一种,不用每次都要修改类名 private static final Logger logger = LoggerFactory.getLogger(this.getClass()); private static final Logger logger = LogManager.getLogger(UserController.class); //... logger.debug("this is debug"); logger.info("this is info");
log4j2 日志级别
从小到大依次是:all、trace、debug、info、warn、error、fatal、off
因为咱们使用的是 slf4j 接口包,该接口包中只提供了未标有删除线的日志级别的输出。
log4j2 配置文件结构
配置文件的主要结构以下:
- Appenders:
- Appender
- Filter
- Layout
- Policies
- Strategy
- Appender
- Loggers
- Logger
- RootLogger
Appender
Appender 能够理解为一个管道,定义了日志内容的去向(保存位置)。
- 配置一个或者多个
Filter
。 - 配置
Layout
来控制日志信息的输出格式。 - 配置
Policies
以控制日志什么时候(When)进行滚动。 - 配置
Strategy
以控制日志如何(How)进行滚动。
注意点:
- 多个
appender
不能指向同一个日志文件,不然会报错:Configuration has multiple incompatible Appenders pointing to the same resource 'logs/mybatis-demo-warn.log'
ImmediateFlush=true
,一旦有新日志写入,立马将日志写入到磁盘的文件中。当日志不少,这种频繁操做文件显然性能很低下immediateFlush
:log4j2 接收到日志事件时,是否当即将日志刷到磁盘。默认为 true。- BufferedIO: 文件流写出是否使用缓冲,true 表示使用,默认值为 false 即不使用缓冲。测试显示,即便在启用immediateFlush 的状况下,设置
bufferedIO=true
也能提升性能。 - 一个 LogConfig 可使用多个 appender,一个 appender 也能够被多个 LogConfig 使用
PatternLayout
这是经常使用的日志格式化类,其它日志格式化类不多用。
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/>
经常使用说明:
%d{HH:mm:ss.SSS} 表示输出到毫秒的时间 %t 输出当前线程名称 %-5level 输出日志级别,-5表示左对齐而且固定输出5个字符,若是不足在右边补0 %logger 输出logger名称,由于Root Logger没有名称,因此没有输出 %msg 日志文本 %n 换行 其余经常使用的占位符有: %F 输出所在的类文件名,如Client.java %L 输出行号 %M 输出所在方法名 %l 输出语句所在的行数, 包括类名、方法名、文件名、行数
关于 pattern 的格式点击 官宣——Pattern Layout
Filter
Filters 决定日志事件可否被输出。过滤条件有三个值:ACCEPT(接受)
,DENY(拒绝)
,NEUTRAL(中立)
。
经常使用的 Filter 实现类有:
- LevelRangeFilter
- TimeFilter
- ThresholdFilter
简单说就是 log4j2 中的过滤器 ACCEPT
和 DENY
以后,后续的过滤器就不会执行了,只有在 NEUTRAL
的时候才会执行后续的过滤器
<Console name="Console"> <!-- 设置 onMismatch="NEUTRAL" 可让日志通过后续的过滤器 最后一个过滤器建议设置 onMismatch="DENY", 否则日志就输出了。 --> <Filters> <!-- 从大到小:error, warn, info, debug, trace --> <LevelRangeFilter minLevel="error" maxLevel="info" onMatch="ACCEPT" onMismatch="NEUTRAL" /> <!-- 只容许在天天的 8点~8点半 之间输出日志 --> <TimeFilter start="08:00:00" end="08:30:00" onMatch="ACCEPT" onMismatch="DENY" /> </Filters> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/> </Console>
LevelRangeFilter 对它们进行了 ACCEPT
,而剩下的 trace Msg
和 debug Msg
则会通过下一个过滤器。
Policy
Policy & Strategy
- Policy 是用来控制日志文件什么时候(When)进行 Rolling/滚动的;
- Strategy 是用来控制日志文件如何(How)进行 Rolling/滚动的。
所谓「日志滚动」就是当达到设定的条件后,日志文件进行切分。好比:工程师想让系统中的日志按日进行切分,而且按月归档。
Rolling
的意思是当知足必定条件后,就重命名原日志文件用于备份,并从新生成一个新的日志文件。例如需求是天天生成一个日志文件,可是若是一天内的日志文件体积已经超过 1G,就从新生成。两个条件知足一个便可。
Policy经常使用的实现类:
SizeBasedTriggeringPolicy
,根据日志文件的大小进行滚动。单位有:KB
,MB
,GB
CronTriggeringPolicy
,使用 Cron 表达式进行日志滚动,很灵活TimeBasedTriggeringPolicy
,这个配置须要和filePattern
结合使用,注意filePattern
中配置的文件重命名规则。滚动策略依赖于filePattern
中配置的最具体的时间单位,根据最具体的时间单位进行滚动。这种方式比较简洁。CronTriggeringPolicy
策略更强大
在 TimeBasedTriggeringPolicy
标签中加上了 modulate
属性并设置为 true
,该属性的意思是是否对日志生成时间进行调制。若为 true
,则日志时间将以 0 点为边界进行偏移计算。例如第一第二天志保存时间是 3 点,modulate
为 true
,interval
是 4h
。那么下次生成日志时间是 4点,08:00,12:00……。
<Appenders> <RollingRandomAccessFile name="File" fileName="logs/app.log" filePattern="logs/$${date:hh-mm}/%d{hh-mm-ss}.app.%i.log" > <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/> <Policies> <!-- 每 5s 翻滚一次 --> <!--<CronTriggeringPolicy schedule="0/5 * * * * ?" />--> <!-- filePattern中最具体的时间单位是 秒。 这里用 TimeBasedTriggeringPolicy 替换 CronTriggeringPolicy 注意:modulate属性是指从启动时间开始算5秒,仍是从0秒开始算5秒,运行一下就明白了。 modulate: true(默认值) // 会从启动时间开始算 5秒 modulate: false // 从 0秒开始算 --> <TimeBasedTriggeringPolicy interval="5" modulate="true"/> <SizeBasedTriggeringPolicy size="10 MB"/> </Policies> <DefaultRolloverStrategy max="10" /> </RollingRandomAccessFile> </Appenders>
Strategy
Strategy经常使用的实现类:
- DefaultRolloverStrategy
- DirectWriteRolloverStrategy
这两个 Strategy 都是控制如何进行日志滚动的。
DefaultRolloverStrategy
默认的 max
为 7。
<DefaultRolloverStrategy max="7"/>
max 参数指定了计数器的最大值。一旦计数器达到了最大值,过旧的文件将被删除。
注意:不要认为 max 参数是须要保留的日志文件的最大数目。
max 参数是与 filePattern
中的计数器 %i
配合起做用的,其具体做用方式与 filePattern
的配置密切相关。
1.若是filePattern中仅含有date/time pattern,每次rollover时,将用当前的日期和时间替换文件中的日期格式对文件进行重命名。max参数将不起做用。
如,filePattern="logs/app-%d{yyyy-MM-dd}.log"
2.若是 filePattern
中仅含有整数计数器(即%i
),每次 rollover
时,文件重命名时的计数器将每次加1(初始值为1),若达到 max 的值,将删除旧的文件。
如,filePattern="logs/app-%i.log"
3.若是 filePattern
中既含有 date/time pattern
,又含有 %i
,每次 rollover
时,计数器将每次加 1,若达到 max 的值,将删除旧的文件,直到 data/time pattern
再也不符合,被替换为当前的日期和时间,计数器再从1开始。
如,filePattern="logs/app-%d{yyyy-MM-dd HH-mm}-%i.log"
Appender 类型
FileAppender(File)、RandomAccessFileAppender(RandomAccessFile)
- 相同点:写入日志信息到文件
- 不一样点:使用的
I/O
实现类不一样,前者使用FileOutputStream
,后者使用RandomAccessFile
。
官方文档说是在 bufferedIO=true
(默认是 true
)的状况下,性能提高 20% ~ 200%
。
经常使用属性:
fileName
:来指定文件位置,文件或目录不存在则会自动建立。immediateFlush
:是否每次写入都要马上刷新到硬盘中。默认true
,若是使用默认值可能会影响性能。
RollingFileAppender(RollingFile)、RollingRandomAccessFileAppender(RollingRandomAccessFile)
- 相同点:写入日志信息到文件
- 不一样点:使用的
I/O
实现类不一样,前者使用FileOutputStream
,后者使用RandomAccessFile
。 - 上一对的实现类不能进行「日志滚动」,而带有
rolling
字样的 appender 就能够实现「滚动」功能。有「滚动」,会判断是否知足封存文件的要求,执行日志存档操做。
RollingRandomAccessFile Appender,相比 RollingFileAppender有很大的性能提高,官网宣称是20-200%
<RollingRandomAccessFile name="File" fileName="logs/app.log" filePattern="logs/$${date:hh-mm}/%d{hh-mm-ss}.app.%i.log" >
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/>
<Policies>
<!-- 每 5s 翻滚一次 -->
<CronTriggeringPolicy schedule="0/5 * * * * ?" />
<SizeBasedTriggeringPolicy size="10 MB"/>
</Policies>
<DefaultRolloverStrategy max="10" />
</RollingRandomAccessFile>
</Appenders>
经常使用属性:
filePattern
:指定当发生Rolling时,文件的转移和重命名规则。至于其中的$${date:hh-mm}
是表示了一个文件夹(以小时-分钟
)命名。DefaultRolloverStrategy
指定了如何(How)进行翻滚,而且指定了最大翻滚次数(影响%i参数值),超过次数以后会按照相应的规则删除旧日志。Policies
: 这里就是规定了什么时候进行滚动(When),能够有多个Policy。CronTriggeringPolicy
,好比设置每 5s 进行一次翻滚SizeBasedTriggeringPolicy
指定当文件体积大于size指定的值时,触发Rolling。例如,若是当前文件超过了 10MB,可是文件的名字尚未进行翻滚(创建新文件),那么就会用%i的方式进行翻滚。
若是配置的是 RollingFile
或 RollingRandomAccessFile
,则必须配置一个 Policy
。
翻滚理解
假设计数器次数设为2次 <DefaultRolloverStrategy max="2" />
,filePattern
中既含有 date/time pattern
,又含有 %i
。
当知足翻滚触发条件时(时间间隔到了 OR 文件大小超了),就会启动 Rolling
:
app.log
第一次翻滚:app.log app.1.log // app.log -> app.1.log
第二次翻滚:app.log app.1.log app.2.lop // app.log -> app.2.log
一个循环结束,到达了最大保存数 2 了,那么,app1.log
会被删除,下一个 app3.log
就会覆盖 app2.log
,app2.log
会更名为app1.log
第三次翻滚:app.log app.2.lop app.3.lop // app.log -> app.3.log
第四次翻滚:app.log app.3.lop app.4.lop // app.log -> app.4.log
理解:编号最近的一次也就是最新的一次 log,而采起了 Policy
方式的日志,fileName
中保存的日志将不会是全量的日志,而是根据你 Policy
的条件切分后的最近一次的日志内容。
一个 Appender 示例
按月归档日志,按日进行切分,限制单文件大小为 500MB, 一天最多生成20个文件,也就是(20 * 500)MB大小的日志
<?xml version="1.0" encoding="UTF-8"?>
<Configuration name="baseConf" status="warn" monitorInterval="30">
<Appenders>
<RollingRandomAccessFile name="File" fileName="logs/app.log" filePattern="logs/$${date:yyyy-MM}/%d{yyyy-MM-dd}.app.%i.log" >
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="false"/>
<SizeBasedTriggeringPolicy size="500MB"/>
</Policies>
<DefaultRolloverStrategy max="20" />
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>
Logger
简单说 Logger 就是一个路由器,指定类、包中的日志信息流向哪一个管道,以及控制他们的流量(日志级别)
Logger 部分为两个 Logger:
- RootLogger(必须配置)
- Logger
注意:Logger 中也能够加过滤器的!
日志重复打印问题
若是 Root 中的日志包含了 Logger 中的日志信息,而且 AppenderRef 是同样的配置,则日志会打印两次。
这时候咱们须要使用一个 Logger 的属性来解决,那就是 additivity
,其默认值为 true
,须要配置为false
<?xml version="1.0" encoding="UTF-8"?> <Configuration name="baseConf" status="warn" monitorInterval="30"> <Appenders> <Console name="Console"> <PatternLayout> <Pattern>%d %p %c{1.} [%t] %m%n</Pattern> </PatternLayout> </Console> </Appenders> <Loggers> <Logger name="me.master.snail.log.LogMain" level="info" additivity="false"> <AppenderRef ref="Console"/> </Logger> <Root level="trace"> <AppenderRef ref="Console"/> <Filters> <LevelRangeFilter minLevel="error" maxLevel="info" onMatch="ACCEPT" onMismatch="DENY" /> </Filters> </Root> </Loggers> </Configuration>
- RootLogger 只能有 1 个,普通的 Logger 能够定义多个,能够细致到给某个类定义;
- 多个 Logger 配置重复了,在日志文件中会重复;
- 每个 Logger 对应的 name 是包路径,表示在 name 包下的类使用 AppenderRef 指向的日志模板来输出日志;
- 不一样的 LogConfig 之间实际上是有继承关系的,子 LogConfig 会继承 parent 的属性,而全部 LogConfig 都继承自 Root LogConfig。因此即便只配置了 root logger,你同样能够在任何地方经过
LoggerFactory.getLogger
获取一个 logger 对象,记录日志; - 先配置一个 Root,让全部须要使用日志的 logger 继承,而后对有特别须要的 logger 进行特殊的配置,好比咱们但愿
org.springframework
包只记录error
以及warn
级别的 log,再好比,咱们但愿能显示mybatis 执行的 sql 的日志,均可以进行个性化的配置;
Logger 等级实验
<logger name="org.springframework" level="INFO" additivity="true"> <AppenderRef ref="InfoLog"/> </logger> <Root level="ERROR" additivity="true"> <AppenderRef ref="Console"/> <AppenderRef ref="InfoLog"/> <AppenderRef ref="WarnLog"/> <AppenderRef ref="ErrorLog"/> </Root>
- ROOT 等级设为
ERROR
时,org.springframework
Logger 等级设为OFF
时,发现原来的warn.log
和info.log
文件中,都只有级别大于或等于 ERROR 的日志信息了; - ROOT 等级设为
ERROR
时,org.springframework
Logger 等级设为INFO
时,发现info.log
文件中,增长了org.springframework
包的相关INFO
级别的日志信息了;
总结:
- Logger 日志等级和 appender 日志等级的关系:logger 日志等级和 appender 日志等级,谁「高」听谁的;
- 普通 Logger 的优先级高