SpringBoot實(shí)現(xiàn)日志系統(tǒng)的完整代碼
一、日志系統(tǒng):程序員的“偵探助手”
如果你的程序突然“掛掉”了,你卻不知道它死前經(jīng)歷了什么——這比看懸疑電影看到一半停電還難受!日志系統(tǒng)就是你的“偵探助手”,它悄咪咪地記錄著程序的一舉一動(dòng),就像:
- 攝像頭:誰(shuí)在什么時(shí)候訪問(wèn)了哪個(gè)接口
- 記事本:程序想了什么、做了什么、遇到了什么挫折
- 告密者:偷偷告訴你“老板,數(shù)據(jù)庫(kù)又連不上了!”
- 時(shí)間機(jī)器:能讓你穿越回錯(cuò)誤發(fā)生的瞬間
SpringBoot的日志系統(tǒng)就像一個(gè)“智能管家”,你不配置它也能工作,但配置好了它就能變成“超級(jí)管家”!
二、詳細(xì)步驟:打造你的“程序監(jiān)控室”
第1步:創(chuàng)建SpringBoot項(xiàng)目
# 用Spring Initializr創(chuàng)建一個(gè)新項(xiàng)目 # 或者用IDE的Spring Initializr功能 # 記得勾選: # - Spring Web (因?yàn)槲覀円獙懡涌? # - Lombok (減少代碼量,程序員要懶一點(diǎn))
第2步:基礎(chǔ)配置 - 給日志系統(tǒng)“定規(guī)矩”
在application.yml(或application.properties)中添加:
# application.yml
spring:
application:
name: log-system-demo
logging:
# 日志級(jí)別:TRACE < DEBUG < INFO < WARN < ERROR
level:
root: INFO # 根日志級(jí)別
com.example.demo: DEBUG # 我們的包用DEBUG級(jí)別
org.springframework.web: INFO
org.hibernate: WARN
# 文件輸出配置(讓日志有個(gè)“家”)
file:
name: logs/my-app.log # 日志文件路徑
max-size: 10MB # 單個(gè)文件最大10MB
max-history: 30 # 保留30天的日志
# 控制臺(tái)輸出美化(讓日志“顏值”更高)
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步:創(chuàng)建日志工具類
package com.example.demo.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Component
@Slf4j // Lombok的魔法注解,自動(dòng)生成log對(duì)象
public class LogUtil {
/**
* 記錄方法進(jìn)入(就像進(jìn)門前喊“我進(jìn)來(lái)啦!”)
*/
public void methodEnter(String methodName, Object... params) {
log.debug("方法 {} 被調(diào)用,參數(shù): {}", methodName, params);
}
/**
* 記錄方法退出(出門說(shuō)“我走啦!”)
*/
public void methodExit(String methodName, Object result) {
log.debug("方法 {} 執(zhí)行完成,返回值: {}", methodName, result);
}
/**
* 記錄業(yè)務(wù)關(guān)鍵點(diǎn)(重要的事說(shuō)三遍?不,記一遍就行)
*/
public void businessLog(String template, Object... args) {
log.info("業(yè)務(wù)日志: " + template, args);
}
/**
* 記錄異常(錯(cuò)誤發(fā)生時(shí)大喊“著火啦!”)
*/
public void error(String message, Throwable e) {
log.error("發(fā)生異常: {} - 異常詳情: ", message, e);
}
/**
* 慢查詢警告(程序說(shuō)“我...有點(diǎn)卡...”)
*/
public void slowQuery(long costTime, String query) {
if (costTime > 1000) { // 超過(guò)1秒
log.warn("慢查詢警告! 耗時(shí): {}ms, SQL: {}", costTime, query);
}
}
}
第4步:創(chuàng)建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;
/**
* 切點(diǎn):所有Controller層的方法
*/
@Pointcut("execution(* com.example.demo.controller..*.*(..))")
public void controllerPointcut() {}
/**
* 切點(diǎn):所有Service層的方法
*/
@Pointcut("execution(* com.example.demo.service..*.*(..))")
public void servicePointcut() {}
/**
* 環(huán)繞通知:Controller層日志
*/
@Around("controllerPointcut()")
public Object logController(ProceedingJoinPoint joinPoint) throws Throwable {
// 獲取請(qǐng)求信息
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();
// 記錄請(qǐng)求開始
log.info("\n========== 請(qǐng)求進(jìn)入 ==========");
log.info("URL: {} {}", httpMethod, requestUrl);
log.info("IP: {}", ip);
log.info("類: {}.{}", className, methodName);
log.info("參數(shù): {}", Arrays.toString(args));
long startTime = System.currentTimeMillis();
Object result;
try {
// 執(zhí)行原方法
result = joinPoint.proceed();
long costTime = System.currentTimeMillis() - startTime;
// 記錄請(qǐng)求完成
log.info("請(qǐng)求成功,耗時(shí): {}ms", costTime);
log.info("返回結(jié)果: {}", result);
log.info("========== 請(qǐng)求結(jié)束 ==========\n");
return result;
} catch (Exception e) {
long costTime = System.currentTimeMillis() - startTime;
// 記錄異常
log.error("請(qǐng)求失敗,耗時(shí): {}ms", costTime);
log.error("異常信息: {}", e.getMessage());
log.info("========== 請(qǐng)求異常結(jié)束 ==========\n");
throw e;
}
}
/**
* 環(huán)繞通知: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 + " 方法執(zhí)行");
return result;
} catch (Exception e) {
logUtil.error("Service方法執(zhí)行失敗: " + methodName, e);
throw e;
}
}
}
第5步:創(chuàng)建Controller和Service - 讓日志系統(tǒng)“有活干”
// 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("創(chuàng)建用戶,數(shù)據(jù): {}", userData);
// 模擬業(yè)務(wù)異常
if ("bad".equals(userData)) {
throw new RuntimeException("用戶數(shù)據(jù)不合法!");
}
return "用戶創(chuàng)建成功: " + 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("根據(jù)ID查詢用戶,ID: {}", id);
// 模擬數(shù)據(jù)庫(kù)查詢
try {
Thread.sleep(50); // 模擬耗時(shí)
if (id == 999) {
throw new RuntimeException("用戶不存在!");
}
return "用戶" + id;
} catch (InterruptedException e) {
logUtil.error("查詢用戶時(shí)發(fā)生異常", e);
return "查詢失敗";
}
}
}
第6步:創(chuàng)建全局異常處理 - 給錯(cuò)誤“擦屁股”
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);
// 返回友好錯(cuò)誤信息
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", "服務(wù)器開小差了,請(qǐng)稍后再試!");
result.put("path", request.getRequestURI());
result.put("timestamp", System.currentTimeMillis());
// 開發(fā)環(huán)境顯示詳細(xì)錯(cuò)誤
if (isDevelopment()) {
result.put("error", e.getMessage());
result.put("stackTrace", e.getStackTrace());
}
return result;
}
private boolean isDevelopment() {
// 這里可以根據(jù)配置判斷環(huán)境
return true; // 假設(shè)是開發(fā)環(huán)境
}
}
第7步:創(chuàng)建日志查看接口(可選) - 給日志開個(gè)“后門”
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步:配置文件分離(高級(jí)技巧) - 給不同環(huán)境“穿不同衣服”
# application-dev.yml (開發(fā)環(huán)境)
logging:
level:
root: DEBUG # 開發(fā)環(huán)境詳細(xì)日志
file:
name: logs/dev-app.log
# application-prod.yml (生產(chǎn)環(huán)境)
logging:
level:
root: INFO # 生產(chǎn)環(huán)境精簡(jiǎn)日志
com.example.demo: WARN # 自己的包只記錄警告
file:
name: /var/log/my-app/app.log # Linux系統(tǒng)標(biāo)準(zhǔn)日志目錄
三、啟動(dòng)和測(cè)試
1. 啟動(dòng)應(yīng)用
# 設(shè)置激活的環(huán)境 java -jar demo.jar --spring.profiles.active=dev
2. 測(cè)試接口
# 正常請(qǐng)求 curl http://localhost:8080/users/1 # 觸發(fā)異常 curl -X POST http://localhost:8080/users -d "bad" # 查看日志 curl http://localhost:8080/log/tail?lines=50
3. 觀察控制臺(tái)輸出
你會(huì)看到彩色高亮的日志:
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 被調(diào)用,參數(shù): [1]
四、高級(jí)功能擴(kuò)展
1. 添加日志脫敏(保護(hù)敏感信息)
@Component
public class LogSensitiveFilter {
public String filterSensitive(String logContent) {
// 脫敏手機(jī)號(hào)
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(發(fā)送到企業(yè)微信/釘釘)
public class DingTalkAppender extends AppenderBase<ILoggingEvent> {
@Override
protected void append(ILoggingEvent event) {
if (event.getLevel().isGreaterOrEqual(Level.ERROR)) {
String message = String.format("【系統(tǒng)告警】\n時(shí)間: %s\n級(jí)別: %s\n消息: %s",
new Date(event.getTimeStamp()),
event.getLevel(),
event.getFormattedMessage());
// 調(diào)用釘釘機(jī)器人API
sendToDingTalk(message);
}
}
}
五、總結(jié):日志系統(tǒng)的“生存法則”
1.日志不是越多越好
就像吃飯不是越多越好一樣,日志也要“適量”:
- DEBUG級(jí)別:開發(fā)環(huán)境用,生產(chǎn)環(huán)境關(guān)掉
- INFO級(jí)別:記錄關(guān)鍵業(yè)務(wù)路徑
- WARN級(jí)別:需要關(guān)注但不緊急的問(wèn)題
- ERROR級(jí)別:必須立即處理的問(wèn)題
2.日志要“有意義”
糟糕的日志:用戶操作完成 好的日志:用戶[張三]于[2026-01-21 10:30:25]完成了訂單[202601210001]的支付,金額[299.00]元
3.結(jié)構(gòu)化日志是趨勢(shì)
{
"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 - 避免在日志中拼接大字符串
- 生產(chǎn)環(huán)境關(guān)掉不必要的日志級(jí)別
5.安全不能忘
- 敏感信息必須脫敏
- 日志文件要設(shè)置權(quán)限
- 生產(chǎn)環(huán)境日志不能包含調(diào)試信息
6.監(jiān)控告警要跟上
- 錯(cuò)誤日志實(shí)時(shí)告警
- 慢查詢統(tǒng)計(jì)
- 接口調(diào)用量監(jiān)控
六、最后
- 寫日志就像寫日記:不僅要記錄“做了什么”,還要記錄“為什么這么做”
- 日志是給“未來(lái)的你”看的:想象一下凌晨3點(diǎn)被報(bào)警電話叫醒,清晰的日志能讓你少掉幾根頭發(fā)
- 日志不是萬(wàn)能的:關(guān)鍵業(yè)務(wù)邏輯該有監(jiān)控還要有監(jiān)控,該有告警還要有告警
- 定期review日志:就像定期體檢,能發(fā)現(xiàn)潛在問(wèn)題
一個(gè)好的日志系統(tǒng)就像一位可靠的“副駕駛”,在你開車的路上,它不會(huì)打擾你,但會(huì)在你需要的時(shí)候,準(zhǔn)確地告訴你:
- “前面有坑!”(ERROR)
- “油不多了”(WARN)
- “風(fēng)景不錯(cuò)”(INFO)
- “我在記錄一切”(DEBUG)
去給你的SpringBoot應(yīng)用裝上這個(gè)“智能行車記錄儀”吧!你的程序會(huì)感謝你,你的同事會(huì)感謝你,凌晨三點(diǎn)被叫醒處理問(wèn)題的那個(gè)你,更會(huì)感謝你!
以上就是SpringBoot實(shí)現(xiàn)日志系統(tǒng)的完整代碼的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot日志系統(tǒng)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- SpringBoot實(shí)現(xiàn)日志系統(tǒng)的完整指南
- SpringBoot使用AOP優(yōu)雅實(shí)現(xiàn)系統(tǒng)操作日志的持久化
- 基于SpringBoot實(shí)現(xiàn)簡(jiǎn)單的ELK日志搜索系統(tǒng)
- 如何自定義springboot-starter日志組件供各個(gè)服務(wù)使用(系統(tǒng)日志優(yōu)化)
- SpringBoot打印系統(tǒng)執(zhí)行的sql語(yǔ)句及日志配置指南
- springboot項(xiàng)目配置logback日志系統(tǒng)的實(shí)現(xiàn)
- Springboot項(xiàng)目保存本地系統(tǒng)日志文件的實(shí)現(xiàn)方法
相關(guān)文章
SpringBoot集成Mybatis-plus并實(shí)現(xiàn)自動(dòng)生成相關(guān)文件的示例代碼
Mybatis-Plus是一個(gè)優(yōu)秀的Mybatis增強(qiáng)工具,目前更新到3.1.1,本文通過(guò)示例代碼給大家介紹SpringBoot集成Mybatis-plus并實(shí)現(xiàn)自動(dòng)生成相關(guān)文件的問(wèn)題,感興趣的朋友跟隨小編一起看看吧2021-12-12
Spring?Retry優(yōu)雅地實(shí)現(xiàn)方法重試機(jī)制
Spring?Retry?是?Spring?提供的一個(gè)模塊,它可以幫助我們以聲明式的方式為方法添加重試功能,從而提升系統(tǒng)的健壯性和可用性,下面我們就來(lái)看看如何使用Spring?Retry實(shí)現(xiàn)方法重試機(jī)制吧2025-06-06
淺談java項(xiàng)目與javaweb項(xiàng)目導(dǎo)入jar包的區(qū)別
下面小編就為大家分享一篇淺談java項(xiàng)目與javaweb項(xiàng)目導(dǎo)入jar包的區(qū)別,具有很好的參考價(jià)值。希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2017-11-11
java鎖機(jī)制ReentrantLock源碼實(shí)例分析
這篇文章主要為大家介紹了java鎖機(jī)制ReentrantLock源碼實(shí)例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
springboot2自動(dòng)加載sql文件的實(shí)現(xiàn)
本文主要介紹了springboot2自動(dòng)加載sql文件的實(shí)現(xiàn),通過(guò)配置文件或注解的方式,我們可以輕松地將SQL語(yǔ)句映射到數(shù)據(jù)庫(kù)中,實(shí)現(xiàn)自動(dòng)加載,感興趣的可以了解一下2023-11-11

