在Java代码中打日志需要注意什么?

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 日志是什么?日志是你在代码运行时打印出来的一些数据和记录,是快速排查问题的好帮手,是撕逼和甩锅的利器!

为什么要打日志?


日志是什么?日志是你在代码运行时打印出来的一些数据和记录,是快速排查问题的好帮手,是撕逼和甩锅的利器!

做一件事情之前,先思考为什么。为什么我们在开发中,需要打日志?原因很简单,没人能保证自己写的程序没有BUG,即使你做了足够的测试,也只是能降低产生BUG的概率而已。

尤其是当今分布式环境,定位问题变得越来越复杂。所以我们想要获取一些程序“运行时”的信息,日志就是最方便的。

所以,这种福泽后来人的好东西,当然要用起来了~


Java日志框架


要说Java日志框架啊,要从远古时代的JDK 1.3之前说起。那时候大家打印日志就是直接输出到STDOUT或者STDERR流。

System.out.println()
System.err.println()
e.printStackTrace()

于是log4j在大牛Ceki中应运而生,后面经过一系列的发展,以及Ceki与Apache的吃瓜事件,逐渐发展为slf4j、logback、log4j2三种最主流的日志框架。

  • slf4j: 日志的“门面”框架,对于用户来说只要使用SLF4J提供的接口,即可隐藏日志的具体实现。
  • logback: 与slf4j一样,为Ceki大神创建,所以原生实现slf4j,Spring Boot钦点的默认日志框架。
  • log4j2: Apache顶级项目,性能优于logback,尤其是异步输出,表现比较好。

PS: 4j是for Java的意思,在Java开源项目中是很常见的命名方式。

所以现在主流的框架一般是slf4j + logback或者slf4j + log4j2。

阿里巴巴开发手册中,日志规约第一条就是:应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架(SLF4J、JCL--Jakarta Commons Logging)中的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。


如何选择日志级别?


关于日志级别,不同的标准有不同的定义。鉴于slf4j差不多已经一统天下,所以我们主要介绍slf4j定义的5种日志级别。

5种日志级别

ERROR、WARN、INFO、DEBUG、TRACE这五个级别从高到低,配置级别越高日志输出就越少

我们在配置输出某个级别的日志的时候,它也会输出比它高级别的日志。比如我们配置日志在INFO这一级别,它会输出ERROR、WARN、INFO三种级别的日志。

顾名思义,这几种级别的日志分别会输出不同程度信息:

  • ERROR:错误日志,比较严重的错误,对正常业务有影响;
  • WARN:警告日志,一般的错误,对业务影响不大;
  • INFO:信息日志,记录一些日常的东西,比如调用时间、出参入参、业务信息等;
  • DEBUG:用于DEBUG的,关键逻辑里面的运行时数据;
  • TRACE:最详细的信息,一般这些信息只记录到日志文件中。

PS: 根据笔者的经验来看,使用TRACE级别的比较少。log4j建议只有前4个级别。


级别继承

有时候,我们可能根据不同的业务输出不同级别的日志。比如某些重要业务输出INFO级别,其他业务输出WARN级别的日志,同时关闭所有库、框架的日志,比如Spring等。

日志级别继承

一般情况,我们设置root级别了就行了,对于某些业务有特殊要求的话,对特定的包配置就行了。这里值得一提的是,谨慎开启低级别的日志,尤其是对root级别。之前听说过一个故障,就是为了Debug一个问题,在生产环境开启了DEBUG日志,并且是root级别,导致瞬间打印出巨量的日志,磁盘撑不住,造成了生产事故。

logging:
  level:
    org:
      springframework:
        orm: DEBUG
        transaction: DEBUG
    root: INFO


注意事项


开关判断

在阿里的Java开发手册中,有这么一条规约:

【强制】:对于 trace/debug/info 级别的日志输出,必须进行日志级别的开关判断。

也就是说,在日志级别比较低的时候,应该在打日志前增加一个判断,减少不必要的方法调用开销。举个例子:

// 打日志的业务代码
User user = new User(1L, "username", "pwd");
log.debug("userId is: {}", user.getId());
return user;
// user.getId()实现
public long getId() {
    System.out.println("调用了getId()");
    return id;
}

上面这段代码,我在打debug日志的时候,调用了user.getId()方法,这个时候,即使我们配置只打INFO级别的日志,运行到这段代码的时候,仍然会调用user.getId()方法,造成不必要的开销。

如果我们在前面添加一个判断就可以解决这个问题:

User user = new User(1L, "username", "pwd");
if (log.isDebugEnabled()) {
    log.debug("userId is: {}", user.getId());
}
return user;

当然,这个根据自己的项目来。如果你的项目部署肯定会开启INFO级别的日志,那INFO级别的日志,可以不加条件判断

使用参数占位

在上面的例子中,我们使用了大括号{}来作为日志中的占位符。相比于使用+操作符进行字符串的拼接,使用占位符可以让我们的代码更加优雅简洁。

除此以外,String字符串的拼接会使用StringBuilder的append()方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。

日志越多越好?

日志其实也是代码中的一部分。我们都知道,代码的可读性有多重要。其实并不需要每个地方都打上日志,这样我们在分析日志的时候也比较难快速定位关键信息。

我们要明白的一点是,我们需要的不是日志,而是有效日志。更何况,无效日志打多了,费磁盘~

只需要在我们关注的地方,打印关键的信息就可以了。比较常用的是我们通常能够快速定位数据的唯一id,比如gid等。

同步 vs 异步

我们知道,日志最终会输出到文件或者其它输出流,会重度使用IO。而异步可以显著提升IO性能,所以如果不是有特殊的需求,通常的建议使用异步的方式来输出日志。

以logback为例,要配置异步很简单,使用AsyncAppender就行:

<!--配置异步日志-->
<appender name="FILE_ASYNC" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="ASYNC"/>
</appender>

trace Id

如今的系统做得越来越大,越来越复杂。前后端分离、分布式架构使得排查问题、追踪调用链路也变得越来越复杂。为了解决这个问题,我们可以使用一个trace Id来标识一次完整的调用链路。

目前主流的日志框架已经有这方面的支持了。拿logback为例,它提供了一种MDC机制,MDC为“Mapped Diagnostic Context”(映射诊断上下文),具体实现就是org.sl4j.MDC这个类。本文就不详细介绍如何使用它的了,感兴趣的同学可以去参考官方文档。

日志文件分离

我们打印日志是为了获取一些我们需要的信息。所以我们可以把不同类型的日志分离出去,比如access_log,或者ERROR级别的log,都可以单独打印到一个文件里。

也可以根据不同的业务模块,打印到不同的日志文件里,这样我们排查问题和做数据统计的时候就会比较方便。

获取日志实例

使用过日志框架的同学,对或多或少在类里定义过日志实例吧。但根据笔者的观察,大家定义日志的姿势各有千秋。有叫log的,有叫logger的,也有叫LOGGER的。定义的时候也都比较随意,比如不加static,不加final,获取实例的时候有传入this的,也有传入.class的。

这里不得不提一个优秀的Lombok注解:@Slf4j。它能够根据你的类自动注入一个log实例,非常方便快捷,如果你的项目使用了Lombok,不妨用起来。如果没有使用Lombok也没有关系,可以参考它的定义方式:

@Slf4j
public class Demo {
复制代码}
// 等价于:
public class Demo {
private static final Logger log = LoggerFactory.getLogger(Demo.class);
}

以上,大家就可以愉快地打日志啦~

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
2月前
|
Java
在 Java 中捕获和处理自定义异常的代码示例
本文提供了一个 Java 代码示例,展示了如何捕获和处理自定义异常。通过创建自定义异常类并使用 try-catch 语句,可以更灵活地处理程序中的错误情况。
78 1
|
7天前
|
Java Maven
java项目中jar启动执行日志报错:no main manifest attribute, in /www/wwwroot/snow-server/z-server.jar-jar打包的大小明显小于正常大小如何解决
在Java项目中,启动jar包时遇到“no main manifest attribute”错误,且打包大小明显偏小。常见原因包括:1) Maven配置中跳过主程序打包;2) 缺少Manifest文件或Main-Class属性。解决方案如下:
java项目中jar启动执行日志报错:no main manifest attribute, in /www/wwwroot/snow-server/z-server.jar-jar打包的大小明显小于正常大小如何解决
|
2月前
|
Java
在Java中实现接口的具体代码示例
可以根据具体的需求,创建更多的类来实现这个接口,以满足不同形状的计算需求。希望这个示例对你理解在 Java 中如何实现接口有所帮助。
92 38
|
13天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
35 3
|
30天前
|
监控 测试技术 开发者
一行代码改进:Logtail的多行日志采集性能提升7倍的奥秘
一个有趣的现象引起了作者的注意:当启用行首正则表达式处理多行日志时,采集性能出现下降。究竟是什么因素导致了这种现象?本文将探索Logtail多行日志采集性能提升的秘密。
111 23
|
2月前
|
Java
java小工具util系列4:基础工具代码(Msg、PageResult、Response、常量、枚举)
java小工具util系列4:基础工具代码(Msg、PageResult、Response、常量、枚举)
56 24
|
20天前
|
前端开发 Java 测试技术
java日常开发中如何写出优雅的好维护的代码
代码可读性太差,实际是给团队后续开发中埋坑,优化在平时,没有那个团队会说我专门给你一个月来优化之前的代码,所以在日常开发中就要多注意可读性问题,不要写出几天之后自己都看不懂的代码。
56 2
|
1月前
|
运维 监控 Cloud Native
一行代码都不改,Golang 应用链路指标日志全知道
本文将通过阿里云开源的 Golang Agent,帮助用户实现“一行代码都不改”就能获取到应用产生的各种观测数据,同时提升运维团队和研发团队的幸福感。
136 11
|
1月前
|
Java 编译器 数据库
Java 中的注解(Annotations):代码中的 “元数据” 魔法
Java注解是代码中的“元数据”标签,不直接参与业务逻辑,但在编译或运行时提供重要信息。本文介绍了注解的基础语法、内置注解的应用场景,以及如何自定义注解和结合AOP技术实现方法执行日志记录,展示了注解在提升代码质量、简化开发流程和增强程序功能方面的强大作用。
82 5
|
1月前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
57 5