作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第三篇。
要使用Slf4j,需要先创建一个
org.slf4j.Logger
实例,可以使用它的工厂函数
org.slf4j.LoggerFactory.getLogger()
,参数可以是字符串或Class:
public class ExampleService {
private static final Logger log = LoggerFactory.getLogger(ExampleService.class);
private static final Logger log = LoggerFactory.getLogger("com.example.service.ExampleService");
private static final Logger log = LoggerFactory.getLogger("service");
}
这个字符串格式的「实例名字」可以称之为LoggerName,用于在日志实现层区分如何打印日志(见下一篇【3.1 Conversion Word】节)
无论大家对Lombok或褒或贬,但它已经是Java开发的必备依赖了,我个人是推荐使用Lombok的。
Lombok也提供了针对各种日志系统的支持,比如你只需要
@lombok.extern.slf4j.Slf4j
注解就可以得到一个静态的
log
字段,不用再手动调用工厂函数。默认的LoggerName 即是被注解的Class;同时也支持字符串格式的
topic
字段指定LoggerName。
@Slf4j
public class ExampleService {
}
@Slf4j(topic = "service")
public class ExampleService {
}
除了Slf4j,Lombok几乎支持目前市面上所有的日志方案,从接口到实现都没放过。具体明细可以参考Lombok的官方文档
@Log (and friends)[1]
。
通过
org.slf4j.event.Level
我们可以看到一共有五个等级,按优先级从低到高依次为:
-
TRACE
:一般用于记录调用链路,比如方法进入时打印
xxx start;
-
DEBUG
:个人觉得它和 trace 等级可以合并,如果一定要区分,可以用来打印方法的出入参;
-
INFO
:默认级别,一般用于记录代码执行时的关键信息;
-
WARN
:当代码执行遇到预期外场景,但它不影响后续执行时,可以使用;
-
多说一句,Logback额外还有两个级别
ALL
/
OFF
表示完全开启/关闭日志输出,我们记日志时并不涉及。
日志的实现层会决定哪个等级的日志可以输出,这也是我们打日志时需要区分等级的原因,在保证重要的日志不丢失的同时,仅在有需要时才打印用于Debug的日志。
@Slf4j
public class ExampleService {
@Resource
private RpcService rpcService;
public String querySomething
(String request) {
log.trace("querySomething start");
log.debug("querySomething request={}", request);
String response = null;
try {
RpcResult rpcResult = rpcService.call(a);
if (rpcResult.isSuccess()) {
response = rpcResult.getData();
log.info("querySomething rpcService.call succeed, request={}, rpcResult={}", request, rpcResult);
} else {
log.warn("querySomething rpcService.call failed, request={}, rpcResult={}", request, rpcResult);
}
} catch (Exception e) {
log.error("querySomething rpcService.call abnormal, request={}, exception={}", request, e.getMessage(), e);
}
log.debug("querySomething response={}", response);
log.trace("querySomething end");
return response;
}
}
通过
org.slf4j.Logger
我们可以看到有非常多的日志打印接口,不过定义的格式都类似,以
info
为例,一共有两大类:
这个方法有大量的重载,不过使用逻辑是一致的,为了便于说明,我们直接上图:
可以看到,IDEA编辑器对Slf4j API的支持非常好,那些黄底的警告可以让我们马上知道这句日志记录有问题。
虽然使用字符串模板会略有性能损耗(
比较[2]
),但相比于它提供的可读性和便捷性,这个缺点是可以接受的。最终开发者传入的参数,会由日志实现层拼装,并根据配置输出最终结果(请参考下一篇【三、占位符】节)。
通过
isInfoEnabled
方法可以获取当前Logger实例是否开启了对应的日志级别,比如我们可能见过类似这样的代码:
if (log.isInfoEnabled()) {
log.info(...)
}
但其实日志实现层本身就会判断当前Logger实例的输出等级,低于此等级的日志并不会输出,所以一般并不太需要这样的判断。但如果你的输出需要额外消耗资源,那么先判断一下会比较好,比如:
if (log.isInfoEnabled()) {
String resource = rpcService.call();
log.info("resource={}", resource)
Object result = ....;
log.info("result={}", JSON.toJSONString(result));
}
在前边介绍接口时,我们只提到了
log.info()
中填字符串模板及参数的情况,细心的朋友应该发现,还有一些接口多了一个
org.slf4j.Marker
类型的入参,比如:
我们可以通过工厂函数创建 Marker 并使用,比如:
Marker marker = MarkerFactory.getMarker("foobar");
log.info(marker, "test a={}", 1);
这个 Marker 是一个标记,它会传递给日志实现层,由实现层决定 Marker 的处理方式,比如:
-
将Marker通过
%marker
打印出来;
-
使用
MarkerFilter[3]
过滤出(或过滤掉)带有某个Marker的日志,比如把需要Sunfire监控的日志都过滤出来写到一个单独的日志文件;
MDC的全称是Mapped Diagnostic Context,直译为映射调试上下文,说人话就是用来存储扩展字段的地方,而且它是线程安全的。比如
OpenTelemetry[4]
的traceId就会被存到MDC中(见下一篇【五、MDC 中的 traceId】节)。
而且MDC的使用也很简单,就像是一个
Map
实例,常用的方法
put/get/remove/clear
都有,又到了举粟子🌰时间:
MDC.put("key", "value");
String value = MDC.get("key");
MDC.remove("key");
MDC.clear();
Map<String, String> context = MDC.getCopyOfContextMap();
Fluent API也可以直译为「流式 API」,
Slf4j从2.0.x开始支持[5]
,它很像Lombok中
@Builder
提供的能力,即通过链式调用分别设置各个属性,最后再调用
.log()
(就像调用
.build()
那样)完成整个调用。
Marker marker = MarkerFactory.getMarker("foobar"