前言
日志对于项目的重要性不言而喻,现在市面上的日志框架多种多样:Log4j、Log4j2、Slf4j、JDKLog、Logback 等等。Log4j 目前已经停止更新。Apache 推出了新的 Log4j2 代替 Log4j,Log4j2 是 Log4j 的升级,与其前身Log4j 相比有了显着的改进,并提供了许多 Logback 可用的改进,因此 Log4j2 + Slf4j 应该是未来的大势所趋。
日志作用
在项目开发中,都不可避免的使用到日志。没有日志虽然不会影响项目的正确运行,但是没有日志的项目可以说是不完整的。日志在调试,错误或者异常定位,数据分析中的作用是不言而喻的。
- 调试
在Java项目调试时,查看栈信息可以方便地知道当前程序的运行状态,输出的日志便于记录程序在之前的运行结果。如果你大量使用 System.out 或者 System.err,这是一种最方便最有效的方法,但显得不够专业。
- 错误定位
项目在运行一段时候后,可能由于数据问题,网络问题,内存问题等出现异常。这时日志可以帮助开发或者运维人员快速定位错误位置,提出解决方案。
- 数据分析
日志中蕴含了大量的用户数据,包括点击行为,兴趣偏好等,用户画像对于公司下一步的战略方向有一定指引作用。
日志级别
Log4j2中日志有六个级别(level):
- trace:追踪,是最低的日志级别,相当于追踪程序的执行,一般不怎么使用
- debug:调试,一般在开发中,都将其设置为最低的日志级别
- info:信息,输出重要的信息,使用较多
- warn:警告,有些时候,虽然程序不会报错,但是还是需要告诉程序员的
- error:错误,这个在开发中也挺常用的
- fatal:严重错误,这个一旦发生,程序基本上也要停止了
当日志级别设置为某个值的时候,低于它的日志信息将不会被记录,只有高于设置的级别的信息会被记录。
Spring Boot 集成 log4j2
- pom.xml
Spring Boot默认使用LogBack,但是我们没有看到显示依赖的jar包,其实是因为所在的jar包spring-boot-starter-logging都是作为spring-boot-starter-web或者spring-boot-starter依赖的一部分。
如果这里要使用Log4j2,需要从spring-boot-starter-web中去掉spring-boot-starter-logging依赖,同时显示声明使用Log4j2的依赖jar包,具体如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<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>
- log4j2.xml
在application.properties中指定log4j2.xml的位置
logging.config=classpath:log4j2.xml
logging.level.root=info
log4j2.xml 示例
<?xml version="1.0" encoding="UTF-8" ?>
<!--status的值有trace、debug、info、warn、error和fatal,用于控制log4j2日志框架本身的日志级别,
monitorInterval,含义是每隔多少秒重新读取配置文件,可以不重启应用的情况下修改配置-->
<configuration status="warn" monitorInterval="5">
<!-- 集中配置属性进行管理,使用时通过 ${name} 使用 -->
<properties>
<property name="LOG_HOME">D:/logs</property>
</properties>
<!-- 日志处理 -->
<Appenders>
<!-- 控制台输出 appender,SYSTEM_OUT输出黑色,SYSTEM_ERR输出红色 -->
<Console name="Console" target="SYSTEM_OUT">
<!-- 日志消息格式 -->
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n"/>
</Console>
<!-- 日志文件输出 appender -->
<File name="file" fileName="${LOG_HOME}/myfile.log">
<!-- 日志消息格式 -->
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n"/>
</File>
<!-- 使用随机读写流的日志文件输出 appender,性能提高 -->
<RandomAccessFile name="accessFile" fileName="${LOG_HOME}/myAcclog.log">
<!-- 控制台或文件输出源 -->
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n"/>
</RandomAccessFile>
<!-- 按照一定规则拆分的日志文件的appender -->
<!-- 拆分后的文件 filePattern="${LOG_HOME}/$${date:yyyy-MM-dd}/myrollog-%d{yyyy-MM-dd-HH-mm}-%i.log"> -->
<RollingFile name="rollingFile" fileName="${LOG_HOME}/myrollog.log"
filePattern="D:/logs/$${date:yyyy-MM-dd}/myrollog-%d{yyyy-MM-dd-HH-mm}-%i.log">
<!-- 日志级别过滤器 -->
<ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
<!-- 日志消息格式 -->
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %msg%n"/>
<Policies>
<!-- 在系统启动时,出发拆分规则,生产一个新的日志文件 -->
<OnStartupTriggeringPolicy/>
<!-- 按照文件大小拆分,10MB -->
<SizeBasedTriggeringPolicy size="10 MB"/>
<!-- 按照时间节点拆分,规则根据filePattern定义的 -->
<TimeBasedTriggeringPolicy/>
</Policies>
<!-- 在同一个目录下,文件的个限定为 30个,超过进行覆盖 -->
<DefaultRolloverStrategy max="30"/>
</RollingFile>
<!-- 异步 appender -->
<Async name="Async">
<AppenderRef ref="file"/>
</Async>
</Appenders>
<!-- logger 定义 -->
<Loggers>
<!-- 使用 rootLogger 配置 -->
<!-- 总是存在一个rootLogger,即使没有显示配置也是存在的,并且默认输出级别为DEBUG,所有其他的Logger都默认继承自rootLogger -->
<Root level="info">
<!-- 指定日志使用的处理器 -->
<AppenderRef ref="Console"/>
<AppenderRef ref="file"/>
<AppenderRef ref="rollingFile"/>
<AppenderRef ref="accessFile"/>
<AppenderRef ref="Async"/>
</Root>
<!-- 自定义的Logger(子Loggger)继承自rootLogger -->
<Logger name="com.example.canal.yang" level="info" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
</Loggers>
</configuration>
PatternLayout 各标记符详细含义如下:
%d 输出时间
%d{HH:mm:ss.SSS} 表示输出到毫秒的时间
%t 输出当前线程名称
%p 输出日志级别
%-5p -5表示左对齐并且固定输出5个字符,如果不足在右边补0
%c 日志消息所在类名
%m 消息内容
%n 换行
%F 输出所在的类文件名,如Log4j2Test.java
%L 输出行号
%M 输出所在方法名
%l 输出语句所在的行数, 包括类名、方法名、文件名、行数
%logger 输出logger名称,如果没有名称,就不输出
- 打印日志
只使用log4j2的话,获取Logger对象一般是使用LogManager去获取的,如果使用slf4j的话,使用LoggerFactory去获取
public static final Logger LOGGER = LogManager.getLogger(Log4j2Test.class);
private final static org.slf4j.Logger logger = LoggerFactory.getLogger(Log4j2Controller.class);
LOGGER.trace("trace level");
LOGGER.debug("debug level");
logger.info("info level");
logger.warn("warn level");
logger.error("error level");
使用lombok插件(详细安装方法请上网查询),可以简化Logger对象的获取,首先在相关的类上面添加@Slf4j注解(该注解由lombok提供),然后通过下面的方式打印日志:
log.trace("trace level");
log.debug("debug level");
log.info("info level");
log.warn("warn level");
log.error("error level");
日志重复打印问题
如果Root中的日志包含了Logger中的日志信息,并且AppenderRef是一样的配置,则日志会打印两次。
这是 log4j2 继承机制问题,在Log4j2中,logger 是有继承关系的,root是根节点,在log4j2中,有个 additivity 的属性,它是子 Logger 是否继承 父 Logger 的 输出源(appender) 的属性。具体说,默认情况下子 Logger 会继承父 Logger 的appender,也就是说子 Logger 会在父 Logger 的 appender 里输出。若是 additivity 设为 false,则子 Logger 只会在自己的 appender 里输出,而不会在父 Logger 的 appender 里输出。
要打破这种传递性,也非常简单,在 logger 中添加 additivity = “false”。