Skip to content

最佳实践 - 日志记录

image-20250110124335414

基本原则

  • ❌ 避免记录重复、无意义的日志,确保每一条日志都有助于调试
  • 🔄 避免在循环中使用日志,以免产生大量冗余数据
  • 📘 只记录必要的参数,而不是完整的对象结构

风险防范

  • 🌐 将英文作为日志语言,以预防编码问题

  • 🔨 不滥用Error级别的日志,减轻紧急响应负担

  • 🚫 不要在日志记录过程中阻断业务流程

    错误方式

    不要用可能会发生空指针异常的对象或方法

    java
    public void createShop(Shop shop){
    	log.info("create and print log: {}", shop.getName().toLowerCase());
    }

性能保障

  • 💡 使用专用日志库,而非标准输出,来提高性能并便于管理

    错误方式
    java
    public void love() {
        System.out.println("i love java...");
        // 业务逻辑
        ...
    }
    原因:println 内部实现是带锁的
    java
    public void println(boolean x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
    正确方式
    java
    public void love() {
        log.info("i love java...");
        // 业务逻辑
        ...
    }
  • 🔒 统一日志框架减少依赖耦合,提升维护便利性

    禁用Log4jLogBack的API输出日志,而是用Slf4j,防止代码和日志强耦合

    java
    // 直接使用
    @Slf4j
    public class Test {}
    
    // 或者直接引入
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    private static final Logger log = LoggerFactory.getLogger(xxx.class);
  • ⚙️ 根据日志级别控制输出,防止不必要的资源耗费

    错误方式
    java
    public void hello(String name) {
        log.trace("trace hello" + name);
        log.debug("debug hello" + name);
        log.info("info hello" + name);
        // 业务逻辑
        ...
    }
    正确方式

    强烈建议使用占位符来输出内容,防止执行字符串拼接操作,浪费系统资源

    java
    public void hello() {
        if (log.isTraceEnabled()) {
            log.trace("trace hello {}", name);
        }
        if (log.isDebugEnabled()) {
            log.debug("debug hello {}", name);
        }
        if (log.isInfoEnabled()) {
            log.info("info hello {}", name);
        }
        // 业务逻辑
        ...
    }
  • 👾 错误地使用e.printStackTrace(),可能导致严重的资源耗尽

    错误方式
    java
    public void hello() {
        try {
            // 业务逻辑
            ...
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    原因:e.printStackTrace()会将产生的字符串存放到字符串常量池,降低可用内存空间
    java
    public void printStackTrace() {
        printStackTrace(System.err);
    }
    正确方式
    java
    public void hello() {
        try {
            // 业务逻辑
            ...
        } catch (Exception e) {
            log.error("execute failed", e);
        }
    }
  • 🐛 在日志中禁用复杂对象序列化以防潜在风险

    错误方式

    禁止使用JSON序列化工具,这些工具是通过调用对象的get方法将对象进行序列化的,如果对象的get方法被重写则可能会面临抛出异常的风险

    java
    public void hello(Object data) {
        log.info("print log, data={}", JSON.toJSONString(data));
        // 业务逻辑
        ...
    }
    正确方式

    推荐自定义toString 方法

    java
    public void hello() {
        log.info("hello and print log, data={}", data);
    }

开发简化

  • 🕵️‍♂️ 提供详细异常信息有助于问题追踪

    错误方式
    java
    public void hello() {
        try {
            // 业务逻辑
            ...
        } catch (Exception e) {
            // 没有打印异常 e,无法定位出现什么类型的异常
            log.error("execute failed");
            // 没有记录详细的堆栈异常信息,只记录错误基本描述信息,不利于排查问题
            log.error("execute failed", e.getMessage());
        }
    }
    正确方式
    java
    public void hello() {
        try {
            // 业务逻辑
            ...
        } catch (Exception e) {
            log.error("execute failed", e);
        }
    }
  • 💼 记录关键函数的输入/输出,增强排错能力

    java
    public String doSth(String id, String type) {
        log.info("start: {}, {}", id, type);        // 入口处记录初始值
        String res = process(id, type);
        log.info("end: {}, {}, {}", id, type, res); // 出口处记录结果
    }
  • 🤝 利用条件语句前后的日志,简化程序跟踪

    java
    public void doSth() {
        ...
        if (user.isVip()) {
            log.info("vip member, Id: {}, start vip", userId());
            // 会员逻辑
        } else {
            log.info("该用户是非会员, Id: {}, 开始处理非会员逻辑", userId());
            // 非会员逻辑
        }
        ...
    }
  • 📍 添加Trace ID用于关联相关事件流,串联一次性的日志链路

    java
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import java.io.IOException;
    import java.util.UUID;
    
    @Component
    public class MDCCFilter implements Filter {
    
        private static final String TRACE_ID = "traceId";
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, javax.servlet.ServletException {
            try {
                // 为每个请求生成唯一的 Trace ID
                String traceId = UUID.randomUUID().toString();
                MDC.put(TRACE_ID, traceId);
    
                // 如果需要,可以从请求中提取更多上下文信息,例如用户 ID
                if (request instanceof HttpServletRequest) {
                    HttpServletRequest httpRequest = (HttpServletRequest) request;
                    String userId = httpRequest.getHeader("X-User-Id"); // 假设用户 ID 在请求头中
    
                    if (userId != null) {
                        MDC.put("userId", userId);
                    }
                }
    
                // 执行后续的 Filter 链
                chain.doFilter(request, response);
            } finally {
                // 清理 MDC,避免线程数据污染
                MDC.clear();
            }
        }
    }
    
    // 日志格式
    %d{yyyy-MM-dd HH:mm:ss} [%thread] [%X{traceId}]
    %logger{36} - %msg%n
    ...