一、日志框架概述与SLF4j简介 1.1 为什么需要日志框架 在软件开发中,日志记录是至关重要的组成部分。想象一下你正在开发一个电商系统:
用户下单失败时,你需要知道具体原因 系统性能出现瓶颈时,你需要追踪耗时操作 生产环境出现问题时,你需要排查错误根源 如果没有良好的日志系统,就像在黑暗中摸索,无法快速定位和解决问题。
1.2 主流日志框架对比 框架名称 类型 特点 适用场景 Log4j 实现 Apache出品,功能强大,配置灵活 传统Java项目 Log4j2 实现 Log4j升级版,性能更好,支持异步 高性能要求的系统 Logback 实现 SLF4j原生实现,性能优异 Spring Boot默认 JUL (java.util.logging) 实现 JDK内置,功能简单 小型应用或JDK环境限制时 SLF4j 门面 提供统一接口,不负责具体实现 需要灵活切换日志实现的场景 1.3 SLF4j的核心价值 SLF4j (Simple Logging Facade for Java) 是一个日志门面(Facade),不是具体的日志实现。它类似于JDBC,提供统一的API,底层可以连接不同的数据库驱动。
门面模式的优势:
解耦:业务代码不依赖具体日志实现 灵活:可随时切换底层日志框架 统一:项目中使用一致的日志API 二、Spring Boot默认日志配置 2.1 Spring Boot的日志选择 Spring Boot默认使用SLF4j + Logback组合:
S
LF4j:提供统一的日志API
Logback:作为SLF4j的默认实现,性能优于Log4j
2.2 基本使用示例
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController
public class OrderController {
// 使用SLF4j的LoggerFactory获取Logger实例
private static final Logger logger = LoggerFactory.getLogger(OrderController.class);
@GetMapping("/order/{id}")
public String getOrder(@PathVariable String id) {
// 不同级别的日志记录
logger.trace("追踪订单查询,订单ID: {}", id); // 最详细的日志
logger.debug("调试信息-订单ID: {}", id); // 调试信息
logger.info("查询订单,订单ID: {}", id); // 业务信息
logger.warn("订单查询参数过长,ID: {}", id); // 警告信息
if(id.length() > 20) {
logger.error("订单ID格式异常: {}", id); // 错误信息
throw new IllegalArgumentException("非法的订单ID");
}
return "订单详情";
}
2.3 日志级别详解 SLF4j定义了6种日志级别(从低到高):
级别 含义 使用场景 默认是否输出 TRACE 追踪 最详细的日志信息,记录程序每一步执行 否 DEBUG 调试 调试信息,开发阶段使用 否 INFO 信息 重要的业务过程信息 是 WARN 警告 潜在的问题,不影响系统运行 是 ERROR 错误 错误信息,影响部分功能 是 FATAL 致命 导致系统崩溃的严重错误 是(Logback无此级别,会映射为ERROR) 三、SLF4j配置文件详解 3.1 默认配置与自定义 Spring Boot默认会在classpath下查找以下日志配置文件:
logback-spring.xml (推荐) logback.xml 为什么推荐使用logback-spring.xml?
因为它支持Spring Boot的Profile特性,可以根据不同环境加载不同配置。
3.2 完整配置文件解析 以下是一个完整的logback-spring.xml示例,我们分段解析:
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 控制台只输出INFO及以上级别 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<!-- 滚动文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 按日期和大小滚动 -->
<fileNamePattern>${LOG_HOME}/${APP_NAME}-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 单个文件最大100MB -->
<maxFileSize>100MB</maxFileSize>
<!-- 保留30天日志 -->
<maxHistory>30</maxHistory>
<!-- 总大小不超过5GB -->
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
</appender>
<!-- 异步日志 -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志的阈值,默认256 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender -->
<appender-ref ref="FILE" />
</appender>
<!-- 日志级别设置 -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ASYNC" />
</root>
<!-- 特定包/类日志级别 -->
<logger name="com.example.demo.dao" level="DEBUG" />
<logger name="org.springframework" level="WARN" />
<!-- 生产环境特定配置 -->
<springProfile name="prod">
<root level="WARN">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ASYNC" />
</root>
</springProfile>
<!-- 开发环境特定配置 -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE" />
</root>
</springProfile>
1 属性 说明 示例值 必要性 name 属性名 LOG_HOME 必填 value 属性值 ./logs 必填 scope 作用域 context/system 可选 3.3.2 输出源(Appender)
1. 控制台输出(ConsoleAppender)
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
{APP_NAME}.log
{APP_NAME}-%d{yyyy-MM-dd}.%i.log
100MB
30策略类 说明 适用场景
TimeBasedRollingPolicy 按时间滚动 需要按天/小时分割日志
SizeBasedTriggeringPolicy 按大小滚动 需要限制单个日志文件大小
SizeAndTimeBasedRollingPolicy 时间和大小双重策略 既按时间又按大小分割
3.3.3 日志格式(Pattern)
日志格式由转换符组成,常用转换符:
转换符 说明 示例输出
%d 日期 2023-05-20 14:30:00,123
%thread 线程名 main
%-5level 日志级别(左对齐,固定5字符) INFO
%logger Logger名称 com.example.MyClass
%msg 日志消息 User login success
%n 换行符
%X 输出MDC中的值 %X{userId}
3.3.4 过滤器(Filter)
ERROR
ACCEPT
DENY
过滤器类 功能 参数
LevelFilter 精确匹配级别 level, onMatch, onMismatch
ThresholdFilter 阈值过滤,高于等于该级别才记录 level
EvaluatorFilter 使用表达式过滤 evaluator
3.3.5 异步日志(AsyncAppender)
512
0
参数 说明 默认值 建议值
queueSize 队列大小 256 512-2048
discardingThreshold 当队列剩余容量小于此值时,丢弃TRACE/DEBUG日志 queueSize/5 0(不丢弃)
includeCallerData 是否包含调用者信息 false 生产环境false(性能考虑)
四、高级特性与最佳实践
4.1 MDC (Mapped Diagnostic Context)
MDC用于在日志中保存线程上下文信息,非常适合Web请求跟踪。
示例:添加请求ID到日志
import org.slf4j.MDC;
@RestController public class OrderController {
@GetMapping("/order/{id}")
public String getOrder(@PathVariable String id) {
// 设置MDC值
MDC.put("requestId", UUID.randomUUID().toString());
MDC.put("userId", "user123");
try {
logger.info("查询订单: {}", id);
// 业务逻辑...
return "订单详情";
} finally {
// 清除MDC
MDC.clear();
}
}
logback配置中引用MDC
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{requestId}] %-5level %logger{36} - %msg%n
避免字符串拼接,使用占位符:
// 不推荐
logger.debug("User " + userId + " login from " + ip);
// 推荐
logger.debug("User {} login from {}", userId, ip);
1
2
3
4
5
isXXXEnabled判断
对于高开销的日志:
if(logger.isDebugEnabled()) {
logger.debug("Large data: {}", expensiveOperation());
}
1
2
3
异步日志
如前面示例,使用AsyncAppender减少I/O阻塞
4.3 多环境配置 利用Spring Profile实现环境差异化配置:
ERROR
解决方案:使用mvn dependency:tree检查依赖,排除多余的日志实现
org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-logging
确认配置文件位置正确(resources目录下) 检查文件路径是否有写入权限 查看是否有异常日志输出 确认Appender被引用 5.3 日志级别不生效 可能原因:
配置被覆盖(检查多个配置文件) 包路径配置错误 配置修改后未重新加载(设置scan="true") 六、实战案例:电商系统日志设计 6.1 日志分类设计 日志类型 级别 输出目标 内容 访问日志 INFO access.log 记录所有HTTP请求 业务日志 INFO biz.log 核心业务操作 错误日志 ERROR error.log 系统异常和错误 SQL日志 DEBUG sql.log SQL语句和参数 性能日志 INFO perf.log 接口耗时统计 6.2 完整配置示例
<!-- 公共Pattern -->
<property name="COMMON_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{requestId}] [%thread] %-5level %logger{36} - %msg%n" />
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${COMMON_PATTERN}</pattern>
</encoder>
</appender>
<!-- 访问日志 -->
<appender name="ACCESS" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/access.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{requestId}] %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/access.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<!-- 错误日志 -->
<appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/error.log</file>
<encoder>
<pattern>${COMMON_PATTERN}</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>60</maxHistory>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<!-- 异步Appender -->
<appender name="ASYNC_ERROR" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>1024</queueSize>
<appender-ref ref="ERROR" />
</appender>
<!-- 日志级别配置 -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
<!-- 访问日志Logger -->
<logger name="ACCESS_LOG" level="INFO" additivity="false">
<appender-ref ref="ACCESS" />
</logger>
<!-- 错误日志Logger -->
<logger name="ERROR_LOG" level="ERROR" additivity="false">
<appender-ref ref="ASYNC_ERROR" />
</logger>
<!-- MyBatis SQL日志 -->
<logger name="org.mybatis" level="DEBUG" additivity="false">
<appender-ref ref="SQL" />
</logger>
@Around("execution(* com.example.ecommerce.controller..*.*(..))")
public Object logAccess(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 记录请求信息
String requestId = UUID.randomUUID().toString();
MDC.put("requestId", requestId);
long start = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long cost = System.currentTimeMillis() - start;
accessLog.info("method={} uri={} status=success cost={}ms ip={} params={}",
request.getMethod(),
request.getRequestURI(),
cost,
request.getRemoteAddr(),
request.getQueryString());
return result;
} catch (Exception e) {
long cost = System.currentTimeMillis() - start;
accessLog.error("method={} uri={} status=error cost={}ms error={}",
request.getMethod(),
request.getRequestURI(),
cost,
e.getMessage());
throw e;
} finally {
MDC.clear();
}
}
七、总结 7.1 关键要点总结 统一使用SLF4j API:保持代码与具体实现解耦 合理配置日志级别:生产环境通常INFO,开发环境DEBUG 日志文件分割策略:按时间和大小双维度分割 使用MDC增强日志:添加请求跟踪信息 性能优化:异步日志、参数化日志、isXXXEnabled判断 多环境支持:利用Profile实现差异化配置