大家好呀! 今天我们来聊聊Spring Boot中一个超级重要但又经常被忽视的功能——日志系统!
首先,咱们得明白为什么日志这么重要?♂️
想象一下,你正在玩一个超级复杂的游戏,突然游戏崩溃了,但是没有任何提示!这时候你是不是特别想知道到底哪里出了问题? 日志就像是程序的"日记本",它记录着程序运行时的各种信息,帮助我们:
在Spring Boot中,日志系统是开箱即用的,而且默认集成了Logback和SLF4J,这俩到底是什么呢?咱们接着往下看!
SLF4J(Simple Logging Facade for Java)就像是日志系统的"遥控器",它定义了一套统一的日志接口,但不负责具体的日志实现。这样做的好处是:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyClass {
// 通过SLF4J获取Logger
private static final Logger logger = LoggerFactory.getLogger(MyClass.class);
public void doSomething() {
logger.info("这是一条信息日志");
logger.error("这是一条错误日志");
}
}
Logback是SLF4J的"原生实现",也是Spring Boot默认的日志框架。它比传统的Log4j性能更好、功能更强大:
Spring Boot为我们做了很多自动配置工作,让我们来看看它是如何集成Logback和SLF4J的!
当你在Spring Boot项目中添加spring-boot-starter
或spring-boot-starter-web
依赖时,它会自动引入:
org.springframework.boot
spring-boot-starter-logging
这个starter又引入了以下依赖:
logback-classic
(包含Logback和SLF4J绑定)jul-to-slf4j
(将Java Util Logging重定向到SLF4J)log4j-to-slf4j
(将Log4j2重定向到SLF4J)这样,无论你使用哪种日志API,最终都会统一到SLF4J,再由Logback处理!
Spring Boot默认的日志输出格式是这样的:
2023-03-15 14:30:45.123 INFO 12345 --- [ main] com.example.MyClass : 这是一条日志信息
分解一下各部分含义:
2023-03-15 14:30:45.123
:时间戳⏰INFO
:日志级别12345
:进程ID[main]
:线程名com.example.MyClass
:类名这是一条日志信息
:日志内容Spring Boot默认的日志级别是INFO,也就是说:
虽然Spring Boot提供了合理的默认配置,但我们通常需要根据自己的需求进行调整。️
最简单的配置方式是在application.properties
或application.yml
中设置:
# 设置全局日志级别
logging.level.root=WARN
# 设置特定包的日志级别
logging.level.com.example=DEBUG
# 输出到文件 (默认在项目根目录生成spring.log)
logging.file.name=myapp.log
# 或者指定日志目录 (会在该目录下生成spring.log)
logging.file.path=/var/log
# 自定义日志格式
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
对于更复杂的配置,可以创建logback-spring.xml
文件放在src/main/resources
目录下:
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{36}) - %msg%n
logs/myapp.log
logs/myapp-%d{yyyy-MM-dd}.%i.log
10MB
30
1GB
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
这个配置文件做了以下事情:
Logback支持根据不同的Spring Profile使用不同的配置:
这样,在开发环境可以看到更详细的日志,而在生产环境则只记录重要信息。
日志级别就像是信息的"紧急程度",不同级别用于不同场景:
级别 | 描述 | 使用场景 |
---|---|---|
TRACE | 最详细的跟踪信息 | 开发时追踪程序每一步执行 |
DEBUG | 调试信息 | 开发阶段排查问题 |
INFO | 重要的运行信息 | 生产环境记录应用程序运行状态 |
WARN | 潜在的问题,但不影响程序运行 | 不推荐的做法、即将过期的API使用等 |
ERROR | 错误信息,影响部分功能 | 捕获的异常、业务逻辑错误等 |
FATAL | 严重错误,导致应用程序退出 | 系统崩溃、无法恢复的错误 |
最佳实践:
不好的写法❌:
logger.info("用户ID: " + userId + " 购买了商品: " + productId);
好的写法✅:
logger.info("用户ID: {} 购买了商品: {}", userId, productId);
使用占位符{}
的好处:
不好的写法❌:
try {
// 一些代码
} catch (Exception e) {
logger.error("发生错误了");
}
好的写法✅:
try {
// 一些代码
} catch (Exception e) {
logger.error("处理用户订单时发生错误, 用户ID: {}", userId, e);
}
记录异常时:
e.getMessage()
,会丢失堆栈信息日志不是越多越好,过多的日志会:
应该记录✅:
不应该记录❌:
MDC就像是一个"日志的上下文背包",可以在处理一个请求期间存储一些信息,然后在日志中输出:
// 在处理请求开始时
MDC.put("requestId", UUID.randomUUID().toString());
MDC.put("userId", getCurrentUserId());
// 在日志配置中
%d{yyyy-MM-dd} [%X{requestId}] [%X{userId}] %msg%n
// 在处理请求结束时
MDC.clear();
这样,同一个请求的所有日志都会带上相同的requestId和userId,方便追踪!
有时候我们想过滤掉一些不重要的日志,可以自定义过滤器:
INFO
%msg%n
这个过滤器会只允许INFO及以上级别的日志输出。
为了减少日志对主业务的影响,可以使用异步日志:
512
0
配置说明:
queueSize
:队列大小,默认为256discardingThreshold
:当队列剩余容量小于这个值时,丢弃TRACE、DEBUG和INFO级别的日志appender-ref
:引用的实际appender如果你的项目依赖的库使用了不同的日志框架,可能会出现冲突。Spring Boot已经帮我们解决了大部分问题,但如果遇到冲突:
mvn dependency:tree
查看依赖树
some.group
some-artifact
commons-logging
commons-logging
解决方案:
totalSizeCap
限制日志总大小如果日志影响性能:
根据我的经验,总结了一些Spring Boot日志的最佳实践:
Spring Boot的日志系统看似简单,实则功能强大! 通过本文,你应该已经掌握了:
记住,好的日志习惯是成为优秀开发者的重要一步! 下次写日志时,想想这篇文章,让你的日志更加专业和有用!
如果你有任何问题或建议,欢迎在评论区留言! 我会尽力解答!Happy logging!
由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)
如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系
HTTP、HTTPS、Cookie 和 Session 之间的关系
什么是 Cookie?简单介绍与使用方法
什么是 Session?如何应用?
使用 Spring 框架构建 MVC 应用程序:初学者教程
有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误
如何理解应用 Java 多线程与并发编程?
把握Java泛型的艺术:协变、逆变与不可变性一网打尽
Java Spring 中常用的 @PostConstruct 注解使用总结
如何理解线程安全这个概念?
理解 Java 桥接方法
Spring 整合嵌入式 Tomcat 容器
Tomcat 如何加载 SpringMVC 组件
“在什么情况下类需要实现 Serializable,什么情况下又不需要(一)?”
“避免序列化灾难:掌握实现 Serializable 的真相!(二)”
如何自定义一个自己的 Spring Boot Starter 组件(从入门到实践)
解密 Redis:如何通过 IO 多路复用征服高并发挑战!
线程 vs 虚拟线程:深入理解及区别
深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别
10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!
“打破重复代码的魔咒:使用 Function 接口在 Java 8 中实现优雅重构!”
Java 中消除 If-else 技巧总结
线程池的核心参数配置(仅供参考)
【人工智能】聊聊Transformer,深度学习的一股清流(13)
Java 枚举的几个常用技巧,你可以试着用用
由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)
如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系
HTTP、HTTPS、Cookie 和 Session 之间的关系
使用 Spring 框架构建 MVC 应用程序:初学者教程
有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误
Java Spring 中常用的 @PostConstruct 注解使用总结
线程 vs 虚拟线程:深入理解及区别
深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别
10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!
探索 Lombok 的 @Builder 和 @SuperBuilder:避坑指南(一)
为什么用了 @Builder 反而报错?深入理解 Lombok 的“暗坑”与解决方案(二)