Spring StopWatch 源码解析:从秒表状态机到 SpringBoot 启动计时实战
Spring StopWatch 源码解析从秒表状态机到 SpringBoot 启动计时实战【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunterSpring 框架的org.springframework.util.StopWatch是一个轻量级的秒表工具类用于精确统计一个或多个任务Task的执行耗时。本指南基于 Spring-StopWatch.md 展开源码级剖析并结合仓库中 SpringBoot run 方法解析 展示其在 SpringBoot 启动流程中的真实落地场景。读完本文你将掌握 StopWatch 的核心属性、start/stop 状态机逻辑、任务列表与总耗时统计原理并能在自己的项目中正确使用这套计时工具。StopWatch 解决了什么问题在日常开发中统计代码执行耗时是最常见的需求之一。最朴素的做法是记录System.currentTimeMillis()的差值long start System.currentTimeMillis(); // 业务逻辑 long cost System.currentTimeMillis() - start;但当我们需要同时统计多个任务、累加总耗时、并输出结构化明细时裸用时间戳就显得零散且易错。Spring 的StopWatch将这套逻辑封装为一个独立的工具类其类全路径为org.springframework.util.StopWatch以单个实例管理多个任务的计时并对外暴露总耗时、任务数量、最近任务信息等状态。核心属性解析秒表的内部状态StopWatch 之所以能完成启动 → 停止 → 汇总的完整闭环靠的是以下几个内部字段见 Spring-StopWatch.md属性类型作用taskListListTaskInfo任务信息列表保存每个已完成任务的名称与耗时keepTaskListboolean是否保留任务信息列表默认为true置为false可关闭任务明细记录以省内存startTimeMillislong当前任务开始的时间戳毫秒currentTaskNameString当前正在运行的任务名称为null表示秒表处于停止状态lastTaskInfoTaskInfo最近一次停止的任务信息taskCountint已完成任务的总数量totalTimeMillislong所有任务总共花费的时间毫秒在stop()时累加其中TaskInfo是 StopWatch 的内部嵌套类持有某个任务的taskName与timeMillis是taskList的元素类型也是getLastTaskInfo()的返回值。值得注意的设计点是秒表用currentTaskName是否为null来标识是否正在运行而不是单独维护一个布尔标志。这一约定贯穿start与stop两个核心方法的校验逻辑。核心方法解析start / stop 的状态机StopWatch 的使用模型是一个严格的状态机start()与stop()必须成对出现、交替执行任何不合法调用都会抛出IllegalStateException。start(String taskName)启动一个任务源码如下摘自 Spring-StopWatch.mdpublic void start(String taskName) throws IllegalStateException { if (this.currentTaskName ! null) { throw new IllegalStateException(Cant start StopWatch: its already running); } this.currentTaskName taskName; this.startTimeMillis System.currentTimeMillis(); }关键点幂等性保护如果currentTaskName不为null说明已有任务在运行直接抛出IllegalStateException防止未stop就再次start导致的计时错乱。记录起点将任务名写入currentTaskName并将当前系统时间戳记入startTimeMillis作为该任务的计时起点。该方法还提供一个无参重载start()内部委托给start()用于不关心任务名的场景。stop()停止任务并汇总耗时public void stop() throws IllegalStateException { if (this.currentTaskName null) { throw new IllegalStateException(Cant stop StopWatch: its not running); } // 消费的时间 long lastTime System.currentTimeMillis() - this.startTimeMillis; this.totalTimeMillis lastTime; // 任务信息初始化 this.lastTaskInfo new TaskInfo(this.currentTaskName, lastTime); if (this.keepTaskList) { this.taskList.add(this.lastTaskInfo); } this.taskCount; this.currentTaskName null; }关键点非法调用拦截若currentTaskName为null秒表不在运行状态抛出IllegalStateException。耗时计算lastTime System.currentTimeMillis() - this.startTimeMillis即本次任务从start到stop的毫秒差。总耗时累加totalTimeMillis lastTime多个任务的时间在此累计。任务信息归档将(taskName, lastTime)封装为TaskInfo赋给lastTaskInfo当keepTaskList为true时追加进taskList。这解释了keepTaskListfalse时为何仍能通过getLastTaskInfo()拿到最近一次任务耗时——最近一次任务信息不依赖任务列表永远被记录。任务计数自增taskCount供getTaskCount()使用。状态复位currentTaskName置回null秒表回到可再次start的状态。从源码结构看这一设计实现了单个实例连续计时多个任务start → stop → start → stop循环每次stop都会把耗时累积进totalTimeMillis最终通过getTotalTimeMillis()拿到所有任务的累计耗时。完整方法家族与查询入口除上述两个核心方法外StopWatch 还提供了一系列查询与格式化方法基于该类在 Spring 框架中的标准实现归纳isRunning()返回currentTaskName ! null判断秒表是否正在运行getLastTaskInfo()/getLastTaskName()/getLastTaskTimeMillis()获取最近一次任务的完整信息、名称、耗时getTaskCount()返回已完成任务数量即taskCountgetTotalTimeMillis()/getTotalTimeSeconds()分别以毫秒、秒为单位返回总耗时getTaskInfo()返回taskList.toArray(new TaskInfo[0])即任务明细数组getTotalTimeString()将总耗时格式化为带单位的字符串如1.234sprettyPrint()以表格形式输出每个任务的名称、耗时、占总耗时百分比并附总耗时行shortSummary()输出任务总数 总耗时的简短摘要toString()内部调用prettyPrint()返回完整的多行描述setKeepTaskList(boolean)动态开关任务列表记录配合构造器StopWatch(String id)/StopWatch(boolean keepTaskList)使用getId()返回秒表标识构造时可传入便于区分多个秒表实例。其中prettyPrint()输出的百分比计算以totalTimeMillis为分母能直观看出哪个任务最耗时是定位性能瓶颈的利器。下面给出一个典型的连续计时示例StopWatch stopWatch new StopWatch(示例任务); stopWatch.start(任务A); // 执行任务A的业务逻辑 stopWatch.stop(); stopWatch.start(任务B); // 执行任务B的业务逻辑 stopWatch.stop(); System.out.println(stopWatch.prettyPrint());输出会以表格形式列出任务 A、任务 B 各自耗时及占比以及总计耗时无需手工拼接日志。仓库实战StopWatch 支撑 SpringBoot 启动耗时统计StopWatch 并非只在工具层面存在在仓库的 SpringBoot run 方法解析 中可以看到它被用于统计整个 SpringBoot 应用启动过程的耗时这是该工具类最经典的实战场景之一。SpringApplication.run(String... args)方法对应 SpringBoot 源码中的org.springframework.boot.SpringApplication#run在启动伊始便创建秒表public ConfigurableApplicationContext run(String... args) { // 秒表 StopWatch stopWatch new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context null; ... // 获取监听器 SpringApplicationRunListeners listeners getRunListeners(args); // 监听器启动 listeners.starting(); try { // application 启动参数列表 ApplicationArguments applicationArguments new DefaultApplicationArguments(args); ConfigurableEnvironment environment prepareEnvironment(listeners, applicationArguments); ... // 创建应用上下文 context createApplicationContext(); ... // 准备上下文装配bean prepareContext(context, environment, listeners, applicationArguments, printedBanner); // 上下文刷新 refreshContext(context); // 刷新后做什么 afterRefresh(context, applicationArguments); stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } // 监听器开始了 listeners.started(context); // 唤醒 callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } ... }从这段源码可以梳理出 StopWatch 的完整生命周期new StopWatch()创建秒表实例keepTaskList采用默认值truestopWatch.start()在configureHeadlessProperty()等环境准备之前启动计时记录起点stopWatch.stop()在refreshContext(context)即 IoC 容器刷新、Bean 装配完成和afterRefresh之后停止计时new StartupInfoLogger(...).logStarted(getApplicationLog(), stopWatch)将秒表对象交给日志组件最终输出Started Application in X.XXX seconds这类经典启动日志其中X.XXX seconds正是来自getTotalTimeSeconds()之类的查询方法。由此可见SpringBoot 每次启动时控制台打印的启动耗时其数据源头就是org.springframework.util.StopWatch。这也解释了为什么stopWatch.stop()必须放在 try 块内刷新完成之后、listeners.started之前——它精确覆盖了应用从启动到就绪这一完整区间。使用注意事项与最佳实践结合上面的源码逻辑在使用 StopWatch 时有几点值得注意必须成对调用start与stop交替执行重复start或重复stop都会触发IllegalStateException异常场景下应使用try-finally包裹防止秒表停留在运行态导致后续计时错乱。毫秒级精度局限底层依赖System.currentTimeMillis()适合宏观耗时统计毫秒级若需纳秒级精确测量应改用System.nanoTime()等方案这属于工具类的固有精度边界。keepTaskListfalse的场景当任务数量极大、不需要逐项明细时可关闭任务列表以降低内存占用但lastTaskInfo、totalTimeMillis、taskCount依然正常维护getLastTaskInfo()不受影响。借助prettyPrint()输出结构化结果相比手工拼接日志prettyPrint()自带表格与百分比便于直接写入日志或监控系统。小结org.springframework.util.StopWatch用极简的状态机currentTaskName空与非空 任务列表实现了多任务计时的完整闭环start记录起点stop计算耗时并累加总耗时taskList/lastTaskInfo/taskCount沉淀结构化统计信息。它在仓库中的应用——SpringBoot 启动耗时统计——则展示了这个工具类在生产级框架中的真实价值一行new StopWatch()一段start()/stop()即可为整个应用的生命周期提供可读、可查的耗时数据。当你需要为模块、批处理或微服务链路添加耗时统计时StopWatch 是一个开箱即用的选择本文对应的完整源码注释可继续在 Spring-StopWatch.md 中查阅。【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考