SpringBoot監(jiān)控API請(qǐng)求耗時(shí)的6中解決解決方案
1. 簡(jiǎn)介
在微服務(wù)架構(gòu)與高并發(fā)場(chǎng)景下,API接口的響應(yīng)速度直接影響用戶體驗(yàn)與系統(tǒng)穩(wěn)定性。隨著業(yè)務(wù)復(fù)雜度提升,接口性能問(wèn)題逐漸成為系統(tǒng)瓶頸,例如數(shù)據(jù)庫(kù)查詢延遲、第三方服務(wù)調(diào)用超時(shí)等場(chǎng)景,均可能導(dǎo)致接口耗時(shí)激增。傳統(tǒng)的手動(dòng)埋點(diǎn)統(tǒng)計(jì)方式(如在每個(gè)接口方法中記錄開(kāi)始與結(jié)束時(shí)間)存在代碼侵入性強(qiáng)、維護(hù)成本高的問(wèn)題,難以滿足大規(guī)模接口的監(jiān)控需求。
本篇文章將介紹 Spring Boot 中記錄 API 請(qǐng)求耗時(shí)的 6 種實(shí)用方案,涵蓋從基礎(chǔ)到進(jìn)階的多種實(shí)現(xiàn)方式,幫助開(kāi)發(fā)者根據(jù)業(yè)務(wù)場(chǎng)景選擇最適合的監(jiān)控手段。
2.實(shí)戰(zhàn)案例
2.1 手動(dòng)記錄
我們可以通過(guò)Spring內(nèi)置的StopWatch工具進(jìn)行記錄方法執(zhí)行耗時(shí)情況:
@GetMapping("/query")
public ResponseEntity<?> query() throws Exception {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 業(yè)務(wù)邏輯
TimeUnit.MILLISECONDS.sleep(new Random().nextLong(2000)) ;
stopWatch.stop();
System.out.printf("方法耗時(shí):%dms%n", stopWatch.getTotalTimeMillis()) ;
return ResponseEntity.ok("api query...") ;
}
運(yùn)行結(jié)果
方法耗時(shí):1096ms
針對(duì)手動(dòng)記錄這里總結(jié)2點(diǎn)缺點(diǎn):
- 代碼侵入性強(qiáng):每次需要記錄請(qǐng)求耗時(shí)的時(shí)候都需要在具體的業(yè)務(wù)邏輯中插入相應(yīng)的計(jì)時(shí)代碼(如使用System.currentTimeMillis()或StopWatch)。這種方式會(huì)增加代碼的復(fù)雜度,并且使得業(yè)務(wù)邏輯與性能監(jiān)控代碼耦合在一起,降低了代碼的可讀性和維護(hù)性。
- 重復(fù)工作:如果多個(gè)地方都需要進(jìn)行類似的耗時(shí)統(tǒng)計(jì),則可能需要在每個(gè)地方都添加相同的代碼,這導(dǎo)致了代碼的重復(fù)。違反了DRY(Don't Repeat Yourself)原則。
2.2 自定義AOP記錄
通過(guò)自定義注解結(jié)合Spring AOP切面編程,可實(shí)現(xiàn)無(wú)侵入式的方法執(zhí)行耗時(shí)統(tǒng)計(jì)與記錄。
@Aspect
@Component
public class PerformanceAspect {
private static final Logger logger = LoggerFactory.getLogger("api.timed") ;
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping) || "
+ "@annotation(org.springframework.web.bind.annotation.GetMapping) || "
+ "@annotation(org.springframework.web.bind.annotation.PostMapping) || "
+ "@annotation(org.springframework.web.bind.annotation.PutMapping) || "
+ "@annotation(org.springframework.web.bind.annotation.DeleteMapping) || "
+ "@annotation(org.springframework.web.bind.annotation.PatchMapping)")
public Object recordExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
StopWatch sw = new StopWatch();
sw.start();
Object result = pjp.proceed();
sw.stop();
logger.info("方法【{}】耗時(shí): {}ms", pjp.getSignature(), sw.getTotalTimeMillis()) ;
return result;
}
}
在該示例,我們并沒(méi)有自定義注解,而是直接攔截了定義Controller接口使用的注解。
運(yùn)行結(jié)果
Line:29 - 方法【ResponseEntity ApiController.query()】耗時(shí): 487ms
總結(jié):AOP實(shí)現(xiàn)耗時(shí)記錄非侵入、統(tǒng)一管理、減少重復(fù)代碼,適合全局監(jiān)控。對(duì)非Spring管理的方法無(wú)效,增加切面復(fù)雜度,可能影響性能。
2.3 攔截器技術(shù)
通過(guò)攔截器不用修改任何業(yè)務(wù)代碼就能非常方便的記錄方法執(zhí)行耗時(shí)情況。
public class TimedInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
request.setAttribute("startTime", System.currentTimeMillis());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
long startTime = (long) request.getAttribute("startTime");
long cost = System.currentTimeMillis() - startTime;
System.out.printf("請(qǐng)求【%s】耗時(shí): %dms%n", request.getRequestURI(), cost) ;
}
}
注冊(cè)攔截器
@Component
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TimedInterceptor())
.addPathPatterns("/api/**") ;
}
}運(yùn)行結(jié)果
請(qǐng)求【/api/query】耗時(shí): 47ms
總結(jié):攔截器可集中管理請(qǐng)求耗時(shí)記錄,減少代碼侵入性,適用于Controller層統(tǒng)一監(jiān)控。僅對(duì)Web請(qǐng)求(Controller)生效,無(wú)法捕獲非HTTP接口或內(nèi)部方法調(diào)用耗時(shí),粒度較粗。
2.4 使用Filter
Filter 是 Servlet 規(guī)范中的過(guò)濾器,用于在請(qǐng)求到達(dá)目標(biāo)資源前后進(jìn)行攔截處理,可用于日志記錄、權(quán)限校驗(yàn)等通用邏輯。
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 確保最先執(zhí)行
public class RequestTimingFilter implements Filter {
private static final PathPatternParser parser = new PathPatternParser();
private static final Logger logger = LoggerFactory.getLogger(RequestTimingFilter.class);
// 從配置文件中讀取排除路徑
@Value("${timing.filter.exclude-paths}")
private String[] excludePaths;
// 路徑匹配器緩存
private List<PathPattern> excludedPatterns = Collections.emptyList();
@Override
public void init(FilterConfig filterConfig) {
// 初始化時(shí)編譯排除路徑的正則表達(dá)式
excludedPatterns = Arrays.stream(excludePaths).map(path -> parser.parse(path)).toList() ;
logger.info("記錄請(qǐng)求耗時(shí),不記錄的URI: {}", Arrays.toString(excludePaths));
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
// 檢查是否在排除路徑中
if (shouldExclude(requestURI)) {
chain.doFilter(request, response);
return;
}
long startTime = System.nanoTime();
try {
// 執(zhí)行后續(xù)過(guò)濾器鏈和實(shí)際請(qǐng)求處理
chain.doFilter(request, response);
} finally {
long endTime = System.nanoTime();
long durationNanos = endTime - startTime;
long durationMillis = TimeUnit.NANOSECONDS.toMillis(durationNanos);
// 記錄日志(包含請(qǐng)求方法和狀態(tài)碼)
if (response instanceof HttpServletResponse httpResponse) {
int status = httpResponse.getStatus();
logger.info("[{}] {} - {}ms (Status: {})", httpRequest.getMethod(), requestURI, durationMillis, status);
} else {
logger.info("[{}] {} - {}ms", httpRequest.getMethod(), requestURI, durationMillis);
}
}
}
private boolean shouldExclude(String requestURI) {
return excludedPatterns.stream()
.anyMatch(pattern -> pattern.matches(PathContainer.parsePath(requestURI))) ;
}
}
運(yùn)行結(jié)果
RequestTimingFilter Line:77 - [GET] /api/query - 379ms (Status: 200)
總結(jié):Filter 實(shí)現(xiàn)耗時(shí)記錄非侵入,適用于全局請(qǐng)求監(jiān)控,配置簡(jiǎn)單;僅能記錄整個(gè)請(qǐng)求的處理時(shí)間,無(wú)法精確到具體方法或業(yè)務(wù)邏輯,粒度較粗。
2.5 通過(guò)事件監(jiān)聽(tīng)
在Spring MVC底層內(nèi)部,當(dāng)一個(gè)請(qǐng)求處理完成以后會(huì)發(fā)布ServletRequestHandledEvent 事件,通過(guò)監(jiān)聽(tīng)該事件就能獲取請(qǐng)求的詳細(xì)信息。
@Component
public class TimedListener {
@EventListener(ServletRequestHandledEvent.class)
public void recordTimed(ServletRequestHandledEvent event) {
System.err.println(event) ;
}
}運(yùn)行結(jié)果
ServletRequestHandledEvent: url=[/api/query]; client=[0:0:0:0:0:0:0:1]; method=[GET]; status=[200]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[696ms]
詳細(xì)的輸出了當(dāng)前請(qǐng)求的信息。
總結(jié):非侵入式獲取請(qǐng)求處理耗時(shí),適用于全局監(jiān)控且無(wú)需修改業(yè)務(wù)代碼;僅能獲取整個(gè)請(qǐng)求的耗時(shí)信息,無(wú)法定位具體方法或模塊性能問(wèn)題,粒度較粗。
2.6 Micrometer + Prometheus
Micrometer 是一個(gè)指標(biāo)度量工具,支持多種監(jiān)控系統(tǒng),如 Prometheus。通過(guò) @Timed 注解可輕松記錄方法耗時(shí);Prometheus 是開(kāi)源監(jiān)控系統(tǒng),擅長(zhǎng)拉取和聚合指標(biāo),常與 Micrometer 集成實(shí)現(xiàn)可視化監(jiān)控。兩者結(jié)合適合微服務(wù)性能觀測(cè)。
@Timed(value = "api.query", description = "查詢業(yè)務(wù)接口")
@GetMapping("/query")
public ResponseEntity<?> query() throws Exception {
TimeUnit.MILLISECONDS.sleep(new Random().nextLong(2000)) ;
return ResponseEntity.ok("api query...") ;
}在需要監(jiān)控的接口上添加 @Timed 注解,同時(shí)你還需要進(jìn)行如下的配置:
management:
observations:
annotations:
enabled: true要結(jié)合Prometheus,那么我們還需要引入如下依賴
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
配置actuator暴露Prometheus端點(diǎn)
management:
endpoints:
web:
exposure:
include: '*'
base-path: /ac最后,我們還需要在Prometheus中進(jìn)行配置
- job_name: "testtag"
metrics_path: "/ac/prometheus"
static_configs:
- targets: ["localhost:8080"]總結(jié):非侵入、集成簡(jiǎn)單,支持細(xì)粒度方法級(jí)監(jiān)控,與Spring Boot天然兼容,數(shù)據(jù)可持久化并可視化;需引入監(jiān)控組件,增加系統(tǒng)復(fù)雜度。
2.7 使用Arthas
Arthas 是一款線上監(jiān)控診斷產(chǎn)品,通過(guò)全局視角實(shí)時(shí)查看應(yīng)用 load、內(nèi)存、gc、線程的狀態(tài)信息,并能在不修改應(yīng)用代碼的情況下,對(duì)業(yè)務(wù)問(wèn)題進(jìn)行診斷,包括查看方法調(diào)用的出入?yún)?、異常,監(jiān)測(cè)方法執(zhí)行耗時(shí),類加載信息等,大大提升線上問(wèn)題排查效率。
首先,在如下地址下載最新的Arthas
接下來(lái),通過(guò)如下命令啟動(dòng)Arthas
java -jar arthas-boot.jar
通過(guò)前面的數(shù)字選擇哪個(gè)進(jìn)程。
連接到具體的進(jìn)程后,我們可以通過(guò)如下的命令來(lái)跟蹤記錄方法執(zhí)行耗時(shí)情況
trace com.pack.timed.controller.ApiController query
2.8 使用SkyWalking
SkyWalking 是一個(gè)開(kāi)源的 APM(應(yīng)用性能監(jiān)控)系統(tǒng),支持分布式鏈路追蹤、服務(wù)網(wǎng)格觀測(cè)、度量聚合與可視化,適用于微服務(wù)、云原生和 Service Mesh 架構(gòu),幫助開(kāi)發(fā)者實(shí)現(xiàn)全棧性能監(jiān)控與故障診斷。
我們無(wú)需在代碼中寫任何代碼或是引入依賴,因?yàn)镾kyWalking將使用agent技術(shù)進(jìn)行跟蹤系統(tǒng)。
首先,我們需要在如下地址下載SkyWalking已經(jīng)java對(duì)應(yīng)的agent。
Web UI 運(yùn)行在8080端口
最后,我們?cè)谶\(yùn)行程序時(shí),需要加入如下JVM參數(shù)。
-javaagent:D:\all\opensource\skywalking\skywalking-agent\skywalking-agent.jar -Dskywalking.agent.service_name=pack-api 1.2.
總結(jié):SkyWalking 自動(dòng)完成鏈路追蹤與耗時(shí)監(jiān)控,支持分布式系統(tǒng),無(wú)需修改代碼,可視化強(qiáng),性能影響小。在分布式系統(tǒng)中非常推薦。
到此這篇關(guān)于SpringBoot監(jiān)控API請(qǐng)求耗時(shí)的6中解決解決方案的文章就介紹到這了,更多相關(guān)SpringBoot監(jiān)控API請(qǐng)求耗時(shí)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java程序啟動(dòng)時(shí)初始化數(shù)據(jù)的四種方式
本文主要介紹了Java程序啟動(dòng)時(shí)初始化數(shù)據(jù)的四種方式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-02-02
java實(shí)現(xiàn)mongodb的數(shù)據(jù)庫(kù)連接池
這篇文章主要介紹了基于java實(shí)現(xiàn)mongodb的數(shù)據(jù)庫(kù)連接池,Java通過(guò)使用mongo-2.7.3.jar包實(shí)現(xiàn)mongodb連接池,感興趣的小伙伴們可以參考一下2015-12-12
IDEA代碼規(guī)范&質(zhì)量檢查的實(shí)現(xiàn)
這篇文章主要介紹了IDEA代碼規(guī)范&質(zhì)量檢查的實(shí)現(xiàn),文中通過(guò)圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
使用JVMTI實(shí)現(xiàn)SpringBoot的jar加密,防止反編譯
這篇文章主要介紹了使用JVMTI實(shí)現(xiàn)SpringBoot的jar加密,防止反編譯問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08
關(guān)于mybatis-plus-generator的簡(jiǎn)單使用示例詳解
在springboot項(xiàng)目中集成mybatis-plus是很方便開(kāi)發(fā)的,最近看了一下plus的文檔,簡(jiǎn)單用一下它的代碼生成器,接下來(lái)通過(guò)實(shí)例代碼講解關(guān)于mybatis-plus-generator的簡(jiǎn)單使用,感興趣的朋友跟隨小編一起看看吧2024-03-03
詳細(xì)了解java監(jiān)聽(tīng)器和過(guò)濾器
下面小編就為大家?guī)?lái)一篇基于java servlet過(guò)濾器和監(jiān)聽(tīng)器(詳解)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2021-07-07
使用Java校驗(yàn)SQL語(yǔ)句的合法性五種解決方案
這篇文章主要介紹了如何用java校驗(yàn)SQL語(yǔ)句的合法性(提供五種解決方案),使用JDBC?API和JSqlParser庫(kù)、正則表達(dá)式、ANTLR解析器生成器或Apache?Calcite庫(kù)都可以實(shí)現(xiàn)校驗(yàn)SQL語(yǔ)句的合法性,需要的朋友可以參考下2023-04-04
使用Java打印數(shù)字組成的魔方陣及字符組成的鉆石圖形
這篇文章主要介紹了使用Java打印數(shù)字組成的魔方陣及字符組成的鉆石圖形,可作為一些CLI程序界面的基礎(chǔ)部分,需要的朋友可以參考下2016-03-03
springboot項(xiàng)目之相互依賴報(bào)錯(cuò)問(wèn)題(基于idea)
這篇文章主要介紹了springboot項(xiàng)目之相互依賴報(bào)錯(cuò)問(wèn)題(基于idea),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02

