diff --git a/CHANGELOG.md b/CHANGELOG.md
index f32c13d80..1dc9af17c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
### 新特性
* 【core】 MapUtil增加newConcurrentHashMap(pr#538@Github)
+* 【core】 增加StopWatch(issuepr#539@Github)
### Bug修复
* 【core】 修复DateUtil.endOfYear计算错误问题(issuepr#540@Github)
diff --git a/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java b/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java
index 8e0ab66d9..65c11ed0e 100644
--- a/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java
+++ b/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java
@@ -8,6 +8,7 @@ import java.util.GregorianCalendar;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
+import java.util.concurrent.TimeUnit;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.comparator.CompareUtil;
@@ -1859,6 +1860,28 @@ public class DateUtil {
public static int compare(Calendar calendar1, Calendar calendar2) {
return CompareUtil.compare(calendar1, calendar2);
}
+
+ /**
+ * 纳秒转毫秒
+ *
+ * @param duration 时长
+ * @return 时长毫秒
+ * @since 4.6.6
+ */
+ public static long nanosToMillis(long duration) {
+ return TimeUnit.NANOSECONDS.toMillis(duration);
+ }
+
+ /**
+ * 纳秒转秒,保留小数
+ *
+ * @param duration 时长
+ * @return 秒
+ * @since 4.6.6
+ */
+ public static double nanosToSeconds(long duration) {
+ return duration / 1_000_000_000.0;
+ }
// ------------------------------------------------------------------------ Private method start
/**
diff --git a/hutool-core/src/main/java/cn/hutool/core/date/StopWatch.java b/hutool-core/src/main/java/cn/hutool/core/date/StopWatch.java
new file mode 100755
index 000000000..26bb40eaf
--- /dev/null
+++ b/hutool-core/src/main/java/cn/hutool/core/date/StopWatch.java
@@ -0,0 +1,396 @@
+package cn.hutool.core.date;
+
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.StrUtil;
+
+/**
+ * 秒表封装
+ * 此工具用于存储一组任务的耗时时间,并一次性打印对比。
+ * 比如:我们可以记录多段代码耗时时间,然后一次性打印(StopWatch提供了一个prettyString()函数用于按照指定格式打印出耗时)
+ *
+ *
+ * 此工具来自:https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/util/StopWatch.java
+ *
+ *
+ * 使用方法如下:
+ *
+ *
+ * StopWatch stopWatch = new StopWatch("任务名称");
+ *
+ * // 任务1
+ * stopWatch.start("任务一");
+ * Thread.sleep(1000);
+ * stopWatch.stop();
+ *
+ * // 任务2
+ * stopWatch.start("任务一");
+ * Thread.sleep(2000);
+ * stopWatch.stop();
+ *
+ * // 打印出耗时
+ * Console.log(stopWatch.prettyPrint());
+ *
+ *
+ *
+ * @author Spring Framework, Looly
+ * @since 4.6.6
+ */
+public class StopWatch {
+
+ /** 秒表唯一标识,用于多个秒表对象的区分 */
+ private final String id;
+ private List taskList;
+
+ /** 任务名称 */
+ private String currentTaskName;
+ /** 开始时间 */
+ private long startTimeNanos;
+
+ /** 最后一次任务对象 */
+ private TaskInfo lastTaskInfo;
+ /** 总任务数 */
+ private int taskCount;
+ /** 总运行时间 */
+ private long totalTimeNanos;
+
+ // ------------------------------------------------------------------------------------------- Constructor start
+ /**
+ * 构造,不启动任何任务
+ */
+ public StopWatch() {
+ this(StrUtil.EMPTY);
+ }
+
+ /**
+ * 构造,不启动任何任务
+ *
+ * @param id 用于标识秒表的唯一ID
+ */
+ public StopWatch(String id) {
+ this(id, true);
+ }
+
+ /**
+ * 构造,不启动任何任务
+ *
+ * @param id 用于标识秒表的唯一ID
+ * @param keepTaskList 是否在停止后保留任务,{@code false} 表示停止运行后不保留任务
+ */
+ public StopWatch(String id, boolean keepTaskList) {
+ this.id = id;
+ if (keepTaskList) {
+ this.taskList = new ArrayList<>();
+ }
+ }
+ // ------------------------------------------------------------------------------------------- Constructor end
+
+ /**
+ * 获取{@link StopWatch} 的ID,用于多个秒表对象的区分
+ *
+ * @return the ID 空字符串为
+ * @see #StopWatch(String)
+ */
+ public String getId() {
+ return this.id;
+ }
+
+ /**
+ * 设置是否在停止后保留任务,{@code false} 表示停止运行后不保留任务
+ *
+ * @param keepTaskList 是否在停止后保留任务
+ */
+ public void setKeepTaskList(boolean keepTaskList) {
+ if (keepTaskList) {
+ if (null == this.taskList) {
+ this.taskList = new ArrayList<>();
+ }
+ } else {
+ this.taskList = null;
+ }
+ }
+
+ /**
+ * 开始默认的新任务
+ *
+ * @throws IllegalStateException 前一个任务没有结束
+ */
+ public void start() throws IllegalStateException {
+ start(StrUtil.EMPTY);
+ }
+
+ /**
+ * 开始指定名称的新任务
+ *
+ * @param taskName 新开始的任务名称
+ * @throws IllegalStateException 前一个任务没有结束
+ */
+ public void start(String taskName) throws IllegalStateException {
+ if (null != this.currentTaskName) {
+ throw new IllegalStateException("Can't start StopWatch: it's already running");
+ }
+ this.currentTaskName = taskName;
+ this.startTimeNanos = System.nanoTime();
+ }
+
+ /**
+ * 停止当前任务
+ *
+ * @throws IllegalStateException 任务没有开始
+ */
+ public void stop() throws IllegalStateException {
+ if (null == this.currentTaskName) {
+ throw new IllegalStateException("Can't stop StopWatch: it's not running");
+ }
+
+ final long lastTime = System.nanoTime() - this.startTimeNanos;
+ this.totalTimeNanos += lastTime;
+ this.lastTaskInfo = new TaskInfo(this.currentTaskName, lastTime);
+ if (null != this.taskList) {
+ this.taskList.add(this.lastTaskInfo);
+ }
+ ++this.taskCount;
+ this.currentTaskName = null;
+ }
+
+ /**
+ * 检查是否有正在运行的任务
+ *
+ * @return 是否有正在运行的任务
+ * @see #currentTaskName()
+ */
+ public boolean isRunning() {
+ return (this.currentTaskName != null);
+ }
+
+ /**
+ * 获取当前任务名,{@code null} 表示无任务
+ *
+ * @return 当前任务名,{@code null} 表示无任务
+ * @see #isRunning()
+ */
+ public String currentTaskName() {
+ return this.currentTaskName;
+ }
+
+ /**
+ * 获取最后任务的花费时间(纳秒)
+ *
+ * @return 任务的花费时间(纳秒)
+ * @throws IllegalStateException 无任务
+ */
+ public long getLastTaskTimeNanos() throws IllegalStateException {
+ if (this.lastTaskInfo == null) {
+ throw new IllegalStateException("No tasks run: can't get last task interval");
+ }
+ return this.lastTaskInfo.getTimeNanos();
+ }
+
+ /**
+ * 获取最后任务的花费时间(毫秒)
+ *
+ * @return 任务的花费时间(毫秒)
+ * @throws IllegalStateException 无任务
+ */
+ public long getLastTaskTimeMillis() throws IllegalStateException {
+ if (this.lastTaskInfo == null) {
+ throw new IllegalStateException("No tasks run: can't get last task interval");
+ }
+ return this.lastTaskInfo.getTimeMillis();
+ }
+
+ /**
+ * 获取最后的任务名
+ *
+ * @return 任务名
+ * @throws IllegalStateException 无任务
+ */
+ public String getLastTaskName() throws IllegalStateException {
+ if (this.lastTaskInfo == null) {
+ throw new IllegalStateException("No tasks run: can't get last task name");
+ }
+ return this.lastTaskInfo.getTaskName();
+ }
+
+ /**
+ * 获取最后的任务对象
+ *
+ * @return {@link TaskInfo} 任务对象,包括任务名和花费时间
+ * @throws IllegalStateException 无任务
+ */
+ public TaskInfo getLastTaskInfo() throws IllegalStateException {
+ if (this.lastTaskInfo == null) {
+ throw new IllegalStateException("No tasks run: can't get last task info");
+ }
+ return this.lastTaskInfo;
+ }
+
+ /**
+ * 获取所有任务的总花费时间(纳秒)
+ *
+ * @return 所有任务的总花费时间(纳秒)
+ * @see #getTotalTimeMillis()
+ * @see #getTotalTimeSeconds()
+ */
+ public long getTotalTimeNanos() {
+ return this.totalTimeNanos;
+ }
+
+ /**
+ * 获取所有任务的总花费时间(毫秒)
+ *
+ * @return 所有任务的总花费时间(毫秒)
+ * @see #getTotalTimeNanos()
+ * @see #getTotalTimeSeconds()
+ */
+ public long getTotalTimeMillis() {
+ return DateUtil.nanosToMillis(this.totalTimeNanos);
+ }
+
+ /**
+ * 获取所有任务的总花费时间(秒)
+ *
+ * @return 所有任务的总花费时间(秒)
+ * @see #getTotalTimeNanos()
+ * @see #getTotalTimeMillis()
+ */
+ public double getTotalTimeSeconds() {
+ return DateUtil.nanosToSeconds(this.totalTimeNanos);
+ }
+
+ /**
+ * 获取任务数
+ *
+ * @return 任务数
+ */
+ public int getTaskCount() {
+ return this.taskCount;
+ }
+
+ /**
+ * 获取任务列表
+ *
+ * @return 任务列表
+ */
+ public TaskInfo[] getTaskInfo() {
+ if (null == this.taskList) {
+ throw new UnsupportedOperationException("Task info is not being kept!");
+ }
+ return this.taskList.toArray(new TaskInfo[0]);
+ }
+
+ /**
+ * Get a short description of the total running time.
+ */
+ /**
+ * 获取任务
+ *
+ * @return
+ */
+ public String shortSummary() {
+ return StrUtil.format("StopWatch '{}': running time = {} ns", this.id, this.totalTimeNanos);
+ }
+
+ /**
+ * 生成所有任务的一个任务花费时间表
+ *
+ * @return 任务时间表
+ */
+ public String prettyPrint() {
+ StringBuilder sb = new StringBuilder(shortSummary());
+ sb.append(FileUtil.getLineSeparator());
+ if (null != this.taskList) {
+ sb.append("No task info kept");
+ } else {
+ sb.append("---------------------------------------------").append(FileUtil.getLineSeparator());
+ sb.append("ns % Task name").append(FileUtil.getLineSeparator());
+ sb.append("---------------------------------------------").append(FileUtil.getLineSeparator());
+
+ final NumberFormat nf = NumberFormat.getNumberInstance();
+ nf.setMinimumIntegerDigits(9);
+ nf.setGroupingUsed(false);
+
+ final NumberFormat pf = NumberFormat.getPercentInstance();
+ pf.setMinimumIntegerDigits(3);
+ pf.setGroupingUsed(false);
+ for (TaskInfo task : getTaskInfo()) {
+ sb.append(nf.format(task.getTimeNanos())).append(" ");
+ sb.append(pf.format((double) task.getTimeNanos() / getTotalTimeNanos())).append(" ");
+ sb.append(task.getTaskName()).append(FileUtil.getLineSeparator());
+ }
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder(shortSummary());
+ if (null == this.taskList) {
+ for (TaskInfo task : getTaskInfo()) {
+ sb.append("; [").append(task.getTaskName()).append("] took ").append(task.getTimeNanos()).append(" ns");
+ long percent = Math.round(100.0 * task.getTimeNanos() / getTotalTimeNanos());
+ sb.append(" = ").append(percent).append("%");
+ }
+ } else {
+ sb.append("; no task info kept");
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 存放任务名称和花费时间对象
+ *
+ * @author Looly
+ *
+ */
+ public static final class TaskInfo {
+
+ private final String taskName;
+ private final long timeNanos;
+
+ TaskInfo(String taskName, long timeNanos) {
+ this.taskName = taskName;
+ this.timeNanos = timeNanos;
+ }
+
+ /**
+ * 获取任务名Get the name of this task.
+ */
+ public String getTaskName() {
+ return this.taskName;
+ }
+
+ /**
+ * 获取任务花费时间(单位:纳秒)
+ *
+ * @see #getTimeMillis()
+ * @see #getTimeSeconds()
+ */
+ public long getTimeNanos() {
+ return this.timeNanos;
+ }
+
+ /**
+ * 获取任务花费时间(单位:毫秒)
+ *
+ * @see #getTimeNanos()
+ * @see #getTimeSeconds()
+ */
+ public long getTimeMillis() {
+ return DateUtil.nanosToMillis(this.timeNanos);
+ }
+
+ /**
+ * 获取任务花费时间(单位:秒)
+ *
+ * @see #getTimeMillis()
+ * @see #getTimeNanos()
+ */
+ public double getTimeSeconds() {
+ return DateUtil.nanosToSeconds(this.timeNanos);
+ }
+ }
+}