使用 log 占位符便利日志输出

介绍了如何使用日志的占位符来简化日志字符串的拼接输出

在开发活动中, 记日志是一个很常见的操作. 记日志经常涉及到拼接字符串, 因为我们常常需要把参数的值输出出来, 同时拼上字样的提示, 这样后续查看时才好知道发生了什么.

比如这样:

package net.xiaogd.sample.mybatis.controller;

import lombok.extern.slf4j.Slf4j;
import net.xiaogd.sample.mybatis.entity.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class LogController {

    @GetMapping("/hello")
    public String hello(@ModelAttribute User user) {
        log.info("username: " + user.getUsername() + " , age: " + user.getAge());
        return "hello there";
    }
}

注: 如果对这里 @Slf4j 式注入 log 变量的写法有疑问, 请参考: 使用 lombok @Slf4j 注解简化日志功能的引入

这样的方式一旦参数多了, 就会显得很凌乱, 写起来也不方便, 看上去也是支离破碎的.

很多人可能因此就不去记日志了, 或者只记很少的变量的值, 给后续的排查问题就带来了麻烦.

在前面的一篇 利用 String format 方法及占位符优雅拼接字符串 里, 提到了可以使用 format 方法结合 %X 占位符的方式, 那种方式自然也是可以用在日志里面的:

@GetMapping("/hello")
public String hello(@ModelAttribute User user) {
    log.info(String.format("username: %s, age: %d"), user.getUsername(), user.getAge());
    return "hello there";
}

这种比前一种方式要优雅一点, 不过呢, 却还是要用到 String.format 方法, 并且需要注意变量的类型, 而现在基本上很多的日志框架都直接支持一种占位符的写法, 与 String.format 中的不同, 可能相对来说还更简单一点, 只是它必须依赖于日志框架相关代码, 且只能用于日志输出中, 这样的方式就是利用大括号 {} 这种占位符(也称为 格式化锚点(formatting anchor))的写法:

@GetMapping("/hello")
public String hello(@ModelAttribute User user) {
    log.info("username: {}, age: {}", user.getUsername(), user.getAge());
    return "hello there";
}

这种写法用户也不需要关注变量的类型, 如果有三个或更多的变量值要输出, 就相应多写几个 {}, 以及多传几个对应变量即可:

注: 方法的第二个参数是可变长参数, 因此可以传多个.

有了这种写法的支持, 就可以一口气写完要输出的日志信息, 而要拼接的变量就用 {} 暂时代替, 避免了用 + 号拼接的繁琐.

这样的使用占位符的写法还有一个潜在的好处, 就是性能的提升. 如果配置的日志级别是不输出 info 级别的日志, 那么相应的字符串的拼接与替换就不会发生, 可变长参数数组也不会生成.

而如果使用 + 号式之类的写法, 就不能得到这个好处了, 字符串传入之前就已经被拼接了, 最后因为日志级别的原因又没有输出的话, 无疑是一种浪费.

最后, 如果是 error 日志, 还支持一种特别的写法, 可以把异常传给最后一个参数, 除了替换占位符输出你的异常信息外, 它还会把异常栈整个打印出来, 避免了你自己去调用 e.printStackTrace:

package net.xiaogd.sample.mybatis.service;

import lombok.extern.slf4j.Slf4j;
import net.xiaogd.sample.mybatis.entity.User;

import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.List;

@Slf4j
public class UserServiceImpl implements UserService {
    @Override
    public List<User> fetchUserList() {
        String fileName = "user-list.txt";
        InputStreamReader reader = new InputStreamReader(getClass().getResourceAsStream(fileName), StandardCharsets.UTF_8);
        try {
            int c = reader.read();
        } catch (IOException e) {
            log.error("error while reading {}", fileName, e);
        }
        // todo
        return null;
    }
}

注意以上, 错误信息只有一个占位符, 但后面跟了两个变量, 其中最后的变量是 Throwable 类型的, 只有第一个变量值才参与字符串的拼接.

当最后的变量是 Throwable 类型时, log 框架会将其当作异常栈输出, 类似于这个调用 public void error(String msg, Throwable t);, 异常变量需置于最后.

另注: 此用法需要相应的日志版本的支持, 参见这里: http://www.slf4j.org/faq.html#paramException, 适用 1.6.0 版本及以上, 相应的日志实现也需支持.

以上例子说明了在 log 异常时, 同样可以使用占位符的写法, 同时兼容原有的打印异常栈的写法, 这些都算是方便我们输出异常日志的一些技巧, 关于使用 {} 占位符输出日志的介绍就到这里.