SpringBoot实现日志系统,代码世界的“摄像头”与“记事本”

0 阅读8分钟

大家好,我是小悟。

一、日志系统:程序员的“侦探助手”

如果你的程序突然“挂掉”了,你却不知道它死前经历了什么——这比看悬疑电影看到一半停电还难受!日志系统就是你的“侦探助手”,它悄咪咪地记录着程序的一举一动,就像:

  1. 摄像头:谁在什么时候访问了哪个接口
  2. 记事本:程序想了什么、做了什么、遇到了什么挫折
  3. 告密者:偷偷告诉你“老板,数据库又连不上了!”
  4. 时间机器:能让你穿越回错误发生的瞬间

SpringBoot的日志系统就像一个“智能管家”,你不配置它也能工作,但配置好了它就能变成“超级管家”!


二、详细步骤:打造你的“程序监控室”

第1步:创建SpringBoot项目

# 用Spring Initializr创建一个新项目
# 或者用IDE的Spring Initializr功能
# 记得勾选:
# - Spring Web (因为我们要写接口)
# - Lombok (减少代码量,程序员要懒一点)

第2步:基础配置 - 给日志系统“定规矩”

application.yml(或application.properties)中添加:

# application.yml
spring:
  application:
    name: log-system-demo

logging:
  # 日志级别:TRACE < DEBUG < INFO < WARN < ERROR
  level:
    root: INFO  # 根日志级别
    com.example.demo: DEBUG  # 我们的包用DEBUG级别
    org.springframework.web: INFO
    org.hibernate: WARN
    
  # 文件输出配置(让日志有个“家”)
  file:
    name: logs/my-app.log  # 日志文件路径
    max-size: 10MB  # 单个文件最大10MB
    max-history: 30  # 保留30天的日志
    
  # 控制台输出美化(让日志“颜值”更高)
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} - %magenta([%thread]) - %highlight(%-5level) - %cyan(%logger{36}) - %msg%n"
    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
    
  # 日志分组(给日志“分班”)
  group:
    web: org.springframework.core.codec, org.springframework.http
    sql: org.hibernate.SQL, org.springframework.jdbc

第3步:创建日志工具类 - 你的“日志瑞士军刀”

package com.example.demo.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Component
@Slf4j  // Lombok的魔法注解,自动生成log对象
public class LogUtil {
    
    /**
     * 记录方法进入(就像进门前喊“我进来啦!”)
     */
    public void methodEnter(String methodName, Object... params) {
        log.debug("方法 {} 被调用,参数: {}", methodName, params);
    }
    
    /**
     * 记录方法退出(出门说“我走啦!”)
     */
    public void methodExit(String methodName, Object result) {
        log.debug("方法 {} 执行完成,返回值: {}", methodName, result);
    }
    
    /**
     * 记录业务关键点(重要的事说三遍?不,记一遍就行)
     */
    public void businessLog(String template, Object... args) {
        log.info("业务日志: " + template, args);
    }
    
    /**
     * 记录异常(错误发生时大喊“着火啦!”)
     */
    public void error(String message, Throwable e) {
        log.error("发生异常: {} - 异常详情: ", message, e);
    }
    
    /**
     * 慢查询警告(程序说“我...有点卡...”)
     */
    public void slowQuery(long costTime, String query) {
        if (costTime > 1000) {  // 超过1秒
            log.warn("慢查询警告! 耗时: {}ms, SQL: {}", costTime, query);
        }
    }
}

第4步:创建AOP切面 - 给所有方法“装上摄像头”

package com.example.demo.aop;

import com.example.demo.utils.LogUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class LogAspect {
    
    private final LogUtil logUtil;
    
    /**
     * 切点:所有Controller层的方法
     */
    @Pointcut("execution(* com.example.demo.controller..*.*(..))")
    public void controllerPointcut() {}
    
    /**
     * 切点:所有Service层的方法
     */
    @Pointcut("execution(* com.example.demo.service..*.*(..))")
    public void servicePointcut() {}
    
    /**
     * 环绕通知:Controller层日志
     */
    @Around("controllerPointcut()")
    public Object logController(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取请求信息
        ServletRequestAttributes attributes = 
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        
        String requestUrl = "Unknown";
        String httpMethod = "Unknown";
        String ip = "Unknown";
        
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            requestUrl = request.getRequestURL().toString();
            httpMethod = request.getMethod();
            ip = request.getRemoteAddr();
        }
        
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        Object[] args = joinPoint.getArgs();
        
        // 记录请求开始
        log.info("\n========== 请求进入 ==========");
        log.info("URL: {} {}", httpMethod, requestUrl);
        log.info("IP: {}", ip);
        log.info("类: {}.{}", className, methodName);
        log.info("参数: {}", Arrays.toString(args));
        
        long startTime = System.currentTimeMillis();
        Object result;
        
        try {
            // 执行原方法
            result = joinPoint.proceed();
            long costTime = System.currentTimeMillis() - startTime;
            
            // 记录请求完成
            log.info("请求成功,耗时: {}ms", costTime);
            log.info("返回结果: {}", result);
            log.info("========== 请求结束 ==========\n");
            
            return result;
            
        } catch (Exception e) {
            long costTime = System.currentTimeMillis() - startTime;
            
            // 记录异常
            log.error("请求失败,耗时: {}ms", costTime);
            log.error("异常信息: {}", e.getMessage());
            log.info("========== 请求异常结束 ==========\n");
            
            throw e;
        }
    }
    
    /**
     * 环绕通知:Service层日志
     */
    @Around("servicePointcut()")
    public Object logService(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        
        logUtil.methodEnter(methodName, args);
        long startTime = System.currentTimeMillis();
        
        try {
            Object result = joinPoint.proceed();
            long costTime = System.currentTimeMillis() - startTime;
            
            logUtil.methodExit(methodName, result);
            logUtil.slowQuery(costTime, methodName + " 方法执行");
            
            return result;
        } catch (Exception e) {
            logUtil.error("Service方法执行失败: " + methodName, e);
            throw e;
        }
    }
}

第5步:创建Controller和Service - 让日志系统“有活干”

// UserController.java
package com.example.demo.controller;

import com.example.demo.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
@Slf4j
@RequiredArgsConstructor
public class UserController {
    
    private final UserService userService;
    
    @GetMapping("/{id}")
    public String getUser(@PathVariable Long id) {
        log.info("查询用户,ID: {}", id);
        return userService.getUserById(id);
    }
    
    @PostMapping
    public String createUser(@RequestBody String userData) {
        log.info("创建用户,数据: {}", userData);
        // 模拟业务异常
        if ("bad".equals(userData)) {
            throw new RuntimeException("用户数据不合法!");
        }
        return "用户创建成功: " + userData;
    }
}

// UserService.java
package com.example.demo.service;

import com.example.demo.utils.LogUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserService {
    
    private final LogUtil logUtil;
    
    public String getUserById(Long id) {
        logUtil.businessLog("根据ID查询用户,ID: {}", id);
        
        // 模拟数据库查询
        try {
            Thread.sleep(50);  // 模拟耗时
            if (id == 999) {
                throw new RuntimeException("用户不存在!");
            }
            return "用户" + id;
        } catch (InterruptedException e) {
            logUtil.error("查询用户时发生异常", e);
            return "查询失败";
        }
    }
}

第6步:创建全局异常处理 - 给错误“擦屁股”

package com.example.demo.handler;

import com.example.demo.utils.LogUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
@Slf4j
@RequiredArgsConstructor
public class GlobalExceptionHandler {
    
    private final LogUtil logUtil;
    
    @ExceptionHandler(Exception.class)
    public Map<String, Object> handleException(HttpServletRequest request, Exception e) {
        // 记录异常日志
        logUtil.error("全局异常捕获", e);
        
        // 返回友好错误信息
        Map<String, Object> result = new HashMap<>();
        result.put("success", false);
        result.put("message", "服务器开小差了,请稍后再试!");
        result.put("path", request.getRequestURI());
        result.put("timestamp", System.currentTimeMillis());
        
        // 开发环境显示详细错误
        if (isDevelopment()) {
            result.put("error", e.getMessage());
            result.put("stackTrace", e.getStackTrace());
        }
        
        return result;
    }
    
    private boolean isDevelopment() {
        // 这里可以根据配置判断环境
        return true;  // 假设是开发环境
    }
}

第7步:创建日志查看接口(可选) - 给日志开个“后门”

package com.example.demo.controller;

import org.springframework.web.bind.annotation.*;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/log")
public class LogController {
    
    @GetMapping("/tail")
    public List<String> getLogTail(@RequestParam(defaultValue = "100") int lines) {
        List<String> result = new ArrayList<>();
        String logFile = "logs/my-app.log";
        
        try (BufferedReader reader = new BufferedReader(new FileReader(logFile))) {
            List<String> allLines = new ArrayList<>();
            String line;
            
            while ((line = reader.readLine()) != null) {
                allLines.add(line);
            }
            
            // 获取最后N行
            int start = Math.max(0, allLines.size() - lines);
            for (int i = start; i < allLines.size(); i++) {
                result.add(allLines.get(i));
            }
            
        } catch (IOException e) {
            result.add("读取日志文件失败: " + e.getMessage());
        }
        
        return result;
    }
}

第8步:配置文件分离(高级技巧) - 给不同环境“穿不同衣服”

# application-dev.yml (开发环境)
logging:
  level:
    root: DEBUG  # 开发环境详细日志
  file:
    name: logs/dev-app.log
    
# application-prod.yml (生产环境)
logging:
  level:
    root: INFO  # 生产环境精简日志
    com.example.demo: WARN  # 自己的包只记录警告
  file:
    name: /var/log/my-app/app.log  # Linux系统标准日志目录

三、启动和测试

1. 启动应用

# 设置激活的环境
java -jar demo.jar --spring.profiles.active=dev

2. 测试接口

# 正常请求
curl http://localhost:8080/users/1

# 触发异常
curl -X POST http://localhost:8080/users -d "bad"

# 查看日志
curl http://localhost:8080/log/tail?lines=50

3. 观察控制台输出

你会看到彩色高亮的日志:

2026-01-21 10:30:25 - [http-nio-8080-exec-1] - INFO  - c.e.demo.controller.UserController - 查询用户,ID: 1
2026-01-21 10:30:25 - [http-nio-8080-exec-1] - DEBUG - c.e.demo.aop.LogAspect - 方法 getUserById 被调用,参数: [1]

四、高级功能扩展

1. 添加日志脱敏(保护敏感信息)

@Component
public class LogSensitiveFilter {
    public String filterSensitive(String logContent) {
        // 脱敏手机号
        logContent = logContent.replaceAll("(1[3-9]\\d{9})", "$1****");
        // 脱敏身份证
        logContent = logContent.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1**********$2");
        // 脱敏邮箱
        logContent = logContent.replaceAll("(\\w{3})(\\w+)(@\\w+\\.\\w+)", "$1****$3");
        return logContent;
    }
}

2. 集成ELK(日志分析全家桶)

# 添加Logstash依赖
dependencies:
  implementation 'net.logstash.logback:logstash-logback-encoder:7.0'

配置logback-spring.xml

<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
    <destination>localhost:5000</destination>
    <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
        <providers>
            <timestamp/>
            <logLevel/>
            <loggerName/>
            <message/>
            <mdc/>
            <stackTrace/>
        </providers>
    </encoder>
</appender>

3. 自定义Appender(发送到企业微信/钉钉)

public class DingTalkAppender extends AppenderBase<ILoggingEvent> {
    @Override
    protected void append(ILoggingEvent event) {
        if (event.getLevel().isGreaterOrEqual(Level.ERROR)) {
            String message = String.format("【系统告警】\n时间: %s\n级别: %s\n消息: %s",
                new Date(event.getTimeStamp()),
                event.getLevel(),
                event.getFormattedMessage());
            // 调用钉钉机器人API
            sendToDingTalk(message);
        }
    }
}

五、总结:日志系统的“生存法则”

1. 日志不是越多越好

就像吃饭不是越多越好一样,日志也要“适量”:

  • DEBUG级别:开发环境用,生产环境关掉
  • INFO级别:记录关键业务路径
  • WARN级别:需要关注但不紧急的问题
  • ERROR级别:必须立即处理的问题

2. 日志要“有意义”

糟糕的日志:用户操作完成 好的日志:用户[张三]于[2026-01-21 10:30:25]完成了订单[202601210001]的支付,金额[299.00]元

3. 结构化日志是趋势

{
  "timestamp": "2026-01-21T10:30:25.123Z",
  "level": "INFO",
  "service": "user-service",
  "traceId": "abc-123-def-456",
  "userId": "user_001",
  "action": "place_order",
  "details": {
    "orderId": "202601210001",
    "amount": 299.00
  }
}

4. 性能很重要

  • 使用异步日志:AsyncAppender
  • 避免在日志中拼接大字符串
  • 生产环境关掉不必要的日志级别

5. 安全不能忘

  • 敏感信息必须脱敏
  • 日志文件要设置权限
  • 生产环境日志不能包含调试信息

6. 监控告警要跟上

  • 错误日志实时告警
  • 慢查询统计
  • 接口调用量监控

六、最后

  1. 写日志就像写日记:不仅要记录“做了什么”,还要记录“为什么这么做”
  2. 日志是给“未来的你”看的:想象一下凌晨3点被报警电话叫醒,清晰的日志能让你少掉几根头发
  3. 日志不是万能的:关键业务逻辑该有监控还要有监控,该有告警还要有告警
  4. 定期review日志:就像定期体检,能发现潜在问题

一个好的日志系统就像一位可靠的“副驾驶”,在你开车的路上,它不会打扰你,但会在你需要的时候,准确地告诉你:

  • “前面有坑!”(ERROR)
  • “油不多了”(WARN)
  • “风景不错”(INFO)
  • “我在记录一切”(DEBUG)

去给你的SpringBoot应用装上这个“智能行车记录仪”吧!你的程序会感谢你,你的同事会感谢你,凌晨三点被叫醒处理问题的那个你,更会感谢你!

SpringBoot实现日志系统,代码世界的“摄像头”与“记事本”.png

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。

您的一键三连,是我更新的最大动力,谢谢

山水有相逢,来日皆可期,谢谢阅读,我们再会

我手中的金箍棒,上能通天,下能探海