SpringBoot如何監(jiān)聽接口調(diào)用情況
本文檔介紹在 SpringBoot 項目中監(jiān)聽每個接口的調(diào)用次數(shù)、傳入?yún)?shù)、返回參數(shù)、響應(yīng)耗時的多種實現(xiàn)方案
章節(jié)閱讀路線圖 ???
- AOP 切面方案(最推薦) → 使用 Spring AOP 無侵入式監(jiān)控所有接口
- 自定義注解方案 → 選擇性監(jiān)控,只針對關(guān)鍵接口
- HandlerInterceptor 攔截器方案 → 基于 Spring MVC 攔截器實現(xiàn)
- Micrometer + Actuator 生產(chǎn)級方案 → 接入 Prometheus + Grafana 可視化監(jiān)控
- 方案對比與總結(jié) → 根據(jù)場景選擇最合適的方案
1. AOP 切面方案(最推薦) ??
使用 Spring AOP 無侵入式監(jiān)控所有或者指定接口,記錄調(diào)用次數(shù)、參數(shù)、返回值和耗時
AOP(Aspect Oriented Programming)是 Spring 框架的核心特性之一,它可以在不修改原有代碼的情況下,對方法進(jìn)行增強(qiáng)。利用 AOP,我們只需要編寫一個切面類,就可以統(tǒng)一攔截所有 Controller 方法,實現(xiàn)接口監(jiān)控。
1.1 引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.x</version>
</dependency>?? spring-boot-starter-aop 已包含 AspectJ 依賴。fastjson 用于將參數(shù)和返回值序列化為 JSON 字符串,也可以用 Jackson(SpringBoot 默認(rèn)自帶)。
1.2 編寫切面類
package com.example.aspect; // 包名根據(jù)實際項目調(diào)整
import com.alibaba.fastjson.JSON; // 引入 JSON 序列化工具,用于格式化參數(shù)和返回值
import lombok.extern.slf4j.Slf4j; // 引入 Slf4j 注解,自動生成 log 日志對象
import org.aspectj.lang.ProceedingJoinPoint; // 引入 ProceedingJoinPoint,用于執(zhí)行目標(biāo)方法并獲取上下文
import org.aspectj.lang.annotation.Around; // 引入環(huán)繞通知注解,@Around 可以包裹目標(biāo)方法的執(zhí)行
import org.aspectj.lang.annotation.Aspect; // 引入切面注解,標(biāo)識該類為 AOP 切面
import org.aspectj.lang.annotation.Pointcut; // 引入切點注解,定義攔截規(guī)則
import org.springframework.stereotype.Component; // 引入 Component 注解,將切面注入 Spring 容器
import org.springframework.web.context.request.RequestContextHolder; // 引入 RequestContextHolder,獲取當(dāng)前請求的上下文
import org.springframework.web.context.request.ServletRequestAttributes; // 引入 ServletRequestAttributes,獲取 HttpServletRequest
import javax.servlet.http.HttpServletRequest; // 引入 HttpServletRequest,用于獲取請求 URL、方法類型等
import java.util.Arrays; // 引入 Arrays 工具類,用于數(shù)組操作
import java.util.Map; // 引入 Map 接口,用于存儲調(diào)用次數(shù)統(tǒng)計
import java.util.concurrent.ConcurrentHashMap; // 引入 ConcurrentHashMap,線程安全的計數(shù)器容器
import java.util.concurrent.atomic.AtomicLong; // 引入 AtomicLong,線程安全的原子計數(shù)器
/**
* 接口監(jiān)控切面
*
* 功能:統(tǒng)計每個接口的調(diào)用次數(shù)、記錄傳入?yún)?shù)、返回參數(shù)、響應(yīng)耗時
*
* Args:
* 無
*
* Returns:
* 無(通過 log 輸出監(jiān)控信息)
*
* Example:
* 自動生效,無需手動調(diào)用
*/
@Slf4j // Lombok 注解,自動生成 log 字段,示例:log.info("消息")
@Aspect // 標(biāo)識當(dāng)前類為切面類
@Component // 將當(dāng)前類注入 Spring 容器
public class ApiMonitorAspect {
// 線程安全的調(diào)用次數(shù)統(tǒng)計容器
// Key: 接口方法名(類名+方法名),Value: 原子計數(shù)器
// 示例:{"UserController.getUser" → 42, "OrderController.createOrder" → 15}
private final Map<String, AtomicLong> callCountMap = new ConcurrentHashMap<>();
/**
* 定義切點:攔截 com.example.controller 包下所有類的所有方法
*
* Args:
* 無
*
* Returns:
* 無
*
* Example:
* execution(* com.example.controller.*.*(..)) 表示攔截 controller 包下所有方法
*/
@Pointcut("execution(* com.example.controller.*.*(..))")
public void controllerPointcut() {
// 切點方法,無實際邏輯,僅用于定義攔截規(guī)則
}
/**
* 環(huán)繞通知:在目標(biāo)方法執(zhí)行前后插入監(jiān)控邏輯
*
* Args:
* joinPoint: 連接點對象,包含目標(biāo)方法的全部上下文信息
*
* Returns:
* Object: 目標(biāo)方法的返回值,原封不動返回
*
* Example:
* @Around("controllerPointcut()")
* public Object monitorApi(ProceedingJoinPoint joinPoint)
*/
@Around("controllerPointcut()")
public Object monitorApi(ProceedingJoinPoint joinPoint) throws Throwable {
// ===== 1. 獲取請求上下文信息 =====
// 通過 RequestContextHolder 獲取當(dāng)前線程綁定的 Servlet 請求屬性
// 數(shù)據(jù)流動:HttpServletRequest → RequestContextHolder → 提取 URL/Method/IP
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String requestUrl = request.getRequestURL().toString(); // 獲取完整請求 URL,示例:"http://localhost:8080/api/user/list"
String httpMethod = request.getMethod(); // 獲取 HTTP 方法類型,示例:"GET"、"POST"
String clientIp = request.getRemoteAddr(); // 獲取客戶端 IP 地址,示例:"127.0.0.1"
// ===== 2. 獲取方法信息 =====
// 獲取目標(biāo)方法的全限定名(包名+類名+方法名)
// 數(shù)據(jù)流動:joinPoint.getSignature() → 方法簽名 → 類名.方法名
String className = joinPoint.getTarget().getClass().getSimpleName(); // 獲取類名,示例:"UserController"
String methodName = className + "." + joinPoint.getSignature().getName(); // 組裝方法名,示例:"UserController.getUser"
// ===== 3. 統(tǒng)計調(diào)用次數(shù) =====
// 從 callCountMap 中獲取或創(chuàng)建該方法的原子計數(shù)器,然后自增
// 數(shù)據(jù)流動:methodName → ConcurrentHashMap.get() → AtomicLong.incrementAndGet() → 返回遞增后的值
long currentCount = callCountMap.computeIfAbsent(methodName, k -> new AtomicLong(0)).incrementAndGet();
// ===== 4. 獲取傳入?yún)?shù) =====
// 獲取目標(biāo)方法的所有參數(shù)值列表
// 數(shù)據(jù)流動:joinPoint.getArgs() → Object[] → JSON.toJSONString() → JSON 字符串
Object[] args = joinPoint.getArgs();
String requestParams = JSON.toJSONString(args); // 將參數(shù)數(shù)組序列化為 JSON,示例:"[{"id":1,"name":"張三"}]"
// ===== 5. 記錄開始時間 =====
// 記錄當(dāng)前時間戳,用于后續(xù)計算接口耗時
long startTime = System.currentTimeMillis();
// ===== 6. 執(zhí)行目標(biāo)方法 =====
// proceed() 會執(zhí)行被攔截的原始方法,返回原始結(jié)果
// 數(shù)據(jù)流動:joinPoint.proceed() → 目標(biāo)方法執(zhí)行 → 返回 Object 類型結(jié)果
Object result = joinPoint.proceed();
// ===== 7. 計算響應(yīng)耗時 =====
// 用結(jié)束時間減去開始時間,得到接口處理耗時
long elapsed = System.currentTimeMillis() - startTime; // 計算耗時,單位:毫秒
// ===== 8. 獲取返回參數(shù) =====
// 將方法返回值序列化為 JSON 字符串
// 數(shù)據(jù)流動:result Object → JSON.toJSONString() → JSON 字符串
String responseResult = JSON.toJSONString(result);
// ===== 9. 輸出監(jiān)控日志 =====
// 匯總所有監(jiān)控信息,統(tǒng)一輸出
log.info("\n========================================\n" +
"接口監(jiān)控信息:\n" +
" 請求地址: {} {}\n" +
" 客戶端IP: {}\n" +
" 接口方法: {}\n" +
" 調(diào)用次數(shù)(累計): {}\n" +
" 傳入?yún)?shù): {}\n" +
" 返回參數(shù): {}\n" +
" 響應(yīng)耗時: {}ms\n" +
"========================================",
httpMethod, requestUrl, // 填充請求地址
clientIp, // 填充客戶端 IP
methodName, // 填充接口方法名
currentCount, // 填充累計調(diào)用次數(shù)
requestParams.length() > 500 ? requestParams.substring(0, 500) + "..." : requestParams, // 參數(shù)過長時截斷
responseResult.length() > 500 ? responseResult.substring(0, 500) + "..." : responseResult, // 返回值過長時截斷
elapsed); // 填充響應(yīng)耗時
// 原封不動返回目標(biāo)方法的執(zhí)行結(jié)果,不對業(yè)務(wù)邏輯產(chǎn)生任何影響
return result;
}
/**
* 獲取指定接口的累計調(diào)用次數(shù)
*
* Args:
* methodName: 方法名,格式為 "類名.方法名",示例:"UserController.getUser"
*
* Returns:
* long: 該接口的累計調(diào)用次數(shù),如果未找到返回 0
*
* Example:
* long count = apiMonitorAspect.getCallCount("UserController.getUser");
*/
public long getCallCount(String methodName) {
AtomicLong counter = callCountMap.get(methodName); // 從 Map 中查詢指定方法的計數(shù)器
return counter != null ? counter.get() : 0; // 如果存在則返回計數(shù),否則返回 0
}
/**
* 獲取所有接口的調(diào)用次數(shù)統(tǒng)計(可用于對外暴露 API)
*
* Args:
* 無
*
* Returns:
* Map<String, Long>: 所有接口的調(diào)用次數(shù)映射表,示例:{"UserController.getUser": 42}
*
* Example:
* Map<String, Long> allStats = apiMonitorAspect.getAllCallCounts();
*/
public Map<String, Long> getAllCallCounts() {
Map<String, Long> result = new ConcurrentHashMap<>(); // 創(chuàng)建結(jié)果容器
callCountMap.forEach((key, value) -> result.put(key, value.get())); // 遍歷并轉(zhuǎn)換 AtomicLong 為普通 Long
return result; // 返回不可修改的統(tǒng)計視圖
}
}1.3 輸出示例
2026-05-28 14:30:22.156 [http-nio-8080-exec-1] INFO c.e.aspect.ApiMonitorAspect -
========================================
接口監(jiān)控信息:
請求地址: GET http://localhost:8080/api/user/list
客戶端IP: 127.0.0.1
接口方法: UserController.getUserList
調(diào)用次數(shù)(累計): 1
傳入?yún)?shù): [{"page":1,"size":10}]
返回參數(shù): {"code":200,"data":[{"id":1,"name":"張三"}],"msg":"成功"}
響應(yīng)耗時: 45ms
========================================2026-05-28 14:30:25.891 [http-nio-8080-exec-2] INFO c.e.aspect.ApiMonitorAspect -
========================================
接口監(jiān)控信息:
請求地址: POST http://localhost:8080/api/user/create
客戶端IP: 192.168.1.100
接口方法: UserController.createUser
調(diào)用次數(shù)(累計): 1
傳入?yún)?shù): [{"name":"李四","age":25,"email":"lisi@example.com"}]
返回參數(shù): {"code":200,"data":{"id":2},"msg":"創(chuàng)建成功"}
響應(yīng)耗時: 120ms
========================================
從輸出可以看到,監(jiān)控信息完整覆蓋了需求中的四個維度:
- 調(diào)用次數(shù):調(diào)用次數(shù)(累計): 1
- 傳入?yún)?shù):傳入?yún)?shù): [{"page":1,"size":10}]
- 返回參數(shù):返回參數(shù): {"code":200,"data":[...]}
- 響應(yīng)耗時:響應(yīng)耗時: 45ms
2. 自定義注解方案(選擇性監(jiān)控) ???
通過自定義注解實現(xiàn)選擇性監(jiān)控,只對關(guān)鍵接口生效
如果不想監(jiān)控所有接口,可以自定義一個注解,只有在方法上添加了該注解的接口才進(jìn)行監(jiān)控。
2.1 定義注解
package com.example.annotation; // 包名根據(jù)實際項目調(diào)整
import java.lang.annotation.ElementType; // 引入 ElementType 枚舉,用于指定注解的適用范圍
import java.lang.annotation.Retention; // 引入 Retention 枚舉,用于指定注解的生命周期
import java.lang.annotation.RetentionPolicy; // 引入 RetentionPolicy,RetentionPolicy.RUNTIME 表示運行時保留
import java.lang.annotation.Target; // 引入 Target 注解,用于限制注解能標(biāo)注的位置
/**
* 接口監(jiān)控注解
*
* 用于標(biāo)記需要監(jiān)控調(diào)用次數(shù)、參數(shù)、耗時等信息的接口方法
*
* Args:
* value: 接口描述信息,可選,示例:"用戶登錄接口"
*
* Returns:
* 無(注解本身不返回值)
*
* Example:
* @Monitored("用戶登錄接口")
* public Result login(@RequestBody LoginDTO dto)
*/
@Target(ElementType.METHOD) // 注解只能標(biāo)注在方法上
@Retention(RetentionPolicy.RUNTIME) // 注解在運行時保留,AOP 才能在運行時讀取
public @interface Monitored {
/**
* 接口描述信息
*
* Args:
* 無
*
* Returns:
* String: 接口的描述說明,默認(rèn)為空字符串
*
* Example:
* @Monitored("用戶登錄接口")
*/
String value() default ""; // 可選屬性,用于描述接口用途
}2.2 修改切面類
/**
* 基于自定義注解的接口監(jiān)控切面
*
* 功能:只對標(biāo)注了 @Monitored 注解的方法進(jìn)行監(jiān)控
*
* Args:
* 無
*
* Returns:
* 無(通過 log 輸出監(jiān)控信息)
*
* Example:
* 在 Controller 方法上添加 @Monitored 即可生效
*/
@Slf4j
@Aspect
@Component
public class MonitoredApiAspect {
// 線程安全的調(diào)用次數(shù)統(tǒng)計容器
// Key: 接口方法名,Value: 原子計數(shù)器
private final Map<String, AtomicLong> callCountMap = new ConcurrentHashMap<>();
/**
* 定義切點:攔截所有標(biāo)注了 @Monitored 注解的方法
*
* Args:
* monitored: 注入的注解實例,可在通知中讀取注解屬性
*
* Returns:
* 無
*
* Example:
* @annotation(com.example.annotation.Monitored) 匹配所有標(biāo)有 @Monitored 的方法
*/
@Pointcut("@annotation(com.example.annotation.Monitored)")
public void monitoredPointcut() {
// 切點方法,無實際邏輯
}
/**
* 環(huán)繞通知:攔截并監(jiān)控標(biāo)注了 @Monitored 的方法
*
* Args:
* joinPoint: 連接點對象,包含目標(biāo)方法的全部上下文信息
* monitored: 目標(biāo)方法上的 @Monitored 注解實例,可讀取 value 屬性
*
* Returns:
* Object: 目標(biāo)方法的返回值
*
* Example:
* @Around("monitoredPointcut() && @annotation(monitored)")
*/
@Around("monitoredPointcut() && @annotation(monitored)")
public Object monitorAnnotatedApi(ProceedingJoinPoint joinPoint, Monitored monitored) throws Throwable {
// 獲取注解中填寫的接口描述,示例:"用戶登錄接口"
String description = monitored.value();
// 獲取方法信息
String methodName = joinPoint.getTarget().getClass().getSimpleName() + "." + joinPoint.getSignature().getName();
// 統(tǒng)計調(diào)用次數(shù)
long currentCount = callCountMap.computeIfAbsent(methodName, k -> new AtomicLong(0)).incrementAndGet();
// 獲取傳入?yún)?shù)
Object[] args = joinPoint.getArgs();
String requestParams = JSON.toJSONString(args);
// 獲取請求信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 記錄開始時間
long startTime = System.currentTimeMillis();
// 執(zhí)行目標(biāo)方法
Object result = joinPoint.proceed();
// 計算耗時
long elapsed = System.currentTimeMillis() - startTime;
// 輸出監(jiān)控日志
log.info("[接口監(jiān)控] {} | {} | 方法: {} | 累計調(diào)用: {} | 參數(shù): {} | 耗時: {}ms | 返回: {}",
request.getMethod(), request.getRequestURL(),
methodName, currentCount,
requestParams, elapsed,
JSON.toJSONString(result));
return result;
}
}
2.3 使用方式
@RestController
@RequestMapping("/api/user")
public class UserController {
@Monitored("用戶登錄接口") // 標(biāo)注需要監(jiān)控的方法
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO) {
// 業(yè)務(wù)邏輯
return Result.success("登錄成功");
}
@GetMapping("/list") // 未標(biāo)注 @Monitored,不會被監(jiān)控
public Result list() {
return Result.success(userService.list());
}
}
3. HandlerInterceptor 攔截器方案 ??
使用 Spring MVC 的 HandlerInterceptor,在請求處理前后插入監(jiān)控邏輯
Interceptor 是 Spring MVC 特有的攔截機(jī)制,在請求進(jìn)入 Controller 之前和響應(yīng)返回之后執(zhí)行。適合只需要監(jiān)控 HTTP 請求的場景。
3.1 實現(xiàn)攔截器
package com.example.interceptor; // 包名根據(jù)實際項目調(diào)整
import com.alibaba.fastjson.JSON; // 引入 JSON 序列化工具,用于格式化請求參數(shù)
import lombok.extern.slf4j.Slf4j; // 引入 Slf4j 注解
import org.springframework.stereotype.Component; // 引入 Component 注解,將攔截器注入 Spring 容器
import org.springframework.web.servlet.HandlerInterceptor; // 引入 HandlerInterceptor 接口,需要實現(xiàn) preHandle 和 afterCompletion
import org.springframework.web.servlet.ModelAndView; // 引入 ModelAndView 類
import javax.servlet.http.HttpServletRequest; // 引入 HttpServletRequest
import javax.servlet.http.HttpServletResponse; // 引入 HttpServletResponse
import java.util.Map; // 引入 Map 接口,用于存儲統(tǒng)計信息
import java.util.concurrent.ConcurrentHashMap; // 引入 ConcurrentHashMap,線程安全的統(tǒng)計容器
import java.util.concurrent.atomic.AtomicLong; // 引入 AtomicLong,線程安全的計數(shù)器
/**
* 接口監(jiān)控攔截器
*
* 功能:在請求處理前后記錄調(diào)用次數(shù)、參數(shù)、耗時等監(jiān)控信息
*
* Args:
* 無
*
* Returns:
* 無(通過 log 輸出監(jiān)控信息)
*
* Example:
* 在 WebConfig 中注冊該攔截器即可生效
*/
@Slf4j
@Component
public class ApiMonitorInterceptor implements HandlerInterceptor {
// 線程安全的統(tǒng)計容器
// Key: URL(請求路徑),Value: 原子計數(shù)器
// 示例:{"/api/user/list": 42, "/api/user/create": 15}
private final Map<String, AtomicLong> urlCountMap = new ConcurrentHashMap<>();
// 使用 ThreadLocal 存儲每個請求的開始時間,確保線程隔離
// 數(shù)據(jù)流動:preHandle 中 set(startTime) → afterCompletion 中 get() 計算耗時 → finally 中 remove() 清理
private final ThreadLocal<Long> startTimeHolder = new ThreadLocal<>();
// 使用 ThreadLocal 存儲每個請求的 URL,用于在 afterCompletion 中統(tǒng)計
private final ThreadLocal<String> urlHolder = new ThreadLocal<>();
/**
* 請求處理前執(zhí)行
*
* Args:
* request: HTTP 請求對象
* response: HTTP 響應(yīng)對象
* handler: 處理該請求的處理器(Controller 方法)
*
* Returns:
* boolean: true 表示繼續(xù)執(zhí)行,false 表示中斷請求
*
* Example:
* preHandle 中記錄開始時間和請求參數(shù)
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 獲取請求 URL,示例:"/api/user/list"
String requestUrl = request.getRequestURI();
// 將請求 URL 存入 ThreadLocal,供 afterCompletion 使用
urlHolder.set(requestUrl);
// 該請求的調(diào)用次數(shù)加 1
// 數(shù)據(jù)流動:urlCountMap.get(requestUrl) → AtomicLong.incrementAndGet() → 返回自增后的值
long currentCount = urlCountMap.computeIfAbsent(requestUrl, k -> new AtomicLong(0)).incrementAndGet();
// 記錄請求開始時間,存入 ThreadLocal
startTimeHolder.set(System.currentTimeMillis());
// 記錄請求信息
log.info("[請求開始] {} {} | 累計調(diào)用: {} | 參數(shù): {}",
request.getMethod(), // HTTP 方法,示例:"GET"
requestUrl, // 請求路徑,示例:"/api/user/list"
currentCount, // 累計調(diào)用次數(shù)
JSON.toJSONString(request.getParameterMap()) // 請求參數(shù) Map,示例:{"page":["1"],"size":["10"]}
);
return true; // 返回 true,繼續(xù)執(zhí)行后續(xù)處理
}
/**
* 請求完成后執(zhí)行(無論是否異常)
*
* Args:
* request: HTTP 請求對象
* response: HTTP 響應(yīng)對象
* handler: 處理該請求的處理器
* ex: 處理過程中拋出的異常(如果有)
*
* Returns:
* 無
*
* Example:
* afterCompletion 中計算耗時并輸出日志
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
// 從 ThreadLocal 中取出開始時間
Long startTime = startTimeHolder.get();
if (startTime == null) { // 安全校驗:如果 startTime 為空,說明 preHandle 未執(zhí)行成功
return; // 直接返回,不進(jìn)行后續(xù)處理
}
// 計算處理耗時,單位:毫秒
// 數(shù)據(jù)流動:結(jié)束時間戳 - 開始時間戳 → 耗時毫秒數(shù)
long elapsed = System.currentTimeMillis() - startTime;
// 獲取請求 URL
String requestUrl = urlHolder.get();
// 獲取 HTTP 狀態(tài)碼,示例:200、404、500
int status = response.getStatus();
// 輸出監(jiān)控日志
log.info("[請求結(jié)束] {} | 狀態(tài): {} | 耗時: {}ms{}",
requestUrl, // 請求路徑
status, // HTTP 狀態(tài)碼
elapsed, // 響應(yīng)耗時
ex != null ? " | 異常: " + ex.getMessage() : "" // 如果有異常則追加異常信息
);
} finally {
// 清理 ThreadLocal,防止內(nèi)存泄漏(線程池中的線程會被復(fù)用)
startTimeHolder.remove();
urlHolder.remove();
}
}
}
3.2 注冊攔截器
package com.example.config; // 包名根據(jù)實際項目調(diào)整
import com.example.interceptor.ApiMonitorInterceptor; // 引入自定義攔截器
import org.springframework.beans.factory.annotation.Autowired; // 引入自動注入注解
import org.springframework.context.annotation.Configuration; // 引入 Configuration 注解,標(biāo)識為配置類
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; // 引入攔截器注冊器
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; // 引入 WebMvcConfigurer 接口,用于自定義 Spring MVC 配置
/**
* Web MVC 配置類
*
* 功能:注冊自定義攔截器到 Spring MVC 攔截器鏈中
*
* Args:
* 無
*
* Returns:
* 無
*
* Example:
* @Configuration 注解的類會自動被 Spring 掃描并加載配置
*/
@Configuration // 標(biāo)識為配置類
public class WebConfig implements WebMvcConfigurer {
@Autowired
private ApiMonitorInterceptor apiMonitorInterceptor; // 注入自定義的接口監(jiān)控攔截器
/**
* 添加攔截器到 Spring MVC 框架
*
* Args:
* registry: 攔截器注冊器,用于添加和配置攔截器
*
* Returns:
* 無
*
* Example:
* registry.addInterceptor(interceptor).addPathPatterns("/**");
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiMonitorInterceptor) // 添加自定義攔截器
.addPathPatterns("/**"); // 攔截所有請求路徑
}
}
4. Micrometer + Actuator 生產(chǎn)級方案 ??
使用 Micrometer 指標(biāo)框架 + Spring Actuator 暴露指標(biāo),接入 Prometheus + Grafana 實現(xiàn)可視化監(jiān)控
前面的方案適合開發(fā)調(diào)試和簡單的日志監(jiān)控,但在生產(chǎn)環(huán)境中,我們需要持久化的指標(biāo)存儲和可視化看板。Micrometer 是 Spring Boot 官方推薦的指標(biāo)采集框架,可以無縫對接 Prometheus、InfluxDB 等監(jiān)控系統(tǒng)。
4.1 引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>4.2 配置 application.yml
spring:
application:
name: my-api-service # 應(yīng)用名稱,作為 Prometheus 指標(biāo)的標(biāo)簽
management:
endpoints:
web:
exposure:
include: prometheus,metrics,health # 暴露 Prometheus、Metrics、Health 端點
metrics:
tags:
application: ${spring.application.name} # 為所有指標(biāo)添加 application 標(biāo)簽
distribution:
percentiles-histogram:
http.server.requests: true # 為 HTTP 請求指標(biāo)啟用百分位數(shù)直方圖
slo:
http.server.requests: 10ms, 50ms, 100ms, 200ms, 500ms, 1s # 定義服務(wù)級別目標(biāo)(SLO)的耗時邊界4.3 驗證指標(biāo)端點
啟動應(yīng)用后,訪問 http://localhost:8080/actuator/prometheus,可以看到類似以下輸出:
# HELP http_server_requests_seconds
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{application="my-api-service",method="GET",outcome="SUCCESS",status="200",uri="/api/user/list"} 15.0
http_server_requests_seconds_sum{application="my-api-service",method="GET",outcome="SUCCESS",status="200",uri="/api/user/list"} 0.892
http_server_requests_seconds_max{application="my-api-service",method="GET",outcome="SUCCESS",status="200",uri="/api/user/list"} 0.124指標(biāo)含義:
| 指標(biāo) | 含義 |
|---|---|
| http_server_requests_seconds_count | 接口調(diào)用總次數(shù) |
| http_server_requests_seconds_sum | 接口調(diào)用累計耗時(秒) |
| http_server_requests_seconds_max | 接口調(diào)用最大耗時(秒) |
4.4 自定義業(yè)務(wù)指標(biāo)
除了框架自動采集的 HTTP 指標(biāo),還可以自定義業(yè)務(wù)指標(biāo),例如按接口記錄詳細(xì)參數(shù):
package com.example.metrics; // 包名根據(jù)實際項目調(diào)整
import io.micrometer.core.instrument.Counter; // 引入 Micrometer 計數(shù)器,用于統(tǒng)計調(diào)用次數(shù)
import io.micrometer.core.instrument.MeterRegistry; // 引入 MeterRegistry,Micrometer 指標(biāo)注冊中心
import io.micrometer.core.instrument.Timer; // 引入 Micrometer 計時器,用于統(tǒng)計耗時
import lombok.extern.slf4j.Slf4j; // 引入 Slf4j 注解
import org.aspectj.lang.ProceedingJoinPoint; // 引入 ProceedingJoinPoint
import org.aspectj.lang.annotation.Around; // 引入環(huán)繞通知注解
import org.aspectj.lang.annotation.Aspect; // 引入切面注解
import org.aspectj.lang.annotation.Pointcut; // 引入切點注解
import org.springframework.stereotype.Component; // 引入 Component 注解
import java.util.concurrent.TimeUnit; // 引入 TimeUnit,用于時間單位轉(zhuǎn)換
/**
* Micrometer 自定義指標(biāo)切面
*
* 功能:統(tǒng)計每個接口的調(diào)用次數(shù)和耗時,以 Prometheus 指標(biāo)格式暴露
*
* Args:
* 無
*
* Returns:
* 無(指標(biāo)數(shù)據(jù)自動注冊到 MeterRegistry)
*
* Example:
* 結(jié)合 Actuator + Prometheus + Grafana 實現(xiàn)可視化監(jiān)控
*/
@Slf4j
@Aspect
@Component
public class MicrometerMetricsAspect {
private final MeterRegistry meterRegistry; // Micrometer 指標(biāo)注冊中心,用于創(chuàng)建和管理指標(biāo)
/**
* 構(gòu)造方法注入 MeterRegistry
*
* Args:
* meterRegistry: Spring Boot 自動配置的 MeterRegistry 實例
*
* Returns:
* 無
*/
public MicrometerMetricsAspect(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
/**
* 定義切點:攔截所有 Controller 方法
*/
@Pointcut("execution(* com.example.controller.*.*(..))")
public void controllerPointcut() {}
/**
* 環(huán)繞通知:采集接口調(diào)用次數(shù)和耗時指標(biāo)
*
* Args:
* joinPoint: 連接點對象
*
* Returns:
* Object: 目標(biāo)方法的返回值
*/
@Around("controllerPointcut()")
public Object recordMetrics(ProceedingJoinPoint joinPoint) throws Throwable {
// 獲取接口名稱,用于指標(biāo)標(biāo)簽
String methodName = joinPoint.getTarget().getClass().getSimpleName() + "." + joinPoint.getSignature().getName();
// 創(chuàng)建計時器:統(tǒng)計該接口的耗時分布
// 數(shù)據(jù)流動:Timer.Sample 記錄開始 → 方法執(zhí)行 → sample.stop() 記錄結(jié)束 → 指標(biāo)注冊到 Prometheus
Timer.Sample sample = Timer.start(meterRegistry);
try {
// 執(zhí)行目標(biāo)方法
Object result = joinPoint.proceed();
return result;
} finally {
// 停止計時器,并將指標(biāo)注冊到 MeterRegistry
// 數(shù)據(jù)流動:sample.stop(timer) → /actuator/prometheus 暴露 → Prometheus 采集 → Grafana 展示
sample.stop(Timer.builder("api.call.duration") // 指標(biāo)名稱
.description("接口調(diào)用耗時") // 指標(biāo)描述
.tag("method", methodName) // 標(biāo)簽:接口方法名
.tag("class", joinPoint.getTarget().getClass().getSimpleName()) // 標(biāo)簽:類名
.register(meterRegistry)); // 注冊到 MeterRegistry
// 增加調(diào)用次數(shù)計數(shù)器
// 數(shù)據(jù)流動:Counter.increment() → 指標(biāo)值 +1 → Prometheus 采集
Counter counter = Counter.builder("api.call.count") // 指標(biāo)名稱
.description("接口調(diào)用次數(shù)") // 指標(biāo)描述
.tag("method", methodName) // 標(biāo)簽:接口方法名
.register(meterRegistry); // 注冊到 MeterRegistry
counter.increment(); // 計數(shù)器加 1
}
}
}配置完成后,/actuator/prometheus 端點會額外輸出自定義指標(biāo):
# HELP api_call_duration_seconds 接口調(diào)用耗時
# TYPE api_call_duration_seconds summary
api_call_duration_seconds_count{method="UserController.getUserList",} 5.0
api_call_duration_seconds_sum{method="UserController.getUserList",} 0.345
# HELP api_call_count_total 接口調(diào)用次數(shù)
# TYPE api_call_count_total counter
api_call_count_total{method="UserController.getUserList",} 5.0
5. 基于事件監(jiān)聽方案 ??
使用 Spring 框架的 ServletRequestHandledEvent 事件監(jiān)聽 HTTP 請求處理完成事件
Spring 框架在每次 HTTP 請求處理完成后會發(fā)布 ServletRequestHandledEvent 事件,包含了請求的詳細(xì)信息。只需監(jiān)聽該事件即可獲取接口耗時,無需任何切面或攔截器配置。
注意:此方案只能獲取耗時和基本請求信息(URL、方法、狀態(tài)碼),無法獲取傳入?yún)?shù)和返回參數(shù)。
package com.example.listener; // 包名根據(jù)實際項目調(diào)整
import lombok.extern.slf4j.Slf4j; // 引入 Slf4j 注解
import org.springframework.context.ApplicationListener; // 引入 ApplicationListener 接口
import org.springframework.stereotype.Component; // 引入 Component 注解
import org.springframework.web.context.support.ServletRequestHandledEvent; // 引入請求處理事件類
/**
* 請求耗時事件監(jiān)聽器
*
* 功能:監(jiān)聽 ServletRequestHandledEvent 事件,記錄每個接口的調(diào)用耗時
* 注意:此方案無法獲取請求參數(shù)和返回參數(shù),僅能獲取 URL、方法、耗時
*
* Args:
* 無
*
* Returns:
* 無(通過 log 輸出監(jiān)控信息)
*
* Example:
* 自動生效,無需額外配置
*/
@Slf4j
@Component
public class RequestTimeEventListener implements ApplicationListener<ServletRequestHandledEvent> {
/**
* 事件處理方法:ServletRequestHandledEvent 發(fā)布時自動調(diào)用
*
* Args:
* event: ServletRequestHandledEvent 事件對象,包含請求的 URL、方法、耗時、客戶端地址等信息
*
* Returns:
* 無
*
* Example:
* 請求完成后自動觸發(fā),輸出:
* "clientAddress=127.0.0.1, requestUrl=/api/user/list, method=GET, costTime=45ms"
*/
@Override
public void onApplicationEvent(ServletRequestHandledEvent event) {
// 獲取客戶端 IP 地址,示例:"127.0.0.1"
String clientAddress = event.getClientAddress();
// 獲取請求 URL,示例:"/api/user/list"
String requestUrl = event.getRequestUrl();
// 獲取 HTTP 方法類型,示例:"GET"、"POST"
String method = event.getMethod();
// 獲取請求處理耗時,單位:毫秒
// 數(shù)據(jù)流動:Spring 框架自動計時 → event.getProcessingTimeMillis() → 輸出日志
long processingTimeMillis = event.getProcessingTimeMillis();
// 獲取失敗原因(如果有異常)
Throwable failureCause = event.getFailureCause();
String failureMessage = failureCause == null ? "" : failureCause.getMessage();
// 根據(jù)是否有異常,分別輸出不同級別的日志
if (failureCause == null) {
// 請求成功:輸出 INFO 級別日志
log.info("clientAddress={}, requestUrl={}, method={}, costTime={}ms",
clientAddress, requestUrl, method, processingTimeMillis);
} else {
// 請求失?。狠敵?ERROR 級別日志,包含異常信息
log.error("clientAddress={}, requestUrl={}, method={}, costTime={}ms, error={}",
clientAddress, requestUrl, method, processingTimeMillis, failureMessage);
}
}
}參考資料:
6. 方案對比與總結(jié) ??
| 特性 | AOP 切面 | 自定義注解 | HandlerInterceptor | Micrometer + Actuator | 事件監(jiān)聽 |
|---|---|---|---|---|---|
| 調(diào)用次數(shù) | ? | ? | ? | ?(自動采集) | ? |
| 傳入?yún)?shù) | ? | ? | ?(僅 URL 參數(shù)) | ? | ? |
| 返回參數(shù) | ? | ? | ? | ? | ? |
| 響應(yīng)耗時 | ? | ? | ? | ?(自動采集) | ? |
| 代碼入侵 | 無 | 需加注解 | 無 | 需自定義指標(biāo) | 無 |
| 生產(chǎn)可用 | 開發(fā)調(diào)試 | 開發(fā)調(diào)試 | 開發(fā)調(diào)試 | ? 生產(chǎn)級 | 輕量監(jiān)控 |
| 可視化 | ?(日志) | ?(日志) | ?(日志) | ?(Grafana) | ?(日志) |
| 實現(xiàn)難度 | ?? | ?? | ? | ??? | ? |
場景推薦
| 場景 | 推薦方案 |
|---|---|
| 開發(fā)/測試環(huán)境臨時統(tǒng)計 | AOP 切面(方案 1) |
| 只監(jiān)控關(guān)鍵接口 | 自定義注解(方案 2) |
| 需要請求參數(shù)+返回參數(shù)完整記錄 | AOP 切面(方案 1) |
| 只需監(jiān)控 HTTP 請求基本信息 | HandlerInterceptor(方案 3) |
| 生產(chǎn)環(huán)境可視化監(jiān)控大屏 | Micrometer + Prometheus + Grafana(方案 4) |
| 最輕量級、零配置 | 事件監(jiān)聽(方案 5) |
實際項目建議
在實際項目中,通常組合使用多種方案:
- 生產(chǎn)環(huán)境以 Micrometer + Prometheus + Grafana 為基礎(chǔ)監(jiān)控設(shè)施,用于實時告警和歷史趨勢分析
- 開發(fā)測試階段輔以 AOP 切面記錄詳細(xì)的請求參數(shù)和返回參數(shù),方便定位問題
- 關(guān)鍵業(yè)務(wù)接口(如支付、登錄)使用自定義注解進(jìn)行重點監(jiān)控
這種分層監(jiān)控策略既能滿足生產(chǎn)環(huán)境的穩(wěn)定性要求,又能提供足夠的調(diào)試信息,是大型 SpringBoot 項目的推薦實踐。
到此這篇關(guān)于SpringBoot如何監(jiān)聽接口調(diào)用情況的文章就介紹到這了,更多相關(guān)SpringBoot 監(jiān)聽接口調(diào)用情況內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用MockMvc進(jìn)行controller層單元測試 事務(wù)自動回滾的完整案例
這篇文章主要介紹了使用MockMvc進(jìn)行controller層單元測試 事務(wù)自動回滾的完整案例,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06
SpringBoot中四種AOP實戰(zhàn)應(yīng)用場景及代碼實現(xiàn)
面向切面編程(AOP)是Spring框架的核心功能之一,它通過預(yù)編譯和運行期動態(tài)代理實現(xiàn)程序功能的統(tǒng)一維護(hù),在SpringBoot應(yīng)用中,AOP能夠幫助我們優(yōu)雅地解決橫切關(guān)注點問題,本文將介紹SpringBoot中4種AOP實戰(zhàn)應(yīng)用場景,需要的朋友可以參考下2025-05-05
SpringBoot實現(xiàn)WebSocket即時通訊的示例代碼
本文主要介紹了SpringBoot實現(xiàn)WebSocket即時通訊的示例代碼,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-04-04
eclipse springboot工程打war包方法及再Tomcat中運行的方法
這篇文章主要介紹了eclipse springboot工程打war包方法及再Tomcat中運行的方法,本文圖文并茂給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2019-08-08
SpringBoot中手動開啟事務(wù)的實現(xiàn)方法
在Spring Boot中,除了使用@Transactional注解外,還可以通過TransactionTemplate或PlatformTransactionManager手動控制事務(wù),本文給大家介紹SpringBoot中如何手動開啟事務(wù),感興趣的朋友跟隨小編一起看看吧2025-11-11

