SpringBoot中構(gòu)建生產(chǎn)級日志系統(tǒng)的優(yōu)雅指南
一、需求描述
在實(shí)際開發(fā)中,日志系統(tǒng)需要滿足以下需求:
- 區(qū)分日志級別:DEBUG/INFO/WARN/ERROR 各司其職
- 性能友好:避免日志序列化開銷,支持異步輸出
- 鏈路追蹤:一次請求全鏈路可追蹤(RequestId)
- 敏感信息脫敏:手機(jī)號、身份證等自動脫敏
- 日志分類:業(yè)務(wù)日志、錯誤日志、慢SQL日志分開存儲
- 輸出規(guī)范:JSON格式,便于ELK采集分析
- 開發(fā)/生產(chǎn)環(huán)境差異化:開發(fā)環(huán)境輸出控制臺,生產(chǎn)環(huán)境輸出文件
二、詳細(xì)步驟與代碼實(shí)現(xiàn)
1. 添加依賴
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 性能優(yōu)化:異步日志需要disruptor -->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.4</version>
</dependency>
<!-- 鏈路追蹤 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
<version>3.1.5</version>
</dependency>
<!-- 簡化代碼:lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>2. 配置文件(application.yml)
# application.yml
spring:
application:
name: demo-service
# 鏈路追蹤配置
sleuth:
web:
enabled: true
sampler:
probability: 1.0 # 生產(chǎn)環(huán)境建議0.1
# 日志配置
logging:
# 日志文件路徑
file:
path: ./logs
name: ${logging.file.path}/${spring.application.name}.log
# 日志級別
level:
root: INFO
com.example: DEBUG
org.springframework.web: INFO
org.hibernate: WARN
# 日志格式
pattern:
console: "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${PID:- } --- [%t] %-40.40logger{39} : %m%n%wEx"
# 自定義日志配置
log:
# 敏感字段列表
sensitive-fields:
- password
- oldPassword
- newPassword
- idCard
- phone
- bankCard
- token
- secret3. Logback配置(logback-spring.xml)
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds">
<!-- 引入Spring環(huán)境配置 -->
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="app"/>
<springProperty scope="context" name="LOG_PATH" source="logging.file.path" defaultValue="./logs"/>
<!-- 彩色日志依賴 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
<!-- 日志格式 -->
<property name="CONSOLE_LOG_PATTERN"
value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx"/>
<property name="FILE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${PID:- } --- [%t] %-40.40logger{39} : %m%n%wEx"/>
<!-- JSON格式(用于生產(chǎn)環(huán)境ELK) -->
<property name="JSON_LOG_PATTERN"
value='{"timestamp":"%d{yyyy-MM-dd HH:mm:ss.SSS}","level":"%p","app":"${APP_NAME}","traceId":"%X{traceId}","thread":"%t","logger":"%logger","message":"%m","exception":"%wEx"}%n'/>
<!-- 控制臺輸出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 異步控制臺輸出(性能優(yōu)化) -->
<appender name="ASYNC_CONSOLE" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>1024</queueSize>
<appender-ref ref="CONSOLE"/>
</appender>
<!-- 普通日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${APP_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 錯誤日志單獨(dú)文件 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}-error.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${APP_NAME}-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>90</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 業(yè)務(wù)日志文件 -->
<appender name="BIZ_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/biz.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/biz.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 異步文件輸出(性能優(yōu)化) -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>2048</queueSize>
<discardingThreshold>0</discardingThreshold>
<includeCallerData>true</includeCallerData>
<appender-ref ref="FILE"/>
</appender>
<appender name="ASYNC_ERROR_FILE" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>1024</queueSize>
<appender-ref ref="ERROR_FILE"/>
</appender>
<appender name="ASYNC_BIZ_FILE" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>1024</queueSize>
<appender-ref ref="BIZ_FILE"/>
</appender>
<!-- 業(yè)務(wù)日志 Logger -->
<logger name="BIZ_LOGGER" level="INFO" additivity="false">
<appender-ref ref="ASYNC_BIZ_FILE"/>
</logger>
<!-- Root Logger -->
<root level="INFO">
<appender-ref ref="ASYNC_CONSOLE"/>
<appender-ref ref="ASYNC_FILE"/>
<appender-ref ref="ASYNC_ERROR_FILE"/>
</root>
</configuration>4. 日志工具類封裝
package com.example.log.util;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.UUID;
/**
* 日志工具類
*/
@Slf4j
@Component
public class LogUtil {
// 業(yè)務(wù)日志專用Logger
private static final Logger BIZ_LOGGER = LoggerFactory.getLogger("BIZ_LOGGER");
/**
* 獲取當(dāng)前請求的TraceId
*/
public static String getTraceId() {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String traceId = request.getHeader("X-Trace-Id");
if (traceId == null || traceId.isEmpty()) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
return traceId;
}
} catch (Exception e) {
log.warn("獲取TraceId失敗", e);
}
return UUID.randomUUID().toString().replace("-", "");
}
/**
* 業(yè)務(wù)日志(關(guān)鍵操作記錄)
*/
public static void bizLog(String operation, String userId, Object... params) {
String traceId = getTraceId();
String message = String.format("[BIZ][%s][%s][%s] params: %s",
traceId, operation, userId, params);
BIZ_LOGGER.info(message);
}
/**
* 接口調(diào)用日志(簡潔版)
*/
public static void apiLog(String apiName, long costTime, Object request, Object response) {
if (costTime > 3000) {
// 慢接口使用WARN級別
log.warn("[API][{}] cost: {}ms, request: {}, response: {}",
apiName, costTime, request, response);
} else {
log.info("[API][{}] cost: {}ms", apiName, costTime);
}
}
/**
* 方法調(diào)用日志(帶耗時)
*/
public static void methodLog(String methodName, long startTime) {
long cost = System.currentTimeMillis() - startTime;
if (cost > 1000) {
log.warn("[METHOD][{}] cost: {}ms", methodName, cost);
} else {
log.debug("[METHOD][{}] cost: {}ms", methodName, cost);
}
}
}
5. 敏感信息脫敏工具
package com.example.log.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.regex.Pattern;
/**
* 日志脫敏工具
*/
@Slf4j
@Component
public class DesensitizationUtil {
@Value("${log.sensitive-fields:}")
private List<String> sensitiveFields;
private static final Set<String> SENSITIVE_FIELDS = new HashSet<>(Arrays.asList(
"password", "oldPassword", "newPassword", "idCard", "phone",
"bankCard", "token", "secret", "authorization"
));
private static final Pattern PHONE_PATTERN = Pattern.compile("(\\d{3})\\d{4}(\\d{4})");
private static final Pattern ID_CARD_PATTERN = Pattern.compile("(\\d{4})\\d{10}(\\d{4})");
private static final Pattern BANK_CARD_PATTERN = Pattern.compile("(\\d{4})\\d{10,12}(\\d{4})");
private ObjectMapper objectMapper;
@PostConstruct
public void init() {
this.objectMapper = new ObjectMapper();
this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
if (sensitiveFields != null) {
SENSITIVE_FIELDS.addAll(sensitiveFields);
}
}
/**
* 對象脫敏(JSON序列化前處理)
*/
public String toJsonWithDesensitization(Object obj) {
try {
if (obj == null) {
return "null";
}
Object desensitized = desensitize(obj);
return objectMapper.writeValueAsString(desensitized);
} catch (JsonProcessingException e) {
log.error("JSON序列化失敗", e);
return obj != null ? obj.toString() : "null";
}
}
/**
* 遞歸脫敏
*/
@SuppressWarnings("unchecked")
private Object desensitize(Object obj) {
if (obj == null) {
return null;
}
if (obj instanceof Map) {
Map<String, Object> map = (Map<String, Object>) obj;
Map<String, Object> result = new HashMap<>();
for (Map.Entry<String, Object> entry : map.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (SENSITIVE_FIELDS.contains(key.toLowerCase())) {
result.put(key, "***");
} else {
result.put(key, desensitize(value));
}
}
return result;
}
if (obj instanceof List) {
List<Object> list = (List<Object>) obj;
List<Object> result = new ArrayList<>();
for (Object item : list) {
result.add(desensitize(item));
}
return result;
}
// 字符串類型特殊處理
if (obj instanceof String) {
String str = (String) obj;
// 手機(jī)號脫敏
if (str.matches("^1[3-9]\\d{9}$")) {
return PHONE_PATTERN.matcher(str).replaceAll("$1****$2");
}
// 身份證脫敏
if (str.matches("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]$")) {
return ID_CARD_PATTERN.matcher(str).replaceAll("$1**********$2");
}
}
return obj;
}
/**
* 快速脫敏手機(jī)號
*/
public static String maskPhone(String phone) {
if (phone == null || phone.length() != 11) {
return phone;
}
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
/**
* 快速脫敏身份證
*/
public static String maskIdCard(String idCard) {
if (idCard == null || idCard.length() < 18) {
return idCard;
}
return idCard.replaceAll("(\\d{4})\\d{10}(\\d{4})", "$1**********$2");
}
}
6. 全局日志攔截器(AOP實(shí)現(xiàn))
package com.example.log.aspect;
import com.example.log.util.DesensitizationUtil;
import com.example.log.util.LogUtil;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
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.lang.reflect.Method;
import java.util.Arrays;
import java.util.stream.Collectors;
/**
* Controller層日志切面
*/
@Slf4j
@Aspect
@Component
public class ControllerLogAspect {
@Autowired
private DesensitizationUtil desensitizationUtil;
// 定義切點(diǎn):所有Controller類下的方法
@Pointcut("execution(* com.example..controller.*.*(..))")
public void controllerPointcut() {}
@Around("controllerPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
// 獲取請求信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes != null ? attributes.getRequest() : null;
// 獲取方法信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = method.getName();
// 獲取參數(shù)(脫敏處理)
Object[] args = joinPoint.getArgs();
String params = "";
if (args != null && args.length > 0) {
params = Arrays.stream(args)
.map(arg -> desensitizationUtil.toJsonWithDesensitization(arg))
.collect(Collectors.joining(", "));
}
// 請求信息日志
if (request != null) {
log.info("【請求開始】{} {} | 參數(shù): {}",
request.getMethod(), request.getRequestURI(), params);
} else {
log.info("【方法調(diào)用】{}.{} 參數(shù): {}", className, methodName, params);
}
Object result = null;
try {
result = joinPoint.proceed();
long costTime = System.currentTimeMillis() - startTime;
// 響應(yīng)日志(脫敏處理)
String responseJson = desensitizationUtil.toJsonWithDesensitization(result);
log.info("【請求結(jié)束】{}.{} 耗時: {}ms | 響應(yīng): {}",
className, methodName, costTime, responseJson);
// 慢請求告警
if (costTime > 3000) {
log.warn("【慢請求】{}.{} 耗時: {}ms", className, methodName, costTime);
}
return result;
} catch (Exception e) {
long costTime = System.currentTimeMillis() - startTime;
log.error("【請求異?!縶}.{} 耗時: {}ms 異常: {}",
className, methodName, costTime, e.getMessage(), e);
throw e;
}
}
}
7. MDC鏈路追蹤過濾器
package com.example.log.filter;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
/**
* MDC過濾器:實(shí)現(xiàn)全鏈路追蹤
*/
@Slf4j
@Component
@Order(1)
public class TraceIdFilter implements Filter {
private static final String TRACE_ID = "traceId";
@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 = UUID.randomUUID().toString().replace("-", "");
}
try {
// 將traceId放入MDC,日志模板中可通過%X{traceId}引用
MDC.put(TRACE_ID, traceId);
MDC.put("clientIp", getClientIp(httpRequest));
chain.doFilter(request, response);
} finally {
// 清理MDC,避免內(nèi)存泄漏
MDC.clear();
}
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddr();
}
return ip != null ? ip.split(",")[0].trim() : "unknown";
}
}
8. 業(yè)務(wù)中使用示例
package com.example.demo.controller;
import com.example.demo.service.UserService;
import com.example.log.util.LogUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping("/login")
public Map<String, Object> login(@RequestBody @Valid LoginRequest request) {
// 使用Lombok的@Slf4j
log.info("用戶登錄請求: username={}", request.getUsername());
long startTime = System.currentTimeMillis();
try {
UserVO user = userService.login(request);
// 記錄業(yè)務(wù)日志
LogUtil.bizLog("用戶登錄", user.getId(), "login", request.getUsername());
// 記錄方法耗時
LogUtil.methodLog("login", startTime);
return Map.of("success", true, "data", user);
} catch (Exception e) {
log.error("用戶登錄失敗: username={}, error={}", request.getUsername(), e.getMessage(), e);
throw e;
}
}
@PostMapping("/update")
public Map<String, Object> updateUser(@RequestBody UserUpdateRequest request) {
// 這里password字段會被自動脫敏
log.info("更新用戶信息: {}", request);
userService.updateUser(request);
return Map.of("success", true);
}
}
// 請求對象示例
@Data
class LoginRequest {
private String username;
private String password; // 會被脫敏
}
@Data
class UserUpdateRequest {
private String userId;
private String phone;
private String idCard;
private String password;
}
9. 異常統(tǒng)一處理中的日志
package com.example.demo.handler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusinessException(BusinessException e) {
// 業(yè)務(wù)異常:記錄WARN級別即可
log.warn("業(yè)務(wù)異常: code={}, message={}", e.getCode(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
// 系統(tǒng)異常:記錄ERROR級別,包含堆棧
log.error("系統(tǒng)異常", e);
return Result.error(500, "系統(tǒng)繁忙,請稍后重試");
}
}
三、總結(jié)
最佳實(shí)踐總結(jié)
| 原則 | 說明 | 示例 |
|---|---|---|
| 級別正確 | DEBUG調(diào)試、INFO業(yè)務(wù)流程、WARN異常可恢復(fù)、ERROR系統(tǒng)錯誤 | 循環(huán)內(nèi)用DEBUG,關(guān)鍵節(jié)點(diǎn)用INFO |
| 參數(shù)占位 | 使用{}占位符,避免字符串拼接 | log.info("user: {}", user) |
| 異常記錄 | 必須傳入異常對象,輸出堆棧 | log.error("錯誤", e) |
| 異步輸出 | 生產(chǎn)環(huán)境必須配置AsyncAppender | 使用Disruptor提升性能 |
| 鏈路追蹤 | 使用MDC傳遞traceId | 全鏈路可追蹤 |
| 敏感脫敏 | 密碼、手機(jī)號等必須脫敏 | 實(shí)現(xiàn)自定義脫敏工具 |
| 日志開關(guān) | 使用isDebugEnabled()避免無效序列化 | if(log.isDebugEnabled()){...} |
| 合理采樣 | 高頻日志需采樣 | 1%或千分之一 |
性能優(yōu)化要點(diǎn)
- 異步日志:AsyncAppender + Disruptor,QPS提升5-10倍
- 條件日志:使用
log.isDebugEnabled()避免參數(shù)計(jì)算 - 合理級別:生產(chǎn)環(huán)境建議INFO,DEBUG僅開發(fā)/測試環(huán)境
- 避免打印大對象:大List/大JSON需要截?cái)嗷蛑淮蛴¢L度
監(jiān)控告警建議
- ERROR日志數(shù)量突增 → 釘釘/企微告警
- 慢請求日志(>3s) → 性能監(jiān)控
- 特定業(yè)務(wù)日志(登錄失敗頻繁) → 安全告警
日志規(guī)范Checklist
- 禁止使用
System.out.println() - 禁止打印密碼、token等敏感信息
- 日志消息清晰,包含關(guān)鍵業(yè)務(wù)標(biāo)識(userId、orderId)
- 異常日志必須包含堆棧信息
- 關(guān)鍵業(yè)務(wù)操作必須有業(yè)務(wù)日志
- 日志文件配置滾動策略和保留時長(建議30天)
- 生產(chǎn)環(huán)境關(guān)閉DEBUG日志
通過以上方案,可以實(shí)現(xiàn)生產(chǎn)級的日志系統(tǒng),既保證性能又便于問題排查和監(jiān)控告警。
以上就是SpringBoot中構(gòu)建生產(chǎn)級日志系統(tǒng)的優(yōu)雅指南的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot日志系統(tǒng)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot全局域名替換的實(shí)現(xiàn)
本文主要介紹了SpringBoot全局域名替換的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-09-09
screw?Maven插件方式運(yùn)行時在編譯打包時跳過執(zhí)行的問題解決方法
這篇文章主要介紹了screw?Maven插件方式運(yùn)行時在編譯打包時跳過執(zhí)行的問題解決方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03
Java中將File轉(zhuǎn)化為MultipartFile的操作
這篇文章主要介紹了Java中將File轉(zhuǎn)化為MultipartFile的操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10
springboot+dubbo實(shí)現(xiàn)時間輪算法
時間輪是一種高效利用線程資源進(jìn)行批量化調(diào)度的算法,本文主要介紹了springboot+dubbo實(shí)現(xiàn)時間輪算法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-04-04
SpringBoot集成Redis之配置、序列化與持久化方式
本文介紹了Redis的基本概念、常用數(shù)據(jù)類型及操作、SpringBoot整合Redis的方法、高級特性與安全性、性能優(yōu)化、測試與部署、數(shù)據(jù)一致性及版本更新等內(nèi)容,通過本文的學(xué)習(xí),讀者可以掌握Redis的使用方法,并在實(shí)際項(xiàng)目中發(fā)揮其優(yōu)勢2024-11-11

