引言 在日常的 Java
开发过程中,性能问题往往是影响用户体验与系统稳定性的关键因素之一。无论是在调试某段逻辑执行是否过慢,还是在优化某个热点方法,我们都离不开一个基本动作——统计代码的执行耗时。
Java
提供了多种方式来实现耗时统计,从最原始的 System.currentTimeMillis()
、System.nanoTime()
,到更现代化的 StopWatch
工具类(Spring 工具类的 Stopwatch 计时的简单使用 ),再到通过 AOP
切面、监听器进行自动埋点监控,每种方式各有优劣,适用于不同的场景,本篇文章将系统地整理并实战演示这些主流的耗时统计方法。
使用 StopWatch 统计方法耗时 topWatch
是 Spring
框架提供的一个轻量级工具类,主要用于开发过程中对方法执行时间的监控和分析。它支持多个任务的耗时记录,并能输出结构化的统计信息,非常适合在调试阶段快速定位性能瓶颈.
示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import org.springframework.util.StopWatch;public class StopWatchExample { public static void main (String[] args) throws InterruptedException { StopWatch stopWatch = new StopWatch ("性能统计" ); stopWatch.start("任务一 - 模拟数据库查询" ); Thread.sleep(300 ); stopWatch.stop(); stopWatch.start("任务二 - 模拟业务逻辑计算" ); Thread.sleep(500 ); stopWatch.stop(); System.out.println(stopWatch.prettyPrint()); System.out.println("总耗时: " + stopWatch.getTotalTimeMillis() + " ms" ); } }
输出示例 1 2 3 4 5 6 StopWatch '性能统计': running time (millis) = 801 ----------------------------------------- ms % Task name ----------------------------------------- 00300 037% 任务一 - 模拟数据库查询 00501 063% 任务二 - 模拟业务逻辑计算
优缺点: 使用 AOP 实现全局方法耗时统计 AOP
(面向切面编程)是 Spring
的核心特性之一,允许你将横切关注点(如日志、事务、安全、耗时统计等)从业务逻辑中分离出来。借助 AOP
,我们可以对指定包下的所有方法进行统一的耗时统计,实现无侵入式的全局监控。
示例代码: 添加 AOP 依赖(Spring Boot 项目无需手动添加) 如果你是普通 Spring 项目,可添加以下依赖:
1 2 3 4 5 <!-- AOP 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
定义耗时统计切面类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Log4j2 @Aspect @Component public class ExecutionTimeAspect { @Around("execution(* com.youfeng.*.service..*(..))") public Object logExecutionTime (ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long duration = System.currentTimeMillis() - start; String methodName = joinPoint.getSignature().toShortString(); log.info("请求方法: {}, 处理耗时: {} 毫秒" , methodName, duration); return result; } }
业务代码模拟 1 2 3 4 5 6 7 @Service public class UserService { public void simulateBusinessLogic () throws InterruptedException { Thread.sleep(200 ); } }
调用此方法时控制台会输出类似:
1 INFO 37233 --- [nio-8088-exec-2] c.j.c.b.ExecutionTimeAspect : 请求方法: UserService.simulateBusinessLogic(), 处理耗时: 201 毫秒
优缺点 需要 Spring AOP 支持(或 AspectJ)
不支持非 Spring Bean 的方法(例如静态工具类)
使用自定义注解 + AOP 实现更精细的控制 在某些场景下,我们不希望对整个包下所有方法都做耗时统计,而是希望仅对特定方法进行监控。通过自定义注解配合 AOP
,我们可以实现更细粒度的控制,只有标注了该注解的方法才会被统计耗时,实现更灵活、更安全的性能监控方案。
示例代码 定义时间单位枚举类 1 2 3 public enum ExecutionTimeUnit { MILLISECONDS, NANOSECONDS }
定义注解类 1 2 3 4 5 6 7 8 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LogExecutionTime { String name () default "" ; ExecutionTimeUnit timeUnit () default ExecutionTimeUnit.MILLISECONDS; }
定义 AOP 切面逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Log4j2 @Aspect @Component public class LogExecutionTimeAspect { @Pointcut("@annotation(com.youfeng.aop.executionTime.annotation.LogExecutionTime)") public void pointCut () { } @Around("pointCut()") public Object logExecutionTime (ProceedingJoinPoint joinPoint) throws Throwable { StopWatch stopWatch = new StopWatch (); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); LogExecutionTime logExecutionTime = method.getAnnotation(LogExecutionTime.class); String taskName = logExecutionTime.name().isEmpty() ? joinPoint.getSignature().toShortString() : logExecutionTime.name(); stopWatch.start(taskName); Object result = joinPoint.proceed(); stopWatch.stop(); long duration; if (ExecutionTimeUnit.MILLISECONDS == logExecutionTime.timeUnit()) { duration = stopWatch.getLastTaskTimeMillis(); } else { duration = stopWatch.getLastTaskTimeNanos(); } log.info("请求方法: {}, 处理耗时:{}, 时间单位:{}" , taskName, duration, logExecutionTime.timeUnit()); return result; } }
应用于业务方法 1 2 3 4 5 6 7 8 9 @LogExecutionTime(name = "模拟业务逻辑", timeUnit = ExecutionTimeUnit.NANOSECONDS) public void simulateBusinessLogic () throws InterruptedException { Thread.sleep(200 ); }
调用该方法时时,控制台输出
1 INFO 40397 --- [nio-8088-exec-2] c.j.a.e.aspect.LogExecutionTimeAspect : 请求方法: 模拟业务逻辑, 处理耗时:208599750, 时间单位:NANOSECONDS
优缺点 使用拦截器实现请求耗时统计 在基于 Spring MVC
的 Web
项目中,拦截器(HandlerInterceptor
)是一种常用机制,允许在请求处理流程中对请求进行前置处理、后置处理、完成处理。我们可以借助拦截器在请求进入 Controller
前记录开始时间,在处理完成后统计整个请求的耗时,实现对请求级别的性能监控。
示例代码 实现类拦截器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Log4j2 @Component public class RequestTimeInterceptor implements HandlerInterceptor { private static final String START_TIME = "startTime" ; @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) { request.setAttribute(START_TIME, System.currentTimeMillis()); return true ; } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { Long startTime = (Long) request.getAttribute(START_TIME); if (startTime != null ) { long duration = System.currentTimeMillis() - startTime; String uri = request.getRequestURI(); log.info("请求路径: {},处理耗时: {} 毫秒" , uri, duration); } } }
注册拦截器(Spring Boot 项目) 1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private RequestTimeInterceptor requestTimeInterceptor; @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(requestTimeInterceptor) .addPathPatterns("/**" ); } }
优缺点 可根据路径、方法名、用户等信息进行过滤、告警等扩展
使用 Filter 实现请求耗时统计 Filter
是 Java Servlet
规范定义的组件,属于最底层的 Web
请求拦截机制。它在请求到达 Spring MVC
(或任何 Web
框架)之前就可以进行处理,也可以在响应返回前执行清理或记录逻辑。因此,使用 Filter
实现耗时统计可以覆盖整个请求生命周期,包括过滤器链、Spring DispatcherServlet
、控制器、异常处理等。
示例代码 实现 Filter 类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Log4j2 @Component public class RequestTimeFilter implements Filter { @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { long start = System.currentTimeMillis(); chain.doFilter(request, response); long duration = System.currentTimeMillis() - start; String uri = ((HttpServletRequest) request).getRequestURI(); log.info("请求路径: {},处理耗时: {} 毫秒" , uri, duration); } }
优缺点 覆盖范围广,可监控所有请求(包括静态资源、错误页等)
可灵活结合日志、Tracing 等系统进行链路耗时分析
若不筛选路径,可能输出无关资源(如 .js, .css, .png)的日志
使用 ServletRequestHandledEvent 实现请求耗时统计 ServletRequestHandledEvent
是 Spring
框架提供的一个内建事件,专用于监听每个 HTTP
请求处理的完成情况。Spring MVC
会在请求处理完成(即 Controller
执行完毕,包括异常情况)后自动发布该事件,我们可以通过监听它来记录每个请求的耗时和异常信息。
相比拦截器与 Filter
,这种方式的优势在于与 Spring
的事件机制集成良好,非侵入、解耦,非常适合用于日志采集、慢接口告警等场景。
示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Log4j2 @Component @ConditionalOnProperty(name = "request.timing.enabled", havingValue = "true") public class RequestTimingEventListener implements ApplicationListener <ServletRequestHandledEvent> { @Override public void onApplicationEvent (ServletRequestHandledEvent event) { Throwable failureCause = event.getFailureCause(); String failureMessage = ObjectUtils.isEmpty(failureCause) ? StringPool.EMPTY : failureCause.getMessage(); String clientAddress = event.getClientAddress(); String requestUrl = event.getRequestUrl(); String method = event.getMethod(); long processingTimeMillis = event.getProcessingTimeMillis(); if (ObjectUtils.isEmpty(failureCause)) { log.info("客户端地址: {},请求路径: {},请求方法: {},处理耗时: {} 毫秒" , clientAddress, requestUrl, method, processingTimeMillis); } else { log.error("客户端地址: {},请求路径: {},请求方法: {},处理耗时: {} 毫秒,错误信息: {}" , clientAddress, requestUrl, method, processingTimeMillis, failureMessage); } } }
配置示例(application-dev.yml)可以通过配置开关来在不同环境(如仅开发或测试环境)启用该监听器,避免对生产性能产生影响
1 2 3 4 5 6 7 ... request: timing: enabled: true ...
扩展:监听器开启方式调整 可以用 @Profile 限制监听器仅在开发环境启用: 1 2 3 4 5 6 7 8 9 @Log4j2 @Component @Profile("dev") public class RequestTimingEventListener implements ApplicationListener <ServletRequestHandledEvent> { @Override public void onApplicationEvent (ServletRequestHandledEvent event) { ... } }
配置环境:
1 2 3 4 5 6 7 8 9 spring: profiles: active: dev spring: profiles: active: prod
继续使用 @ConditionalOnProperty,但结合 profile 控制配置项 配置文件
1 2 3 4 5 6 7 8 9 request: timing: enabled: true request: timing: enabled: false
继续使用原有的注解:
1 2 3 4 5 6 7 8 9 @Log4j2 @Component @ConditionalOnProperty(name = "request.timing.enabled", havingValue = "true") public class RequestTimingEventListener implements ApplicationListener <ServletRequestHandledEvent> { @Override public void onApplicationEvent (ServletRequestHandledEvent event) { ... } }
通过该方法可以在使用 Nacos 作为配置中心时,在线切换监听器启用状态
总结 在 Java
开发中,准确地统计代码或请求的执行耗时,是优化系统性能、排查瓶颈的基础手段。本文系统地介绍了多种常见的耗时统计方式,涵盖了从代码片段级别到请求生命周期级别的不同方案:
方法 粒度 是否侵入 适用场景 特点 StopWatch
(Spring 工具类)方法级 ✅ 开发调试阶段,任务分段分析 支持多个任务、输出清晰 AOP 全局耗时统计 方法级 ❌ 对 Service/Controller 做统一监控 非侵入、配置灵活 注解 + AOP 方法级 ❌ 精细控制关键方法的耗时记录 灵活性强、可扩展 拦截器(HandlerInterceptor
) 请求级 ❌ Spring MVC 请求整体耗时 使用简单,控制路径灵活 过滤器(Filter
) 请求级 ❌ Servlet 层全局请求监控 可监控所有请求 ServletRequestHandledEvent
请求级 ❌ 请求结束时统一记录耗时与异常 与 Spring 事件机制集成良好
每种方法各有优劣,选择合适的方式,取决于你的目标:
如果你只是临时调试某段逻辑 —— 用 StopWatch 最方便
想对某一类方法做统一日志 —— 推荐使用 AOP 切面
想精细标记哪些方法记录耗时 —— 加上注解配合 AOP
需要全局请求监控 —— 拦截器或 Filter 更合适
想最解耦地记录请求耗时 —— ServletRequestHandledEvent 是一个好选择
在性能调优与问题排查的工作中,耗时统计是不可或缺的一环。建议结合实际项目架构、运行环境以及监控平台综合考虑,开发阶段方便调试,生产环境则尽量非侵入、可控、低成本地进行埋点和分析。