Java 日志通关(三) - Slf4j 介绍
阿里妹导读
作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次 Java 日志的分享,本文是整理出的系列文章第三篇。
一、创建 Logger 实例
1.1 工厂函数
要使用 Slf4j,需要先创建一个 org.slf4j.Logger 实例,可以使用它的工厂函数 org.slf4j.LoggerFactory.getLogger(),参数可以是字符串或 Class:
- 如果是字符串,这个字符串会作为返回 Logger 实例的名字;
- 如果是 Class,会调用它的 getName() 获取 Class 的全路径,作为 Logger 实例的名字;
public class ExampleService {
// 传 Class,一般都是传当前的 Class
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】节)
1.2 Lombok
无论大家对 Lombok 或褒或贬,但它已经是 Java 开发的必备依赖了,我个人是推荐使用 Lombok 的。
Lombok 也提供了针对各种日志系统的支持,比如你只需要 @lombok.extern.slf4j.Slf4j 注解就可以得到一个静态的 log 字段,不用再手动调用工厂函数。默认的 LoggerName 即是被注解的 Class;同时也支持字符串格式的 topic 字段指定 LoggerName。
@Slf4j
public class ExampleService {
// 注解 @Slf4j 会帮你生成下边这行代码
// private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ExampleService.class);
}
@Slf4j(topic = "service")
public class ExampleService {
// 注解 @Slf4j(topic = "service") 会帮你自动生成下边这行代码
// private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger("service");
}
除了 Slf4j,Lombok 几乎支持目前市面上所有的日志方案,从接口到实现都没放过。具体明细可以参考 Lombok 的官方文档 @Log (and friends)[1]。
二、日志级别
通过 org.slf4j.event.Level 我们可以看到一共有五个等级,按优先级从低到高依次为:
- TRACE:一般用于记录调用链路,比如方法进入时打印 xxx start;
- DEBUG:个人觉得它和 trace 等级可以合并,如果一定要区分,可以用来打印方法的出入参;
- INFO:默认级别,一般用于记录代码执行时的关键信息;
- WARN:当代码执行遇到预期外场景,但它不影响后续执行时,可以使用;
- ERROR:出现异常,以及代码无法兜底时使用;
多说一句,Logback 额外还有两个级别 ALL/OFF 表示完全开启 / 关闭日志输出,我们记日志时并不涉及。
日志的实现层会决定哪个等级的日志可以输出,这也是我们打日志时需要区分等级的原因,在保证重要的日志不丢失的同时,仅在有需要时才打印用于 Debug 的日志。
上边的解释比较抽象,来个栗子🌰:
@Slf4j
public class ExampleService {
@Resource
private RpcService rpcService;
public String querySomething(String request) {
// 使用 trace 标识这个方法调用情况
log.trace("querySomething start");
// 使用 debug 记录出入参
log.debug("querySomething request={}", request);
String response = null;
try {
RpcResult rpcResult = rpcService.call(a);
if (rpcResult.isSuccess()) {
response = rpcResult.getData();
// 使用 info 标识重要节点
log.info("querySomething rpcService.call succeed, request={}, rpcResult={}", request, rpcResult);
} else {
// 使用 warn 标识程序调用有预期外错误,但这个错误在可控范围内
log.warn("querySomething rpcService.call failed, request={}, rpcResult={}", request, rpcResult);
}
} catch (Exception e) {
// 使用 error 记录程序的异常信息
log.error("querySomething rpcService.call abnormal, request={}, exception={}", request, e.getMessage(), e);
}
// 使用 debug 记录出入参
log.debug("querySomething response={}", response);
// 使用 trace 标识这个方法调用情况
log.trace("querySomething end");
return response;
}
}
三、打印接口
通过 org.slf4j.Logger 我们可以看到有非常多的日志打印接口,不过定义的格式都类似,以 info 为例,一共有两大类:
- public boolean info(...);
- public boolean isInfoEnabled(...);
3.1 info 方法
这个方法有大量的重载,不过使用逻辑是一致的,为了便于说明,我们直接上图:
可以看到,IDEA 编辑器对 Slf4j API 的支持非常好,那些黄底的警告可以让我们马上知道这句日志记录有问题。
虽然使用字符串模板会略有性能损耗(比较 [2]),但相比于它提供的可读性和便捷性,这个缺点是可以接受的。最终开发者传入的参数,会由日志实现层拼装,并根据配置输出最终结果(请参考下一篇【三、占位符】节)。
3.2 isInfoEnabled 方法
通过 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));
}
Marker marker = MarkerFactory.getMarker("foobar");
log.info(marker, "test a={}", 1);
在前边介绍接口时,我们只提到了 log.info() 中填字符串模板及参数的情况,细心的朋友应该发现,还有一些接口多了一个 org.slf4j.Marker 类型的入参,比如:
- log.info(Marker, ...)
我们可以通过工厂函数创建 Marker 并使用,比如:
// 和 Map<String, String> 相似的接口定义
MDC.put("key", "value");
String value = MDC.get("key");
MDC.remove("key");
MDC.clear();
// 获取 MDC 中的所有内容
Map<String, String> context = MDC.getCopyOfContextMap();
这个 Marker 是一个标记,它会传递给日志实现层,由实现层决定 Marker 的处理方式,比如:
- 将 Marker 通过 %marker 打印出来;
- 使用 MarkerFilter[3] 过滤出(或过滤掉)带有某个 Marker 的日志,比如把需要 Sunfire 监控的日志都过滤出来写到一个单独的日志文件;
五、MDC
MDC 的全称是 Mapped Diagnostic Context,直译为映射调试上下文,说人话就是用来存储扩展字段的地方,而且它是线程安全的。比如 OpenTelemetry[4] 的 traceId 就会被存到 MDC 中(见下一篇【五、MDC 中的 traceId】节)。
而且 MDC 的使用也很简单,就像是一个 Map<String, String> 实例,常用的方法 put/get/remove/clear 都有,又到了举粟子🌰时间:
Marker marker = MarkerFactory.getMarker("foobar");
Exception e = new RuntimeException();
// == 以下几个示例的最终效果是完全一致的 ==
// 这是传统的调用方式
log.info(market, "request a={}, b={}", 1, 2, e);
// Fluent API 例1
log.atInfo() // 表示这是 INFO 级别。你猜对了,还有 atTrace/atDebug/atWarn/atError
.addMarker(marker)
.log("request a={}, b={}", 1, 2, e); // 与传统 API 很像
// Fluent API 例2
log.atInfo()
.addMarker(marker)
.setCause(e)
.setMessage("request a={}, b={}") // 传字符串模板
.setMessage(() -> "request a={}, b={}") // setMessage 支持传入 Supplier
.addArgument(1) // 添加与字符串模板中占们符所对应的值
.addArgument(() -> 2) // addArgument 支持传入 Supplier
.log(); // 大火收汁
// == addKeyValue 的输出格式依赖日志实现层的配置,默认格式与上边示例不同 ==
// Fluent API 例3
log.atInfo()
.setMessage("request") // 注意这里没有占位符
.setKeyValue("a", 1) // 通过 setKeyValue 添加关心的变量
.setKeyValue("b", () -> 2) // value 支持传入 Supplier
.log();
// 通过 setKeyValue 设置的值默认会放在 message 前边,比如上边这个例子,默认会输出:
// a=1 b=2 request
六、Fluent API (链式调用)
Fluent API 也可以直译为「流式 API」, Slf4j 从 2.0.x 开始支持 [5],它很像 Lombok 中 @Builder 提供的能力,即通过链式调用分别设置各个属性,最后再调用.log()(就像调用.build() 那样)完成整个调用。
举个例子:
log.info("request a={}", () -> a);
总结一下:
- 所有 add 前缀的方法,都支持设置多个,比如 addMarker/addArgument/addKeyValue。所以在 Fluent API 中是支持给一条日志添加多个 Marker 的,而传统 API 不可以。
- 所有 set 前缀的方法,对应的值都只有一个,比如 setMessage/setCause,虽然你可以多次调用,但只有最后一次会生效。
在上边的示例中传统 API 看起来更简洁。但如果日志中占位符很多,那用 Fluent API,特别是使用其中的 addKeyValue 就很有优势。
不过目前 IDEA 编辑器对流式 API 的支持还不太好,无法支持占位符与参数不匹配的情况:
顺便说一下,相比 Slf4j,更晚推出的 Log4j 2 在传统 API 中也支持通过传入 Supplier 惰性求值,就像这样:
log.info("request a={}", () -> a);
七、后记
以上只是简单介绍了 Slf4j 的常用功能,如需进一步了解可以参考官方文档 SLF4J user manual[6]。
参考链接:
[1]https://projectlombok.org/features/log
[2]https://juejin.cn/post/6915015034565951501
[3]https://logback.qos.ch/apidocs/ch/qos/logback/classic/turbo/MarkerFilter.html
[4]https://www.aliyun.com/product/xtrace**
**
[5]https://www.slf4j.org/manual.html#fluent
[6]https://www.slf4j.org/manual.htm
点击查看《Java 日志通关(一) - 前世今生》
点击查看《Java 日志通关(二) - Slf4j+Logback 整合及排包》
全文完
本文由 简悦 SimpRead 优化,用以提升阅读体验
使用了 全新的简悦词法分析引擎 beta,点击查看详细说明