Java 中多种计算耗时方式详解

HYF Lv4

引言

在日常的 Java 开发过程中,性能问题往往是影响用户体验与系统稳定性的关键因素之一。无论是在调试某段逻辑执行是否过慢,还是在优化某个热点方法,我们都离不开一个基本动作——统计代码的执行耗时。

Java 提供了多种方式来实现耗时统计,从最原始的 System.currentTimeMillis()System.nanoTime(),到更现代化的 StopWatch 工具类(Spring 工具类的 Stopwatch 计时的简单使用 ),再到通过 AOP 切面、监听器进行自动埋点监控,每种方式各有优劣,适用于不同的场景,本篇文章将系统地整理并实战演示这些主流的耗时统计方法。

使用 StopWatch 统计方法耗时

topWatchSpring 框架提供的一个轻量级工具类,主要用于开发过程中对方法执行时间的监控和分析。它支持多个任务的耗时记录,并能输出结构化的统计信息,非常适合在调试阶段快速定位性能瓶颈.

示例代码

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% 任务二 - 模拟业务逻辑计算

优缺点:

使用简单,API 清晰易懂

支持多任务时间分段统计

提供结构化输出,方便日志记录和分析

可无需引入外部依赖(只要已有 Spring)

仅适用于开发调试,非线程安全

不适用于高并发场景下的共享使用

需要手动在代码中植入统计逻辑,侵入性较高

使用 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
// @LogExecutionTime(name = "模拟业务逻辑")
// @LogExecutionTime(timeUnit = ExecutionTimeUnit.MILLISECONDS)
// @LogExecutionTime(name = "模拟业务逻辑", timeUnit = ExecutionTimeUnit.MILLISECONDS)
@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

优缺点

灵活控制:只统计标注了注解的方法

非侵入式:不修改原有逻辑结构

可扩展性强:注解还能携带参数实现更多定制化行为

适合精确监控核心接口或特定业务逻辑

相比通用 AOP 多了注解使用的工作量

对非 Spring 管理的类/方法无效

需要开发和团队明确使用规范

使用拦截器实现请求耗时统计

在基于 Spring MVCWeb 项目中,拦截器(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("/**");
}
}

优缺点

非侵入式,无需修改 Controller 代码

可统一统计所有或部分路径的请求耗时

可访问请求路径、方法等上下文信息,便于日志记录

可根据路径、方法名、用户等信息进行过滤、告警等扩展

无法统计 Controller 内部方法调用耗时

粒度为整个请求,不适用于方法级别性能分析

需要 Web 项目(Spring MVC 环境)

使用 Filter 实现请求耗时统计

FilterJava 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);
}
}

优缺点

覆盖范围广,可监控所有请求(包括静态资源、错误页等)

与框架无关,适用于所有 Servlet 容器项目

执行顺序早于拦截器,可记录更完整的处理耗时

可灵活结合日志、Tracing 等系统进行链路耗时分析

粒度较粗,无法细化到方法级别

实现上略显底层,不如 AOP 灵活

若不筛选路径,可能输出无关资源(如 .js, .css, .png)的日志

使用 ServletRequestHandledEvent 实现请求耗时统计

ServletRequestHandledEventSpring 框架提供的一个内建事件,专用于监听每个 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
# dev 环境
spring:
profiles:
active: dev

# prod 环境
spring:
profiles:
active: prod

继续使用 @ConditionalOnProperty,但结合 profile 控制配置项

配置文件

1
2
3
4
5
6
7
8
9
# application-dev.yml
request:
timing:
enabled: true

# application-prod.yml
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 是一个好选择

在性能调优与问题排查的工作中,耗时统计是不可或缺的一环。建议结合实际项目架构、运行环境以及监控平台综合考虑,开发阶段方便调试,生产环境则尽量非侵入、可控、低成本地进行埋点和分析。

  • 标题: Java 中多种计算耗时方式详解
  • 作者: HYF
  • 创建于 : 2025-06-19 22:02:39
  • 更新于 : 2025-06-19 23:55:16
  • 链接: https://youfeng.ink/statistics-time-consumption-fc7abd24613f/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。