参数化日志记录
高效格式化消息
参数化日志记录 是 CoddyKit 上的免费 Java Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Java Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Java Academy 课程共包含 4 节课。
字符串拼接的问题
拼接日志消息时,即使该级别已被禁用,也会在调用之前构造完整字符串。
log.debug("id=" + id + " name=" + name) 会在 DEBUG 关闭时浪费 CPU,因为无论如何都会构造字符串。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
private static final Logger log = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
String name = "Ada"; int id = 7;
// Bad: string is built even if DEBUG is disabled
log.debug("id=" + id + " name=" + name);
}
}使用 {} 占位符
SLF4J 使用 {} 作为占位符。请将值作为额外参数传入。
只有启用该级别时才会组装消息,因此被禁用的日志几乎不会产生开销。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
private static final Logger log = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
String name = "Ada"; int id = 7;
// Good: no string built unless DEBUG is on
log.debug("id={} name={}", id, name);
}
}多个占位符
占位符会按顺序填充。第一个 {} 使用第一个参数,第二个使用第二个参数,依此类推。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
private static final Logger log = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
log.info("order {} for user {} totaling {} cents", "A-1", 42, 1599);
}
}为什么这样高效
框架会先检查级别。如果级别已禁用,就会立即返回,不会对任何参数调用 toString()。
开销较大的格式化操作会延迟执行,只有在消息确实要输出时才会进行。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
private static final Logger log = LoggerFactory.getLogger(Main.class);
static String describe(Object o) { System.out.println("toString called"); return o.toString(); }
public static void main(String[] args) {
// Note: passing describe(...) still evaluates eagerly; pass the raw object instead
log.trace("value={}", new int[]{1, 2, 3});
}
}传入原始对象,而不是预格式化字符串
要获得延迟执行的好处,请传入原始对象,不要传入会对其进行格式化的方法调用。
如果写成 log.debug("{}", buildReport()),buildReport() 仍会立即执行。请传入由日志记录器自行格式化的参数。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
private static final Logger log = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
Object user = new Object();
// Logger calls user.toString() only if DEBUG is enabled
log.debug("current user: {}", user);
}
}使用延迟供应器处理繁重工作
当格式化操作确实开销很大时,请使用 SLF4J 2.x 的流式 API和 Supplier,以延迟执行相关工作。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
private static final Logger log = LoggerFactory.getLogger(Main.class);
static String heavyReport() { return "...expensive..."; }
public static void main(String[] args) {
log.atDebug()
.setMessage("report: {}")
.addArgument(() -> heavyReport())
.log();
}
}转义字面量大括号
如果消息中需要包含字面量 {},请使用反斜杠进行转义:\\{}。
这种情况并不常见,但记录类似 JSON 的文本时很有用。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
private static final Logger log = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
// Logs a literal {} then the value
log.info("empty object is \\{} and id is {}", 7);
}
}异常与占位符结合使用
您可以将占位符与末尾的异常结合使用。SLF4J 会检测最后一个参数是否为 Throwable,并输出其堆栈跟踪。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
private static final Logger log = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
int id = 99;
try {
throw new RuntimeException("db down");
} catch (Exception e) {
log.error("failed processing record {}", id, e);
}
}
}避免使用过多参数
如果占位符数量与参数数量不一致,SLF4J 会尽力处理,但输出结果可能看起来不正确。
请保持二者数量一致。如果值超过两三个,可以考虑改用结构化上下文(MDC)。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
private static final Logger log = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
// Two placeholders, two arguments: correct
log.info("user {} did {}", "ada", "login");
}
}使用 MDC 添加上下文
对于许多日志行都会共享的值(例如请求 ID),请将它们放入 MDC,而不要在每条消息中重复传入。
这样,模式布局就可以自动在每一行输出 MDC 中的值。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class Main {
private static final Logger log = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
MDC.put("requestId", "req-1234");
try {
log.info("handling request");
log.info("request done");
} finally {
MDC.clear();
}
}
}最佳实践总结
高效记录日志的规则:
- 始终使用
{}占位符,绝不要使用+拼接。 - 传入原始对象,而不是预先构造的字符串。
- 对于真正开销很大的工作,请使用供应器或
isXEnabled()。 - 使用 MDC 保存跨多个部分共享的上下文。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
private static final Logger log = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
log.info("processed {} items in {} ms", 1000, 42);
}
}快速检查
测试您对参数化日志记录的了解。
回顾
您已经学习了参数化日志记录:
- 将
{}占位符与额外参数结合使用。 - 消息会延迟构造,仅在级别启用时执行。
- 传入原始对象;对于开销较大的工作,请使用供应器。
- 使用 MDC 保存共享的上下文值。
接下来将学习配置 Logback 后端。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
private static final Logger log = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
log.info("Parameterized logging recap: {}", "done");
}
}常见问题解答
「参数化日志记录」课时是免费的吗?
是的 — 「参数化日志记录」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Java Academy 课程的其余内容,请升级到 CoddyKit PRO。 Java Academy 课程共包含 4 节课。
「参数化日志记录」这节课中我会学到什么?
高效格式化消息 你通过在浏览器中直接运行的动手代码来练习 Java Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Java Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Java Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「参数化日志记录」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Java Academy 课中编写并运行代码吗?
能。每节 Java Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。