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