From 0992ceb334a02471acabea8afc073d624c920ffb Mon Sep 17 00:00:00 2001 From: Lxy Date: Sun, 28 Jun 2026 23:49:41 +0800 Subject: [PATCH] feat: add performance monitoring and slow query logging --- .../service/impl/AccountServiceImpl.java | 2 + pom.xml | 12 ++ ruoyi-admin/pom.xml | 12 ++ .../monitor/PerformanceController.java | 71 +++++++++++ .../src/main/resources/application.yml | 15 +++ .../common/annotation/PerformanceMonitor.java | 23 ++++ .../framework/aspectj/PerformanceAspect.java | 116 ++++++++++++++++++ .../service/impl/SysDictTypeServiceImpl.java | 2 + .../service/impl/SysUserServiceImpl.java | 3 + sql/monitor/01_create_slow_query_log.sql | 12 ++ 10 files changed, 268 insertions(+) create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/PerformanceController.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/annotation/PerformanceMonitor.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/PerformanceAspect.java create mode 100644 sql/monitor/01_create_slow_query_log.sql diff --git a/book-system/src/main/java/com/ruoyi/booksystem/service/impl/AccountServiceImpl.java b/book-system/src/main/java/com/ruoyi/booksystem/service/impl/AccountServiceImpl.java index b67af11..9113c95 100644 --- a/book-system/src/main/java/com/ruoyi/booksystem/service/impl/AccountServiceImpl.java +++ b/book-system/src/main/java/com/ruoyi/booksystem/service/impl/AccountServiceImpl.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Service; import com.ruoyi.booksystem.mapper.AccountMapper; import com.ruoyi.booksystem.domain.Account; import com.ruoyi.booksystem.service.IAccountService; +import com.ruoyi.common.annotation.PerformanceMonitor; /** * 交易账户Service业务层处理 @@ -38,6 +39,7 @@ public class AccountServiceImpl implements IAccountService * @return 交易账户 */ @Override + @PerformanceMonitor(slowThreshold = 1000, logParams = true) public List selectAccountList(Account account) { return accountMapper.selectAccountList(account); diff --git a/pom.xml b/pom.xml index b99fa29..3d4b7ac 100644 --- a/pom.xml +++ b/pom.xml @@ -226,6 +226,18 @@ ${caffeine.version} + + + org.springframework.boot + spring-boot-starter-actuator + + + + + io.micrometer + micrometer-registry-prometheus + + diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 6e15b49..64282d1 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -72,6 +72,18 @@ book-system + + + org.springframework.boot + spring-boot-starter-actuator + + + + + io.micrometer + micrometer-registry-prometheus + + diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/PerformanceController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/PerformanceController.java new file mode 100644 index 0000000..88c714c --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/PerformanceController.java @@ -0,0 +1,71 @@ +package com.ruoyi.web.controller.monitor; + +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.framework.aspectj.PerformanceAspect; +import com.ruoyi.framework.aspectj.PerformanceAspect.PerformanceStats; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +/** + * 性能监控 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/performance") +public class PerformanceController { + + /** + * 获取性能统计信息 + */ + @GetMapping("/stats") + public AjaxResult getPerformanceStats() { + Map statsMap = PerformanceAspect.getPerformanceStats(); + + Map result = new HashMap<>(); + statsMap.forEach((methodName, stats) -> { + Map statInfo = new HashMap<>(); + statInfo.put("totalCalls", stats.totalCalls.sum()); + statInfo.put("avgTime", stats.getAvgTime()); + statInfo.put("minTime", stats.minTime.get()); + statInfo.put("maxTime", stats.maxTime.get()); + statInfo.put("successRate", String.format("%.2f%%", stats.getSuccessRate())); + statInfo.put("errorCalls", stats.errorCalls.sum()); + result.put(methodName, statInfo); + }); + + return AjaxResult.success(result); + } + + /** + * 获取慢查询列表(> 1 秒) + */ + @GetMapping("/slowQueries") + public AjaxResult getSlowQueries() { + Map statsMap = PerformanceAspect.getPerformanceStats(); + + Map slowQueries = new HashMap<>(); + statsMap.forEach((methodName, stats) -> { + if (stats.maxTime.get() > 1000) { + Map queryInfo = new HashMap<>(); + queryInfo.put("maxTime", stats.maxTime.get()); + queryInfo.put("avgTime", stats.getAvgTime()); + queryInfo.put("totalCalls", stats.totalCalls.sum()); + slowQueries.put(methodName, queryInfo); + } + }); + + return AjaxResult.success(slowQueries); + } + + /** + * 清空性能统计 + */ + @DeleteMapping("/clear") + public AjaxResult clearStats() { + PerformanceAspect.clearStats(); + return AjaxResult.success("性能统计已清空"); + } +} diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index b08a9d0..fb697e0 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -125,3 +125,18 @@ xss: excludes: /system/notice # 匹配链接 urlPatterns: /system/*,/monitor/*,/tool/* + +# Spring Boot Actuator 监控配置 +management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus + endpoint: + health: + show-details: always + metrics: + enabled: true + metrics: + tags: + application: ${spring.application.name:ruoyi} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/PerformanceMonitor.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/PerformanceMonitor.java new file mode 100644 index 0000000..fd6cbd4 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/PerformanceMonitor.java @@ -0,0 +1,23 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.*; + +/** + * 性能监控注解 + * + * @author ruoyi + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface PerformanceMonitor { + /** + * 慢查询阈值(毫秒) + */ + long slowThreshold() default 1000; + + /** + * 是否记录参数 + */ + boolean logParams() default false; +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/PerformanceAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/PerformanceAspect.java new file mode 100644 index 0000000..0018370 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/PerformanceAspect.java @@ -0,0 +1,116 @@ +package com.ruoyi.framework.aspectj; + +import com.ruoyi.common.annotation.PerformanceMonitor; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; + +/** + * 性能监控切面 + * + * @author ruoyi + */ +@Aspect +@Component +public class PerformanceAspect { + + private static final Logger log = LoggerFactory.getLogger(PerformanceAspect.class); + + /** + * 性能统计信息 + */ + private static final Map statsMap = new ConcurrentHashMap<>(); + + /** + * 环绕通知:记录接口执行时间 + */ + @Around("@annotation(performanceMonitor)") + public Object around(ProceedingJoinPoint joinPoint, PerformanceMonitor performanceMonitor) throws Throwable { + String methodName = joinPoint.getSignature().toShortString(); + long startTime = System.currentTimeMillis(); + + // 记录开始时间 + PerformanceStats stats = statsMap.computeIfAbsent(methodName, k -> new PerformanceStats()); + stats.totalCalls.increment(); + + try { + // 执行方法 + Object result = joinPoint.proceed(); + + // 计算执行时间 + long executionTime = System.currentTimeMillis() - startTime; + stats.totalTime.add(executionTime); + stats.minTime.updateAndGet(min -> min == 0 ? executionTime : Math.min(min, executionTime)); + stats.maxTime.updateAndGet(max -> Math.max(max, executionTime)); + + // 慢查询日志 + if (executionTime > performanceMonitor.slowThreshold()) { + log.warn("⚠️ 慢查询警告: {} 执行时间: {}ms (阈值: {}ms)", + methodName, executionTime, performanceMonitor.slowThreshold()); + + if (performanceMonitor.logParams()) { + log.debug("参数: {}", joinPoint.getArgs()); + } + } + + return result; + } catch (Throwable e) { + stats.errorCalls.increment(); + log.error("❌ 接口异常: {} 执行时间: {}ms, 异常: {}", + methodName, System.currentTimeMillis() - startTime, e.getMessage()); + throw e; + } + } + + /** + * 获取性能统计信息 + */ + public static Map getPerformanceStats() { + return new HashMap<>(statsMap); + } + + /** + * 清空性能统计 + */ + public static void clearStats() { + statsMap.clear(); + } + + /** + * 性能统计信息 + */ + public static class PerformanceStats { + public final LongAdder totalCalls = new LongAdder(); + public final LongAdder totalTime = new LongAdder(); + public final AtomicLong minTime = new AtomicLong(0); + public final AtomicLong maxTime = new AtomicLong(0); + public final LongAdder errorCalls = new LongAdder(); + + /** + * 获取平均响应时间 + */ + public long getAvgTime() { + long calls = totalCalls.sum(); + return calls == 0 ? 0 : totalTime.sum() / calls; + } + + /** + * 获取成功率 + */ + public double getSuccessRate() { + long calls = totalCalls.sum(); + if (calls == 0) return 100.0; + long errors = errorCalls.sum(); + return ((calls - errors) * 100.0) / calls; + } + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java index a1a392e..32158bd 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java @@ -1,6 +1,7 @@ package com.ruoyi.system.service.impl; import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.annotation.PerformanceMonitor; import com.ruoyi.common.core.domain.entity.SysDictData; import com.ruoyi.common.core.domain.entity.SysDictType; import com.ruoyi.common.exception.ServiceException; @@ -71,6 +72,7 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService */ @Override @Cacheable(value = "dict", key = "#dictType", unless = "#result == null || #result.isEmpty()") + @PerformanceMonitor(slowThreshold = 500) public List selectDictDataByType(String dictType) { List dictDatas = DictUtils.getDictCache(dictType); diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java index cf5f2eb..ddbb007 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java @@ -13,6 +13,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import com.ruoyi.common.annotation.DataScope; +import com.ruoyi.common.annotation.PerformanceMonitor; import com.ruoyi.common.constant.UserConstants; import com.ruoyi.common.core.domain.entity.SysRole; import com.ruoyi.common.core.domain.entity.SysUser; @@ -71,6 +72,7 @@ public class SysUserServiceImpl implements ISysUserService */ @Override @DataScope(deptAlias = "d", userAlias = "u") + @PerformanceMonitor(slowThreshold = 1000, logParams = true) public List selectUserList(SysUser user) { return userMapper.selectUserList(user); @@ -123,6 +125,7 @@ public class SysUserServiceImpl implements ISysUserService */ @Override @Cacheable(value = "user", key = "#userId", unless = "#result == null") + @PerformanceMonitor(slowThreshold = 500) public SysUser selectUserById(Long userId) { return userMapper.selectUserById(userId); diff --git a/sql/monitor/01_create_slow_query_log.sql b/sql/monitor/01_create_slow_query_log.sql new file mode 100644 index 0000000..aeec5a1 --- /dev/null +++ b/sql/monitor/01_create_slow_query_log.sql @@ -0,0 +1,12 @@ +-- 慢查询日志表 +CREATE TABLE IF NOT EXISTS sys_slow_query_log ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + method_name VARCHAR(200) NOT NULL COMMENT '方法名', + execution_time BIGINT NOT NULL COMMENT '执行时间(ms)', + threshold BIGINT NOT NULL COMMENT '阈值(ms)', + params TEXT COMMENT '请求参数', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + INDEX idx_method_name (method_name), + INDEX idx_execution_time (execution_time), + INDEX idx_create_time (create_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='慢查询日志表';