大家好,我是小悟。
一、日志系统:程序员的“侦探助手”
如果你的程序突然“挂掉”了,你却不知道它死前经历了什么——这比看悬疑电影看到一半停电还难受!日志系统就是你的“侦探助手”,它悄咪咪地记录着程序的一举一动,就像:
- 摄像头:谁在什么时候访问了哪个接口
- 记事本:程序想了什么、做了什么、遇到了什么挫折
- 告密者:偷偷告诉你“老板,数据库又连不上了!”
- 时间机器:能让你穿越回错误发生的瞬间
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=dev2. 测试接口
# 正常请求 curl http://localhost:8080/users/1 # 触发异常 curl -X POST http://localhost:8080/users -d "bad" # 查看日志 curl http://localhost:8080/log/tail?lines=503. 观察控制台输出
你会看到彩色高亮的日志:
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.监控告警要跟上
- 错误日志实时告警
- 慢查询统计
- 接口调用量监控
六、最后
- 写日志就像写日记:不仅要记录“做了什么”,还要记录“为什么这么做”
- 日志是给“未来的你”看的:想象一下凌晨3点被报警电话叫醒,清晰的日志能让你少掉几根头发
- 日志不是万能的:关键业务逻辑该有监控还要有监控,该有告警还要有告警
- 定期review日志:就像定期体检,能发现潜在问题
一个好的日志系统就像一位可靠的“副驾驶”,在你开车的路上,它不会打扰你,但会在你需要的时候,准确地告诉你:
- “前面有坑!”(ERROR)
- “油不多了”(WARN)
- “风景不错”(INFO)
- “我在记录一切”(DEBUG)
去给你的SpringBoot应用装上这个“智能行车记录仪”吧!你的程序会感谢你,你的同事会感谢你,凌晨三点被叫醒处理问题的那个你,更会感谢你!
谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海