Spring Boot/Spring Cloud 應(yīng)用日志書寫詳細(xì)實(shí)例指南
Spring Boot/Spring Cloud 應(yīng)用日志書寫詳細(xì)指南
1. 日志框架選擇與配置
1.1 推薦日志框架組合
<!-- Spring Boot 默認(rèn)使用,無需額外引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- 如需使用 Log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>1.2 基礎(chǔ)配置
# application.yml
logging:
level:
com.yourcompany: INFO
org.springframework: WARN
org.hibernate: ERROR
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"
file:
name: logs/application.log
logback:
rollingpolicy:
max-file-size: 10MB
max-history: 302. 日志級(jí)別規(guī)范
2.1 級(jí)別定義與使用場(chǎng)景
@Component
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
public User createUser(UserDTO userDTO) {
// TRACE: 詳細(xì)的調(diào)試信息,通常在生產(chǎn)環(huán)境關(guān)閉
logger.trace("開始創(chuàng)建用戶,輸入?yún)?shù): {}", userDTO);
// DEBUG: 調(diào)試信息,用于開發(fā)階段問題定位
logger.debug("用戶數(shù)據(jù)驗(yàn)證通過,準(zhǔn)備持久化");
try {
User user = userRepository.save(convertToEntity(userDTO));
// INFO: 重要的業(yè)務(wù)流程節(jié)點(diǎn)
logger.info("用戶創(chuàng)建成功,用戶ID: {}, 用戶名: {}", user.getId(), user.getUsername());
return user;
} catch (DataIntegrityViolationException e) {
// WARN: 非預(yù)期但可處理的情況
logger.warn("用戶數(shù)據(jù)重復(fù),用戶名: {}", userDTO.getUsername(), e);
throw new BusinessException("用戶已存在");
} catch (Exception e) {
// ERROR: 系統(tǒng)錯(cuò)誤和異常情況
logger.error("用戶創(chuàng)建失敗,用戶名: {}", userDTO.getUsername(), e);
throw new SystemException("系統(tǒng)內(nèi)部錯(cuò)誤");
}
}
}3. 日志內(nèi)容規(guī)范
3.1 結(jié)構(gòu)化日志格式
@Service
@Slf4j // 推薦使用 Lombok @Slf4j 注解
public class OrderService {
public Order createOrder(OrderRequest request) {
// 關(guān)鍵業(yè)務(wù)操作開始日志
log.info("創(chuàng)建訂單開始 | 用戶ID:{} | 商品數(shù)量:{} | 總金額:{}",
request.getUserId(),
request.getItems().size(),
request.getTotalAmount());
try {
// 業(yè)務(wù)邏輯處理
Order order = processOrder(request);
// 成功日志包含關(guān)鍵業(yè)務(wù)ID
log.info("訂單創(chuàng)建成功 | 訂單ID:{} | 狀態(tài):{} | 用戶ID:{}",
order.getId(),
order.getStatus(),
order.getUserId());
return order;
} catch (InventoryException e) {
// 異常日志包含上下文信息和異常
log.error("庫存不足創(chuàng)建訂單失敗 | 用戶ID:{} | 錯(cuò)誤碼:{}",
request.getUserId(),
"INSUFFICIENT_INVENTORY", e);
throw e;
}
}
}3.2 JSON 結(jié)構(gòu)化日志(推薦用于微服務(wù))
@Component
public class StructuredLogger {
public void logPaymentEvent(PaymentEvent event) {
Map<String, Object> logData = new HashMap<>();
logData.put("event", "payment_processed");
logData.put("orderId", event.getOrderId());
logData.put("amount", event.getAmount());
logData.put("paymentMethod", event.getPaymentMethod());
logData.put("timestamp", Instant.now().toString());
logData.put("traceId", MDC.get("traceId"));
log.info(JSON.toJSONString(logData));
}
public void logError(String errorCode, String message, String module, Throwable t) {
Map<String, Object> errorLog = new HashMap<>();
errorLog.put("level", "ERROR");
errorLog.put("errorCode", errorCode);
errorLog.put("message", message);
errorLog.put("module", module);
errorLog.put("timestamp", Instant.now().toString());
errorLog.put("stackTrace", getStackTrace(t));
errorLog.put("traceId", MDC.get("traceId"));
log.error(JSON.toJSONString(errorLog));
}
private String getStackTrace(Throwable t) {
StringWriter sw = new StringWriter();
t.printStackTrace(new PrintWriter(sw));
return sw.toString();
}
}4. 日志上下文信息
4.1 使用 MDC (Mapped Diagnostic Context)
@Aspect
@Component
@Slf4j
public class LoggingAspect {
@Around("execution(* com.yourcompany..*Controller.*(..))")
public Object logController(ProceedingJoinPoint joinPoint) throws Throwable {
// 設(shè)置請(qǐng)求上下文
MDC.put("traceId", generateTraceId());
MDC.put("userId", getCurrentUserId());
MDC.put("clientIp", getClientIp());
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
log.info("請(qǐng)求開始 | 類:{} | 方法:{}", className, methodName);
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - startTime;
log.info("請(qǐng)求完成 | 類:{} | 方法:{} | 耗時(shí):{}ms",
className, methodName, executionTime);
return result;
} catch (Exception e) {
long executionTime = System.currentTimeMillis() - startTime;
log.error("請(qǐng)求異常 | 類:{} | 方法:{} | 耗時(shí):{}ms | 異常:{}",
className, methodName, executionTime, e.getMessage(), e);
throw e;
} finally {
// 清理 MDC
MDC.clear();
}
}
}4.2 請(qǐng)求鏈路追蹤
@Component
public class TraceFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String traceId = httpRequest.getHeader("X-Trace-Id");
if (traceId == null || traceId.isEmpty()) {
traceId = generateTraceId();
}
MDC.put("traceId", traceId);
MDC.put("requestUri", httpRequest.getRequestURI());
MDC.put("userAgent", httpRequest.getHeader("User-Agent"));
try {
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}5. 性能優(yōu)化建議
5.1 避免日志性能問題
@Service
@Slf4j
public class PerformanceSensitiveService {
// 不好的寫法:在調(diào)試級(jí)別進(jìn)行字符串拼接
public void badLogging(User user) {
// 即使日志級(jí)別高于DEBUG,字符串拼接仍然會(huì)執(zhí)行
log.debug("用戶信息: " + user.toString() + ", 時(shí)間: " + System.currentTimeMillis());
}
// 好的寫法:使用參數(shù)化日志
public void goodLogging(User user) {
// 只有在DEBUG級(jí)別啟用時(shí)才會(huì)進(jìn)行字符串處理
log.debug("用戶信息: {}, 時(shí)間: {}", user, System.currentTimeMillis());
}
// 對(duì)于復(fù)雜的日志消息,使用條件判斷
public void conditionalLogging(List<User> users) {
if (log.isDebugEnabled()) {
log.debug("處理用戶列表: {}", formatUserList(users)); // formatUserList 比較耗時(shí)
}
}
}6. 異常日志處理
6.1 統(tǒng)一的異常日志處理
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
// WARN 級(jí)別記錄業(yè)務(wù)異常
log.warn("業(yè)務(wù)異常 | 錯(cuò)誤碼:{} | 錯(cuò)誤信息:{} | 請(qǐng)求路徑:{}",
e.getErrorCode(), e.getMessage(), getCurrentRequestPath());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(e.getErrorCode(), e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleSystemException(Exception e) {
// ERROR 級(jí)別記錄系統(tǒng)異常
log.error("系統(tǒng)異常 | 異常類型:{} | 請(qǐng)求路徑:{}",
e.getClass().getSimpleName(), getCurrentRequestPath(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("SYSTEM_ERROR", "系統(tǒng)內(nèi)部錯(cuò)誤"));
}
}7. 日志配置文件示例
7.1 Logback 配置 (logback-spring.xml)
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{traceId}] - %msg%n"/>
<property name="LOG_PATH" value="logs"/>
<!-- 控制臺(tái)輸出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 文件輸出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/application.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/application.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
</appender>
<!-- 錯(cuò)誤日志單獨(dú)文件 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/error.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
<!-- 特定包日志級(jí)別 -->
<logger name="com.yourcompany" level="DEBUG"/>
<logger name="org.springframework" level="WARN"/>
<logger name="org.hibernate" level="WARN"/>
</configuration>8. 最佳實(shí)踐總結(jié)
8.1 必須遵循的原則
- 適當(dāng)?shù)娜罩炯?jí)別:生產(chǎn)環(huán)境使用 INFO,開發(fā)環(huán)境使用 DEBUG
- 有意義的日志消息:包含足夠的上下文信息
- 統(tǒng)一格式:團(tuán)隊(duì)使用統(tǒng)一的日志格式
- 性能考慮:避免在日志中執(zhí)行耗時(shí)操作
- 安全考慮:不要記錄敏感信息(密碼、token等)
8.2 推薦的日志場(chǎng)景
// 1. 方法入口和出口
log.info("方法開始: {},參數(shù): {}", methodName, parameters);
log.info("方法結(jié)束: {},結(jié)果: {}", methodName, result);
// 2. 業(yè)務(wù)流程關(guān)鍵節(jié)點(diǎn)
log.info("訂單狀態(tài)變更 | 訂單ID:{} | 原狀態(tài):{} | 新狀態(tài):{}", orderId, oldStatus, newStatus);
// 3. 外部調(diào)用
log.info("調(diào)用外部服務(wù)開始 | 服務(wù)名:{} | 請(qǐng)求參數(shù):{}", serviceName, request);
log.info("調(diào)用外部服務(wù)結(jié)束 | 服務(wù)名:{} | 響應(yīng)結(jié)果:{}", serviceName, response);
// 4. 異常情況
log.error("數(shù)據(jù)庫操作失敗 | SQL:{} | 參數(shù):{}", sql, params, e);
// 5. 定時(shí)任務(wù)
log.info("定時(shí)任務(wù)開始 | 任務(wù)名:{} | 執(zhí)行時(shí)間:{}", taskName, startTime);
log.info("定時(shí)任務(wù)結(jié)束 | 任務(wù)名:{} | 執(zhí)行結(jié)果:{} | 耗時(shí):{}ms", taskName, result, costTime);通過遵循這些指南,可以建立統(tǒng)一、可維護(hù)、高效的日志系統(tǒng),大大提高系統(tǒng)的可觀測(cè)性和問題排查效率。
Spring Boot/Cloud 全場(chǎng)景日志書寫示例
1. 控制器層日志
1.1 REST Controller 完整示例
@RestController
@RequestMapping("/api/v1/users")
@Slf4j
public class UserController {
@GetMapping("/{userId}")
public ResponseEntity<UserVO> getUser(@PathVariable Long userId,
@RequestHeader("User-Agent") String userAgent) {
// 請(qǐng)求入口日志
log.info("查詢用戶開始 | 用戶ID:{} | 客戶端:{} | 線程:{}",
userId, userAgent, Thread.currentThread().getName());
long startTime = System.currentTimeMillis();
try {
UserVO user = userService.getUserById(userId);
// 成功響應(yīng)日志
log.info("查詢用戶成功 | 用戶ID:{} | 用戶名:{} | 耗時(shí):{}ms",
userId, user.getUsername(), System.currentTimeMillis() - startTime);
return ResponseEntity.ok(user);
} catch (UserNotFoundException e) {
// 業(yè)務(wù)異常日志
log.warn("用戶不存在 | 用戶ID:{} | 耗時(shí):{}ms",
userId, System.currentTimeMillis() - startTime, e);
return ResponseEntity.notFound().build();
} catch (Exception e) {
// 系統(tǒng)異常日志
log.error("查詢用戶異常 | 用戶ID:{} | 異常類型:{} | 耗時(shí):{}ms",
userId, e.getClass().getSimpleName(),
System.currentTimeMillis() - startTime, e);
return ResponseEntity.internalServerError().build();
}
}
@PostMapping
public ResponseEntity<CreateUserResponse> createUser(@Valid @RequestBody CreateUserRequest request,
HttpServletRequest httpRequest) {
String clientIp = httpRequest.getRemoteAddr();
log.info("創(chuàng)建用戶開始 | 用戶名:{} | 郵箱:{} | 客戶端IP:{}",
request.getUsername(), request.getEmail(), clientIp);
// 敏感信息脫敏
log.debug("創(chuàng)建用戶詳細(xì)參數(shù) | 用戶名:{} | 角色:{} | 部門:{}",
request.getUsername(), request.getRole(), request.getDepartment());
long startTime = System.currentTimeMillis();
try {
UserVO user = userService.createUser(request);
log.info("創(chuàng)建用戶成功 | 用戶ID:{} | 用戶名:{} | 耗時(shí):{}ms",
user.getId(), user.getUsername(), System.currentTimeMillis() - startTime);
return ResponseEntity.status(HttpStatus.CREATED)
.body(new CreateUserResponse(user.getId(), "創(chuàng)建成功"));
} catch (DuplicateUserException e) {
log.warn("創(chuàng)建用戶失敗-用戶已存在 | 用戶名:{} | 郵箱:{}",
request.getUsername(), request.getEmail(), e);
return ResponseEntity.badRequest()
.body(new CreateUserResponse(null, "用戶已存在"));
}
}
@PutMapping("/{userId}/status")
public ResponseEntity<Void> updateUserStatus(@PathVariable Long userId,
@RequestBody UpdateStatusRequest request) {
log.info("更新用戶狀態(tài)開始 | 用戶ID:{} | 原狀態(tài):{} | 新狀態(tài):{}",
userId, getCurrentStatus(userId), request.getStatus());
userService.updateStatus(userId, request.getStatus());
log.info("更新用戶狀態(tài)成功 | 用戶ID:{} | 新狀態(tài):{}", userId, request.getStatus());
return ResponseEntity.ok().build();
}
}2. 服務(wù)層日志
2.1 業(yè)務(wù)服務(wù)完整示例
@Service
@Slf4j
@Transactional
public class OrderService {
public Order createOrder(CreateOrderRequest request) {
log.info("創(chuàng)建訂單開始 | 用戶ID:{} | 商品數(shù)量:{} | 總金額:{}",
request.getUserId(), request.getItems().size(), request.getTotalAmount());
// 參數(shù)驗(yàn)證日志
if (log.isDebugEnabled()) {
log.debug("訂單詳細(xì)參數(shù) | 商品列表:{} | 收貨地址:{}",
request.getItems(), maskSensitiveInfo(request.getAddress()));
}
try {
// 1. 驗(yàn)證庫存
log.debug("開始驗(yàn)證庫存 | 商品數(shù)量:{}", request.getItems().size());
inventoryService.validateStock(request.getItems());
log.debug("庫存驗(yàn)證通過");
// 2. 計(jì)算價(jià)格
log.debug("開始計(jì)算訂單價(jià)格");
BigDecimal finalAmount = calculateFinalAmount(request);
log.debug("價(jià)格計(jì)算完成 | 最終金額:{}", finalAmount);
// 3. 創(chuàng)建訂單
Order order = buildOrder(request, finalAmount);
order = orderRepository.save(order);
log.info("訂單創(chuàng)建完成 | 訂單ID:{} | 訂單號(hào):{} | 狀態(tài):{}",
order.getId(), order.getOrderNo(), order.getStatus());
// 4. 扣減庫存
log.debug("開始扣減庫存 | 訂單ID:{}", order.getId());
inventoryService.deductStock(order.getId(), request.getItems());
log.debug("庫存扣減完成");
// 5. 發(fā)送創(chuàng)建事件
eventPublisher.publishEvent(new OrderCreatedEvent(order));
log.debug("訂單創(chuàng)建事件已發(fā)布");
log.info("訂單流程全部完成 | 訂單ID:{} | 總耗時(shí):{}ms",
order.getId(), System.currentTimeMillis() - startTime);
return order;
} catch (InventoryException e) {
log.error("創(chuàng)建訂單失敗-庫存不足 | 用戶ID:{} | 錯(cuò)誤詳情:{}",
request.getUserId(), e.getMessage(), e);
throw new BusinessException("庫存不足,創(chuàng)建訂單失敗");
} catch (PaymentException e) {
log.error("創(chuàng)建訂單失敗-支付異常 | 用戶ID:{} | 錯(cuò)誤碼:{}",
request.getUserId(), e.getErrorCode(), e);
throw new BusinessException("支付處理失敗");
} catch (Exception e) {
log.error("創(chuàng)建訂單失敗-系統(tǒng)異常 | 用戶ID:{} | 異常類型:{}",
request.getUserId(), e.getClass().getSimpleName(), e);
throw new SystemException("系統(tǒng)繁忙,請(qǐng)稍后重試");
}
}
public void processExpiredOrders() {
log.info("開始處理過期訂單 | 線程:{}", Thread.currentThread().getName());
int processedCount = 0;
int successCount = 0;
int failureCount = 0;
long startTime = System.currentTimeMillis();
try {
List<Order> expiredOrders = orderRepository.findExpiredOrders();
log.info("找到過期訂單數(shù)量:{}", expiredOrders.size());
for (Order order : expiredOrders) {
processedCount++;
try {
log.debug("處理過期訂單 | 訂單ID:{} | 訂單號(hào):{}", order.getId(), order.getOrderNo());
cancelOrder(order.getId(), "系統(tǒng)自動(dòng)取消-超時(shí)未支付");
successCount++;
log.debug("過期訂單處理成功 | 訂單ID:{}", order.getId());
} catch (Exception e) {
failureCount++;
log.error("處理過期訂單失敗 | 訂單ID:{} | 異常:{}", order.getId(), e.getMessage(), e);
}
}
log.info("過期訂單處理完成 | 總數(shù):{} | 成功:{} | 失敗:{} | 總耗時(shí):{}ms",
processedCount, successCount, failureCount,
System.currentTimeMillis() - startTime);
} catch (Exception e) {
log.error("處理過期訂單任務(wù)異常 | 異常類型:{}", e.getClass().getSimpleName(), e);
throw e;
}
}
}3. 數(shù)據(jù)訪問層日志
3.1 Repository 日志示例
@Repository
@Slf4j
public class UserRepository {
public User findByUsername(String username) {
log.debug("根據(jù)用戶名查詢用戶 | 用戶名:{}", username);
long startTime = System.currentTimeMillis();
try {
User user = userMapper.selectByUsername(username);
if (user == null) {
log.debug("用戶不存在 | 用戶名:{} | 耗時(shí):{}ms",
username, System.currentTimeMillis() - startTime);
} else {
log.debug("用戶查詢成功 | 用戶ID:{} | 用戶名:{} | 耗時(shí):{}ms",
user.getId(), username, System.currentTimeMillis() - startTime);
}
return user;
} catch (DataAccessException e) {
log.error("數(shù)據(jù)庫查詢異常 | SQL:查詢用戶 | 參數(shù):{} | 異常類型:{}",
username, e.getClass().getSimpleName(), e);
throw new RepositoryException("用戶查詢失敗", e);
}
}
@Transactional
public void updateUserStatus(Long userId, UserStatus newStatus) {
UserStatus oldStatus = getUserStatus(userId);
log.info("更新用戶狀態(tài) | 用戶ID:{} | 原狀態(tài):{} | 新狀態(tài):{}", userId, oldStatus, newStatus);
try {
int affectedRows = userMapper.updateStatus(userId, newStatus);
if (affectedRows == 0) {
log.warn("更新用戶狀態(tài)失敗-記錄不存在 | 用戶ID:{}", userId);
throw new EntityNotFoundException("用戶不存在");
}
log.info("用戶狀態(tài)更新成功 | 用戶ID:{} | 影響行數(shù):{}", userId, affectedRows);
} catch (DataIntegrityViolationException e) {
log.error("更新用戶狀態(tài)失敗-數(shù)據(jù)完整性異常 | 用戶ID:{} | 新狀態(tài):{}", userId, newStatus, e);
throw new RepositoryException("數(shù)據(jù)完整性約束沖突", e);
}
}
public Page<User> findUsersByCondition(UserQuery query, Pageable pageable) {
if (log.isDebugEnabled()) {
log.debug("分頁查詢用戶 | 查詢條件:{} | 分頁參數(shù):{}",
query.toString(), pageable.toString());
}
long startTime = System.currentTimeMillis();
try {
Page<User> result = userMapper.selectByCondition(query, pageable);
log.info("用戶分頁查詢完成 | 總數(shù):{} | 當(dāng)前頁:{} | 耗時(shí):{}ms",
result.getTotalElements(), result.getNumber(),
System.currentTimeMillis() - startTime);
return result;
} catch (DataAccessException e) {
log.error("用戶分頁查詢異常 | 查詢條件:{} | 異常類型:{}",
query, e.getClass().getSimpleName(), e);
throw new RepositoryException("用戶查詢失敗", e);
}
}
}4. 外部服務(wù)調(diào)用日志
4.1 HTTP 客戶端調(diào)用
@Component
@Slf4j
public class PaymentServiceClient {
public PaymentResponse createPayment(PaymentRequest request) {
String requestId = generateRequestId();
log.info("調(diào)用支付服務(wù)開始 | 請(qǐng)求ID:{} | 訂單號(hào):{} | 金額:{} | 端點(diǎn):{}",
requestId, request.getOrderNo(), request.getAmount(), "/api/payments");
long startTime = System.currentTimeMillis();
try {
// 記錄請(qǐng)求詳情(敏感信息脫敏)
if (log.isDebugEnabled()) {
log.debug("支付請(qǐng)求詳情 | 請(qǐng)求ID:{} | 支付方式:{} | 銀行編碼:{}",
requestId, request.getPaymentMethod(),
maskBankCode(request.getBankCode()));
}
PaymentResponse response = restTemplate.postForObject(
paymentServiceUrl + "/api/payments", request, PaymentResponse.class);
log.info("支付服務(wù)調(diào)用成功 | 請(qǐng)求ID:{} | 支付狀態(tài):{} | 耗時(shí):{}ms",
requestId, response.getStatus(), System.currentTimeMillis() - startTime);
return response;
} catch (HttpClientErrorException e) {
log.warn("支付服務(wù)調(diào)用失敗-客戶端錯(cuò)誤 | 請(qǐng)求ID:{} | 狀態(tài)碼:{} | 響應(yīng):{}",
requestId, e.getStatusCode(), e.getResponseBodyAsString(), e);
throw new PaymentException("支付請(qǐng)求參數(shù)錯(cuò)誤");
} catch (HttpServerErrorException e) {
log.error("支付服務(wù)調(diào)用失敗-服務(wù)端錯(cuò)誤 | 請(qǐng)求ID:{} | 狀態(tài)碼:{} | 異常:{}",
requestId, e.getStatusCode(), e.getMessage(), e);
throw new PaymentException("支付服務(wù)暫時(shí)不可用");
} catch (ResourceAccessException e) {
log.error("支付服務(wù)調(diào)用失敗-網(wǎng)絡(luò)異常 | 請(qǐng)求ID:{} | 異常類型:{}",
requestId, e.getClass().getSimpleName(), e);
throw new PaymentException("網(wǎng)絡(luò)連接超時(shí)");
} catch (Exception e) {
log.error("支付服務(wù)調(diào)用失敗-未知異常 | 請(qǐng)求ID:{} | 異常類型:{}",
requestId, e.getClass().getSimpleName(), e);
throw new PaymentException("支付處理失敗");
}
}
}4.2 Feign Client 調(diào)用
@FeignClient(name = "inventory-service")
@Slf4j
public class InventoryServiceClient {
@PostMapping("/api/inventory/deduct")
ResponseEntity<InventoryResponse> deductStock(@RequestBody DeductRequest request);
// Feign 配置類中的日志
@Component
@Slf4j
public class FeignLogger extends Logger {
@Override
protected void log(String configKey, String format, Object... args) {
if (log.isInfoEnabled()) {
log.info(String.format(methodTag(configKey) + format, args));
}
}
}
}5. 消息隊(duì)列處理日志
5.1 RabbitMQ 消費(fèi)者
@Component
@Slf4j
public class OrderMessageConsumer {
@RabbitListener(queues = "order.create.queue")
public void handleOrderCreate(OrderCreateMessage message, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
log.info("收到訂單創(chuàng)建消息 | 消息ID:{} | 訂單號(hào):{} | 路由鍵:{}",
message.getMessageId(), message.getOrderNo(), "order.create");
long startTime = System.currentTimeMillis();
try {
// 業(yè)務(wù)處理
orderService.processOrderCreate(message);
// 手動(dòng)確認(rèn)消息
channel.basicAck(deliveryTag, false);
log.info("訂單消息處理成功 | 消息ID:{} | 訂單號(hào):{} | 耗時(shí):{}ms",
message.getMessageId(), message.getOrderNo(),
System.currentTimeMillis() - startTime);
} catch (BusinessException e) {
log.warn("訂單消息處理失敗-業(yè)務(wù)異常 | 消息ID:{} | 訂單號(hào):{} | 錯(cuò)誤:{}",
message.getMessageId(), message.getOrderNo(), e.getMessage());
// 業(yè)務(wù)異常,拒絕消息并不再重試
channel.basicReject(deliveryTag, false);
} catch (Exception e) {
log.error("訂單消息處理失敗-系統(tǒng)異常 | 消息ID:{} | 訂單號(hào):{} | 異常類型:{}",
message.getMessageId(), message.getOrderNo(),
e.getClass().getSimpleName(), e);
// 系統(tǒng)異常,拒絕消息并重新入隊(duì)
channel.basicReject(deliveryTag, true);
}
}
}6. 定時(shí)任務(wù)日志
6.1 分布式定時(shí)任務(wù)
@Component
@Slf4j
public class ScheduledTasks {
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2點(diǎn)執(zhí)行
@SchedulerLock(name = "dataCleanupTask", lockAtMostFor = "30m")
public void dataCleanupTask() {
String taskId = UUID.randomUUID().toString();
log.info("數(shù)據(jù)清理任務(wù)開始 | 任務(wù)ID:{} | 執(zhí)行時(shí)間:{}",
taskId, LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
long startTime = System.currentTimeMillis();
int successCount = 0;
int failureCount = 0;
try {
// 清理過期訂單
log.debug("開始清理過期訂單 | 任務(wù)ID:{}", taskId);
int orderCount = orderService.cleanExpiredOrders();
log.debug("過期訂單清理完成 | 清理數(shù)量:{}", orderCount);
successCount++;
// 清理日志文件
log.debug("開始清理日志文件 | 任務(wù)ID:{}", taskId);
int logCount = logService.cleanOldLogs();
log.debug("日志文件清理完成 | 清理數(shù)量:{}", logCount);
successCount++;
log.info("數(shù)據(jù)清理任務(wù)完成 | 任務(wù)ID:{} | 成功子任務(wù):{} | 失敗子任務(wù):{} | 總耗時(shí):{}ms",
taskId, successCount, failureCount,
System.currentTimeMillis() - startTime);
} catch (Exception e) {
log.error("數(shù)據(jù)清理任務(wù)異常 | 任務(wù)ID:{} | 異常類型:{}",
taskId, e.getClass().getSimpleName(), e);
throw e;
}
}
}7. 安全認(rèn)證日志
7.1 JWT 認(rèn)證日志
@Component
@Slf4j
public class JwtTokenProvider {
public String generateToken(UserDetails userDetails) {
log.debug("生成JWT Token開始 | 用戶名:{} | 角色:{}",
userDetails.getUsername(), userDetails.getAuthorities());
String token = doGenerateToken(userDetails);
log.info("JWT Token生成成功 | 用戶名:{} | Token前綴:{}",
userDetails.getUsername(), token.substring(0, 20));
return token;
}
public boolean validateToken(String token) {
log.debug("驗(yàn)證JWT Token | Token長(zhǎng)度:{}", token.length());
try {
boolean isValid = doValidateToken(token);
if (isValid) {
log.debug("JWT Token驗(yàn)證通過");
} else {
log.warn("JWT Token驗(yàn)證失敗");
}
return isValid;
} catch (ExpiredJwtException e) {
log.warn("JWT Token已過期 | 過期時(shí)間:{}", e.getClaims().getExpiration());
return false;
} catch (MalformedJwtException e) {
log.warn("JWT Token格式錯(cuò)誤");
return false;
} catch (Exception e) {
log.error("JWT Token驗(yàn)證異常 | 異常類型:{}", e.getClass().getSimpleName(), e);
return false;
}
}
}8. 文件操作日志
8.1 文件上傳下載
@Service
@Slf4j
public class FileService {
public FileUploadResponse uploadFile(MultipartFile file, String businessType) {
log.info("文件上傳開始 | 文件名:{} | 文件大小:{} | 業(yè)務(wù)類型:{}",
file.getOriginalFilename(), file.getSize(), businessType);
long startTime = System.currentTimeMillis();
try {
// 文件驗(yàn)證
validateFile(file);
log.debug("文件驗(yàn)證通過 | 文件名:{}", file.getOriginalFilename());
// 上傳處理
String filePath = saveFile(file);
log.debug("文件保存完成 | 存儲(chǔ)路徑:{}", filePath);
FileUploadResponse response = new FileUploadResponse(filePath, "上傳成功");
log.info("文件上傳成功 | 文件名:{} | 文件路徑:{} | 耗時(shí):{}ms",
file.getOriginalFilename(), filePath,
System.currentTimeMillis() - startTime);
return response;
} catch (FileSizeException e) {
log.warn("文件上傳失敗-文件大小超限 | 文件名:{} | 實(shí)際大小:{} | 限制大小:{}",
file.getOriginalFilename(), file.getSize(), e.getLimitSize());
throw new BusinessException("文件大小超過限制");
} catch (FileTypeException e) {
log.warn("文件上傳失敗-文件類型不支持 | 文件名:{} | 文件類型:{}",
file.getOriginalFilename(), e.getFileType());
throw new BusinessException("不支持的文件類型");
} catch (IOException e) {
log.error("文件上傳失敗-IO異常 | 文件名:{} | 異常類型:{}",
file.getOriginalFilename(), e.getClass().getSimpleName(), e);
throw new SystemException("文件上傳失敗");
}
}
}9. 緩存操作日志
9.1 Redis 緩存操作
@Service
@Slf4j
public class CacheService {
public <T> T getFromCache(String key, Class<T> clazz) {
log.debug("查詢緩存開始 | 緩存鍵:{} | 類型:{}", key, clazz.getSimpleName());
long startTime = System.currentTimeMillis();
try {
T value = redisTemplate.opsForValue().get(key);
if (value == null) {
log.debug("緩存未命中 | 緩存鍵:{} | 耗時(shí):{}ms",
key, System.currentTimeMillis() - startTime);
} else {
log.debug("緩存命中 | 緩存鍵:{} | 耗時(shí):{}ms",
key, System.currentTimeMillis() - startTime);
}
return value;
} catch (RedisConnectionFailureException e) {
log.error("緩存查詢失敗-連接異常 | 緩存鍵:{}", key, e);
throw new CacheException("緩存服務(wù)不可用");
} catch (Exception e) {
log.error("緩存查詢失敗-未知異常 | 緩存鍵:{} | 異常類型:{}",
key, e.getClass().getSimpleName(), e);
throw new CacheException("緩存查詢失敗");
}
}
public void setToCache(String key, Object value, Duration ttl) {
log.debug("設(shè)置緩存開始 | 緩存鍵:{} | TTL:{}", key, ttl);
try {
redisTemplate.opsForValue().set(key, value, ttl);
log.debug("緩存設(shè)置成功 | 緩存鍵:{}", key);
} catch (Exception e) {
log.error("緩存設(shè)置失敗 | 緩存鍵:{} | 異常類型:{}",
key, e.getClass().getSimpleName(), e);
// 緩存設(shè)置失敗不應(yīng)該影響主流程
}
}
}10. 全局異常處理日志
10.1 統(tǒng)一異常處理器
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e,
HttpServletRequest request) {
String errorId = generateErrorId();
log.warn("業(yè)務(wù)異常處理 | 錯(cuò)誤ID:{} | 錯(cuò)誤碼:{} | 錯(cuò)誤信息:{} | 請(qǐng)求路徑:{} | 客戶端IP:{}",
errorId, e.getErrorCode(), e.getMessage(),
request.getRequestURI(), getClientIp(request));
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(errorId, e.getErrorCode(), e.getMessage()));
}
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ErrorResponse> handleAuthException(AuthenticationException e,
HttpServletRequest request) {
String errorId = generateErrorId();
log.warn("認(rèn)證異常處理 | 錯(cuò)誤ID:{} | 錯(cuò)誤信息:{} | 請(qǐng)求路徑:{} | 客戶端IP:{}",
errorId, e.getMessage(), request.getRequestURI(), getClientIp(request));
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse(errorId, "UNAUTHORIZED", "認(rèn)證失敗"));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleSystemException(Exception e,
HttpServletRequest request) {
String errorId = generateErrorId();
log.error("系統(tǒng)異常處理 | 錯(cuò)誤ID:{} | 異常類型:{} | 請(qǐng)求路徑:{} | 客戶端IP:{} | 堆棧跟蹤:{}",
errorId, e.getClass().getSimpleName(), request.getRequestURI(),
getClientIp(request), getStackTrace(e));
// 生產(chǎn)環(huán)境隱藏詳細(xì)錯(cuò)誤信息
String message = isProduction() ? "系統(tǒng)內(nèi)部錯(cuò)誤" : e.getMessage();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse(errorId, "SYSTEM_ERROR", message));
}
}11. 配置類日志
11.1 應(yīng)用啟動(dòng)配置
@SpringBootApplication
@Slf4j
public class Application {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Application.class);
// 啟動(dòng)前日志
log.info("應(yīng)用啟動(dòng)開始 | 環(huán)境:{} | 版本:{} | 啟動(dòng)時(shí)間:{}",
getActiveProfile(), getAppVersion(), LocalDateTime.now());
try {
ConfigurableApplicationContext context = app.run(args);
// 啟動(dòng)成功日志
Environment env = context.getEnvironment();
String port = env.getProperty("server.port");
String contextPath = env.getProperty("server.servlet.context-path", "");
log.info("應(yīng)用啟動(dòng)成功 | 環(huán)境:{} | 端口:{} | 上下文路徑:{} | 啟動(dòng)耗時(shí):{}ms",
getActiveProfile(), port, contextPath,
System.currentTimeMillis() - startTime);
// 記錄重要的配置信息
log.info("數(shù)據(jù)源配置 | URL:{} | 用戶名:{}",
maskDatabaseUrl(env.getProperty("spring.datasource.url")),
env.getProperty("spring.datasource.username"));
} catch (Exception e) {
log.error("應(yīng)用啟動(dòng)失敗 | 異常類型:{}", e.getClass().getSimpleName(), e);
System.exit(1);
}
}
}這些示例覆蓋了 Spring Boot/Cloud 應(yīng)用中絕大多數(shù)常見的日志場(chǎng)景,提供了完整的、生產(chǎn)可用的日志記錄模式。在實(shí)際開發(fā)中,可以根據(jù)具體業(yè)務(wù)需求進(jìn)行調(diào)整和擴(kuò)展。
到此這篇關(guān)于Spring Boot/Spring Cloud 應(yīng)用日志書寫詳細(xì)指南的文章就介紹到這了,更多相關(guān)Spring Boot日志書寫內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
客戶端Socket與服務(wù)端ServerSocket串聯(lián)實(shí)現(xiàn)網(wǎng)絡(luò)通信
這篇文章主要為大家介紹了客戶端Socket與服務(wù)端ServerSocket串聯(lián)實(shí)現(xiàn)網(wǎng)絡(luò)通信的內(nèi)容詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-03-03
Java selenium上傳文件的實(shí)現(xiàn)
本文主要介紹了Java selenium上傳文件的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04
spring boot動(dòng)態(tài)加載Echart餅狀圖
這篇文章主要為大家詳細(xì)介紹了spring boot動(dòng)態(tài)加載Echart餅狀圖,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-12-12
mybatis數(shù)組和集合的長(zhǎng)度判斷及插入方式
這篇文章主要介紹了mybatis數(shù)組和集合的長(zhǎng)度判斷及插入方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01

