Spring Boot与日志
日志是应用程序中非常重要的组成部分,用于记录程序运行过程中的信息、警告和错误。Spring Boot为日志提供了优秀的支持和默认配置。
日志框架概述
日志门面与日志实现
在Java生态系统中,日志分为两类:
日志门面(Facade): - SLF4J(Simple Logging Facade for Java) - Commons Logging
日志实现: - Logback - Log4j2 - Log4j - Java Util Logging(JUL)
Spring Boot的日志选择
Spring Boot默认使用: - SLF4J 作为日志门面 - Logback 作为日志实现
这是一个非常好的组合,因为: - SLF4J提供了简洁的API - Logback性能优秀,功能强大 - 两者都是同一个作者开发,兼容性好
Spring Boot日志默认配置
默认日志配置
Spring Boot开箱即用的日志配置包括:
- 控制台输出:默认输出到控制台
 - 日志级别:Root级别为INFO
 - 日志格式:包含时间戳、级别、PID、线程名、类名和消息
 - 彩色输出:在支持的终端中显示彩色日志
 
默认日志格式
2023-12-01 10:30:45.123  INFO 12345 --- [main] com.example.Application  : Starting Application
格式说明:
- 2023-12-01 10:30:45.123:时间戳
- INFO:日志级别
- 12345:进程ID
- [main]:线程名
- com.example.Application:类名
- Starting Application:日志消息
日志级别
日志级别层次
从低到高依次为:
- TRACE:最详细的信息,通常只在开发时使用
 - DEBUG:调试信息,用于诊断问题
 - INFO:一般信息,程序正常运行的信息
 - WARN:警告信息,可能出现问题但不影响运行
 - ERROR:错误信息,程序出现错误但可以继续运行
 - FATAL:致命错误,程序无法继续运行
 
在代码中使用日志
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class UserService {
    private static final Logger logger = LoggerFactory.getLogger(UserService.class);
    public User findById(Long id) {
        logger.trace("Entering findById method with id: {}", id);
        try {
            logger.debug("Searching for user with id: {}", id);
            User user = userRepository.findById(id);
            if (user != null) {
                logger.info("User found: {}", user.getUsername());
                return user;
            } else {
                logger.warn("User not found with id: {}", id);
                return null;
            }
        } catch (Exception e) {
            logger.error("Error occurred while finding user with id: {}", id, e);
            throw new UserServiceException("Failed to find user", e);
        } finally {
            logger.trace("Exiting findById method");
        }
    }
}
使用Lombok简化日志
添加Lombok依赖:
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
使用@Slf4j注解:
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class UserService {
    public User findById(Long id) {
        log.debug("Searching for user with id: {}", id);
        try {
            User user = userRepository.findById(id);
            log.info("User found: {}", user.getUsername());
            return user;
        } catch (Exception e) {
            log.error("Error finding user with id: {}", id, e);
            throw new UserServiceException("Failed to find user", e);
        }
    }
}
日志配置
application.yml中的日志配置
# 基本日志配置
logging:
  # 设置日志级别
  level:
    root: INFO                          # 根日志级别
    com.example: DEBUG                  # 包级别配置
    com.example.service: TRACE          # 具体包的日志级别
    org.springframework: WARN           # Spring框架日志级别
    org.hibernate: ERROR                # Hibernate日志级别
  # 日志输出配置
  file:
    name: app.log                       # 日志文件名
    path: /var/logs/                    # 日志文件路径
  # 日志文件配置
  logback:
    rollingpolicy:
      max-file-size: 10MB               # 单个文件最大大小
      max-history: 30                   # 保留的历史文件数量
      total-size-cap: 1GB               # 总日志文件大小限制
  # 日志格式配置
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
    file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
环境特定的日志配置
application-dev.yml(开发环境):
logging:
  level:
    root: DEBUG
    com.example: TRACE
  pattern:
    console: "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr([%thread]){blue} %clr(%-5level){cyan} %clr(%logger{36}){magenta} - %msg%n"
application-prod.yml(生产环境):
logging:
  level:
    root: WARN
    com.example: INFO
  file:
    name: /var/logs/app.log
  logback:
    rollingpolicy:
      max-file-size: 50MB
      max-history: 60
自定义Logback配置
创建logback-spring.xml
在src/main/resources目录下创建logback-spring.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 引入Spring Boot默认配置 -->
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <!-- 定义属性 -->
    <property name="LOG_FILE" value="${LOG_FILE:-app}"/>
    <property name="LOG_PATH" value="${LOG_PATH:-./logs}"/>
    <!-- 控制台输出配置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>
                %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} 
                %clr([%thread]){blue} 
                %clr(%-5level){cyan} 
                %clr(%logger{36}){magenta} 
                %clr(-){faint} %msg%n
            </pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
    <!-- 文件输出配置 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/${LOG_FILE}.log</file>
        <encoder>
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
            </pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 滚动策略 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/${LOG_FILE}-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
    </appender>
    <!-- 错误日志单独输出 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/${LOG_FILE}-error.log</file>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <encoder>
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
            </pattern>
            <charset>UTF-8</charset>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/${LOG_FILE}-error-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
    </appender>
    <!-- 异步日志配置 -->
    <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="FILE"/>
        <queueSize>1024</queueSize>
        <discardingThreshold>0</discardingThreshold>
        <includeCallerData>true</includeCallerData>
    </appender>
    <!-- 环境特定配置 -->
    <springProfile name="dev">
        <logger name="com.example" level="DEBUG"/>
        <root level="INFO">
            <appender-ref ref="CONSOLE"/>
        </root>
    </springProfile>
    <springProfile name="prod">
        <logger name="com.example" level="INFO"/>
        <logger name="org.springframework" level="WARN"/>
        <root level="WARN">
            <appender-ref ref="ASYNC_FILE"/>
            <appender-ref ref="ERROR_FILE"/>
        </root>
    </springProfile>
    <!-- 默认配置 -->
    <springProfile name="!dev & !prod">
        <root level="INFO">
            <appender-ref ref="CONSOLE"/>
            <appender-ref ref="FILE"/>
        </root>
    </springProfile>
</configuration>
高级日志配置
JSON格式日志
适用于日志收集系统(如ELK Stack):
<!-- JSON格式输出 -->
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOG_PATH}/${LOG_FILE}.json</file>
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
        <includeContext>true</includeContext>
        <includeMdc>true</includeMdc>
        <customFields>{"service":"my-app","version":"1.0.0"}</customFields>
    </encoder>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <fileNamePattern>${LOG_PATH}/${LOG_FILE}-%d{yyyy-MM-dd}.%i.json.gz</fileNamePattern>
        <maxFileSize>10MB</maxFileSize>
        <maxHistory>30</maxHistory>
    </rollingPolicy>
</appender>
需要添加依赖:
<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>7.2</version>
</dependency>
MDC(Mapped Diagnostic Context)
MDC用于在日志中添加上下文信息:
@Component
public class LoggingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        // 添加请求ID到MDC
        String requestId = UUID.randomUUID().toString();
        MDC.put("requestId", requestId);
        MDC.put("requestURI", httpRequest.getRequestURI());
        MDC.put("userAgent", httpRequest.getHeader("User-Agent"));
        try {
            chain.doFilter(request, response);
        } finally {
            // 清理MDC
            MDC.clear();
        }
    }
}
在logback配置中使用MDC:
<encoder>
    <pattern>
        %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{requestId}] %logger{36} - %msg%n
    </pattern>
</encoder>
条件日志配置
@Component
public class ConditionalLoggingExample {
    private static final Logger logger = LoggerFactory.getLogger(ConditionalLoggingExample.class);
    public void processData(List<String> data) {
        // 只有在DEBUG级别启用时才执行复杂的日志构建
        if (logger.isDebugEnabled()) {
            logger.debug("Processing data: {}", data.stream()
                    .collect(Collectors.joining(", ", "[", "]")));
        }
        // 对于简单的日志,直接使用占位符
        logger.info("Processing {} items", data.size());
    }
}
性能优化
异步日志
异步日志可以提高应用性能:
<!-- 异步Appender配置 -->
<appender name="ASYNC_CONSOLE" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="CONSOLE"/>
    <queueSize>1024</queueSize>
    <discardingThreshold>0</discardingThreshold>
    <includeCallerData>false</includeCallerData>
    <neverBlock>true</neverBlock>
</appender>
日志级别优化
生产环境建议配置:
logging:
  level:
    root: WARN
    com.yourcompany: INFO
    org.springframework: ERROR
    org.hibernate: ERROR
    com.zaxxer.hikari: WARN
日志监控和分析
集成Micrometer
@RestController
public class UserController {
    private static final Logger logger = LoggerFactory.getLogger(UserController.class);
    private final Counter errorCounter;
    public UserController(MeterRegistry meterRegistry) {
        this.errorCounter = Counter.builder("user.errors")
                .description("User operation errors")
                .register(meterRegistry);
    }
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        try {
            logger.info("Fetching user with id: {}", id);
            return userService.findById(id);
        } catch (Exception e) {
            logger.error("Error fetching user with id: {}", id, e);
            errorCounter.increment();
            throw e;
        }
    }
}
健康检查端点
management:
  endpoints:
    web:
      exposure:
        include: health,info,loggers,metrics
  endpoint:
    loggers:
      enabled: true
可以通过以下端点管理日志:
- GET /actuator/loggers - 查看所有logger配置
- GET /actuator/loggers/{name} - 查看特定logger配置
- POST /actuator/loggers/{name} - 动态修改日志级别
动态修改日志级别:
curl -X POST 'http://localhost:8080/actuator/loggers/com.example.service' \
  -H 'Content-Type: application/json' \
  -d '{"configuredLevel": "DEBUG"}'
最佳实践
1. 日志内容
@Service
public class OrderService {
    private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
    public Order createOrder(OrderRequest request) {
        // 记录关键业务操作
        logger.info("Creating order for user: {}, amount: {}", 
                   request.getUserId(), request.getAmount());
        try {
            Order order = processOrder(request);
            // 记录成功操作
            logger.info("Order created successfully: orderId={}, amount={}", 
                       order.getId(), order.getAmount());
            return order;
        } catch (InsufficientFundsException e) {
            // 记录业务异常(WARN级别)
            logger.warn("Insufficient funds for user: {}, required: {}, available: {}", 
                       request.getUserId(), request.getAmount(), e.getAvailableAmount());
            throw e;
        } catch (Exception e) {
            // 记录系统异常(ERROR级别)
            logger.error("Failed to create order for user: {}", request.getUserId(), e);
            throw new OrderCreationException("Order creation failed", e);
        }
    }
}
2. 避免敏感信息泄露
@Data
public class User {
    private Long id;
    private String username;
    @JsonIgnore
    private String password;  // 密码不会出现在日志中
    @Override
    public String toString() {
        return "User{id=" + id + ", username='" + username + "'}";
    }
}
3. 结构化日志
public class StructuredLogging {
    private static final Logger logger = LoggerFactory.getLogger(StructuredLogging.class);
    public void logUserAction(String action, Long userId, String details) {
        // 使用结构化的方式记录日志
        MDC.put("action", action);
        MDC.put("userId", String.valueOf(userId));
        logger.info("User action completed: {}", details);
        MDC.clear();
    }
}
4. 日志轮转配置
logging:
  logback:
    rollingpolicy:
      max-file-size: 50MB      # 单文件大小
      max-history: 30          # 保留天数
      total-size-cap: 2GB      # 总大小限制
      clean-history-on-start: true  # 启动时清理
小结
Spring Boot的日志系统具有以下特点:
- 开箱即用:默认配置适合大多数场景
 - 灵活配置:支持多种配置方式和格式
 - 性能优化:支持异步日志和条件日志
 - 生产就绪:提供日志轮转、监控等企业级特性
 - 可观测性:与监控系统良好集成
 
正确使用日志可以帮助我们: - 快速定位和解决问题 - 监控应用运行状态 - 分析业务数据和用户行为 - 满足合规和审计要求
记住:好的日志是应用可维护性的重要保障!