Java生态中,记录日志有JDK自带的Logger
,除此之外十分常用的第三方日志模块有Log4j
,Log4j2
,Logback
,commons-logging
等。
Logback
是一个功能丰富的日志库,而且性能不错,是SpringBoot默认集成的日志模块,实际上Logback
、Log4j
等许多日志库实现了SLF4J
的API,我们代码中使用Logger
等类时,你会发现引入的包都是SLF4J
的,它们之间的关系就像接口和实现类一样,使用SLF4J
的API而不是直接调用某个日志库的接口也是最为推荐的方式。
这里我们简单介绍一下Logback的使用。
我们直接将slf4j
和logback
的依赖引入项目即可。
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
实际开发中我们用到的某些库可能依赖一个特定的日志模块,这是相当糟糕的设计,比如ZooKeeper的客户端包就自行依赖了log4j
(至少我下面用到的版本是这样的),而我们整个项目都是Logback
的,我们不能让这些模块自己搞特殊,好在slf4j
提供了bridge
包能够解决这个问题。
以ZooKeeper为例,我们通过依赖分析,发现它自带了log4j
和slf4j
的绑定包,我们把这几个依赖直接排除。
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.10</version>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
然后引入bridge
兼容包。
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.25</version>
</dependency>
之后再手动引入slf4j-api
,logback
等依赖即可。
Logback有五个日志级别:严重性TRACE
<DEBUG
<INFO
<WARN
<ERROR
。
使用Logback非常简单,首先我们要获得Logger对象,然后在其上调用debug()
、error()
等方法即可,和JDK内置的日志工具使用起来是一样的。
package com.gacfox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(Main.class);
logger.debug("hello");
}
}
输出结果如下。
16:32:37.233 [main] DEBUG com.gacfox.Main - hello
我们输出的日志可能带有参数,如果使用+
进行字符串拼接,不仅代码可读性差而且可能有性能问题,这种时候我们可以使用拼接日志参数功能。
String name = "Tom";
String age = "18";
logger.debug("My name is {}, I am {} years old", name, age);
输出结果如下。
16:42:38.772 [main] DEBUG com.gacfox.Main - My name is Tom, I am 18 years old
如果不使用日志框架,我们的异常信息一般都是通过e.printStackTrace()
打印到控制台的。日志框架中则写法例子如下
try {
throw new RuntimeException("Code run failed here...");
} catch (Exception e) {
logger.error("--e:", e);
}
logger.error()
方法有针对Throwable
类型的重载,我们不需要手动编写{}
占位符等操作。
Logback支持XML或groovy脚本格式的配置文件,我们一般使用XML,它默认应放置在classpath:logback.xml
,如果不存在任何配置文件,Logback默认会加载一些默认配置,下面是一个配置文件例子。
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<encoder charset="UTF-8">
<pattern>%yellow(%date) %highlight(%-5level) %cyan(%logger{5}@[%-4.30thread]) - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="stdout"/>
</root>
</configuration>
上面配置的配置和默认差不多,只不过修改了输出格式,加上了颜色输出(需要终端支持)。
<appender>
指定日志输出到哪里,以什么格式输出。上面例子中,我们把日志输出到了终端(标准输出)上。我们也可以把日志输出到文件中,下面例子我们把日志输出到文件里,同时使用基于时间的滚动策略。
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 可选:过滤器,这里用LevelFilter配置只记录ERROR级别的日志 -->
<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>E:/%d{yyyy-MM-dd}/error-log.log</fileNamePattern>
</rollingPolicy>
<encoder charset="UTF-8">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
除了基于时间的滚动策略,另一种非常常用的是基于文件大小的滚动策略。
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 过滤器,只记录ERROR级别的日志 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<file>E:/error.log</file>
<!--基于文件大小的滚动策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>E:/error-%i.log.zip</fileNamePattern>
<!--窗口大小为1-3,当归档文件数量大于3时,会向后覆盖旧的日志,默认为7-->
<!--<minIndex>1</minIndex>-->
<!--<maxIndex>3</maxIndex>-->
</rollingPolicy>
<!--设置日志滚动更新的触发策略-->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>5MB</maxFileSize>
</triggeringPolicy>
<encoder charset="UTF-8">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
这样配置后,当error.log
中的内容大于5MB时,就会归档压缩为一个类似error-1.log.zip
的形式并清空error.log
。注意:窗口大小是有默认值7的,也就是说默认情况下,最多出现error-7.log.zip
,更新的归档会向后覆盖。
<appender>
中的<pattern>
用于指定日志的格式,格式模板配置包括文本颜色和占位符变量。
%yellow
、%blue
等用于指定颜色,这里不多做介绍。注意%hightlight
是比较特殊的,它代表高亮色,一般用于指定到%level
变量上,使得不同的日志级别有不同的颜色。
%date{yyyy-MM-dd HH:mm:ss}
配置日志的输出时间,大括号内为日志的格式化格式,%date
也可以写作%d
。
%logger
指定产生日志的类的全路径,我们可以使用类似%logger{5}
的形式配置,5
是一个路径缩写字符数的指导值,配置后输出以类似c.z.h.p.HikariPool
形式缩写。
%thread
为线程名。
%level
为日志级别。
%msg
为日志中具体输出的内容。
%n
为换行符,因为不同操作系统可能使用不同的换行符,如果使用\n
或\r\n
可能造成日志格式错乱,因此我们需要以%n
换行。
此外还有类似%-5level
的写法,-
代表左对齐,5
代表此处最小宽度为5。
<logger>
用于单独设置某个包或某个类的日志输出级别,以及它使用的<appender>
。下面是一个例子:
<logger name="com.gacfox.demo.MainController" level="DEBUG" additivity="false">
<appender-ref ref="stdout" />
</logger>
name
指定了一个类全名或包名,日志配置会应用到对应类或包上additivity
表示子Logger是否继承根Logger配置:true
表示继承,根Logger和子Logger配置的所有appender都会生效,false
表示只有子Logger配置的appender生效。<root>
和<logger>
相似,只不过它配置的是一个根Logger。
默认情况下异步是以同步方式写入的,日志的IO操作本身也会消耗时间,因此同步方式记录日志在一些高并发的系统中是无法满足要求的。Logback内置了异步日志输出组件,我们可以直接使用。
<appender name="async-file" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="file" />
</appender>
Logback的AsyncAppender
内部使用了默认大小为256的BlockingQueue
队列实现异步功能。代码中,我们创建了一个名为async-file
的appender组件,内部使用<appender-ref>
标签关联了另一个写入文件的RollingFileAppender
。
Mapped Diagnostic Contexts(MDC)中文译为映射诊断上下文,不要被这个专业名词吓到,说白了就是把一个链路跟踪ID(traceId)写到日志文本里。我们知道Tomcat等应用容器都是通过多线程处理用户请求的,链路跟踪可以实现在微服务环境下记录请求的整个链路,而MDC可以将请求链路所有节点的日志串联起来。SLF4J
提供了一个静态类MDC
,我们可以调用MDC.put()
方法向线程局部变量写入数据,然后配置日志输出显示该值。
这里我们直接看一个Spring工程的例子。
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<encoder charset="UTF-8">
<pattern>%yellow(%date) %highlight(%-5level) %cyan(%logger{5}@[%-4.30thread]) %cyan(%X{traceId}) - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.springframework" level="info" additivity="false">
<appender-ref ref="stdout"/>
</logger>
<root level="debug">
<appender-ref ref="stdout"/>
</root>
</configuration>
logback.xml
中,有一个比较特殊的配置%X{traceId}
,其中%X
表示取MDC中的变量,traceId
是我们放入MDC的键。
MdcInterceptor.java
package com.gacfox.interceptor;
import org.slf4j.MDC;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MdcInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String traceId = request.getHeader("traceId");
MDC.put("traceId", traceId);
return true;
}
}
上面代码是一个SpringMVC拦截器,这个拦截器配配置为了拦截所有请求,其代码逻辑非常简单,就是从HTTP请求头中取出traceId
并放入MDC。
此时我们如果在请求头中加入traceId
,就可以在日志中看到其输出了。