Java 进度条工具

有时候批量处理任务无法观察执行情况,例如处理进度、处理数量、耗时等,这种“不确定性”会隐藏 bug,等到出问题就晚了。今天,这篇文章的“主角” —— 进度条工具 —— 就是为了解决这个问题。

进度条工具输出预览:

[4B645A43] 远程请求记录: [####                ] - 22.91% (9835/42920), 耗时: 2m32s
   (1)        (2)               (3)               (4)       (5)          (6)

各部分说明:

(1) Trace ID
(2) 进度条名称
(3) 进度条面板
(4) 执行占比
(5) 执行数量细节
(6) 执行耗时

实现比较简单,源码如下:

public class ProgressBar {
    private final String name;
    private int begin;
    private final int end;
    private final int step;
    private final int printIntervalMills; // 打印间隔, 毫秒
    private final long startMills;
    private long lastPrintMills;
    private final String traceId;
    private final Logger log;
    // "[%s] %s: %s - %s (%d/%d), 耗时: %s"
    private static final String TPL = "[%s] %s: %s - %s (%d/%d), 耗时: %s";
    private static final String PGS = "[####################][                    ]";

    /** 默认打印间隔 5s */
    public static ProgressBar of(String name, int total) {
        return new ProgressBar(name, total, 1, 5000, null);
    }

    public static ProgressBar of(String name, int total, int printIntervalMills) {
        return new ProgressBar(name, total, 1, printIntervalMills, null);
    }

    /** 默认打印间隔 5s */
    public static ProgressBar of(String name, int total, Logger log) {
        return new ProgressBar(name, total, 1, 5000, log);
    }

    public static ProgressBar of(String name, int total, int printIntervalMills, Logger log) {
        return new ProgressBar(name, total, 1, printIntervalMills, log);
    }

    public ProgressBar(String name, int total, int step, int printIntervalMills, Logger log) {
        this.begin = 0;
        this.end = Math.max(1, total); // >= 1 & 分母 不为0
        this.step = Math.max(1, step); // >= 1
        this.printIntervalMills = Math.max(0, printIntervalMills); // >= 0
        this.name = name != null ? name : "";
        this.log = log;
        lastPrintMills = startMills = System.currentTimeMillis();
        traceId = Integer.toHexString((int) startMills).toUpperCase();
    }

    public void nextPrint() {
        next();
        print();
    }

    public void next() {
        begin = Math.min(begin + step, end); // 预防 begin 溢出 end
    }

    public void print() {
        if (System.currentTimeMillis() - lastPrintMills < printIntervalMills) return;
        immediatePrint();
    }

    public void immediatePrint() {
        Object[] args = new Object[]{traceId, name, makeProgress(), calcPercent() + "%", begin, end, makeSpendTime()};
        if (log != null)
            log.info(String.format(TPL, args));
        else
            System.out.printf(TPL + "\n", args);
        lastPrintMills = System.currentTimeMillis();
    }

    // ~ private method
    // --------------------------------------------------------------------------------------------
    private String makeProgress() {
        int per = (int) (calcPercent() * 2 / 10); // per <= 20
        // e.g. [##############      ]
        return PGS.substring(0, per + 1).concat(PGS.substring(23 + per)); // 23: PGS.length / 2 + 1
    }

    private String makeSpendTime() {
        int n = (int) ((System.currentTimeMillis() - startMills) / 1000);
        int h = n / 3600, m = (n % 3600) / 60, s = n % 60;
        String time = "";
        if (h > 0) time += (h + "h");
        if (m > 0) time += (m + "m");
        if (s > 0) time += (s + "s");
        return time.isEmpty() ? "0s" : time; // e.g. 1h23m15s、23m15s、15s
    }

    private float calcPercent() {
        float per = (begin * 1.0F) / end;
        if (1.0F - per < 0.00001F) return 100.0F;
        return Math.round(per * 10000) / 100.0F;
    }
}

使用例子:

// Case 1: 每5s打印一次进度条

ProgressBar pgs = ProgressBar.of("远程请求记录", size, 5000);
for (Request req: reqs) {
    // 1)递增步长;2)在满足打印间隔时间后打印进度条
    pgs.nextPrint();

    // TODO
}

// Case 2: 分为两步,每5s打印一次进度条
ProgressBar pgs2 = ProgressBar.of("远程请求记录", size, 5000);
for (Request req: reqs) {
    pgs2.next();// 1)仅递增步长
    pgs2.print();// 2)在满足打印间隔时间后打印进度条

    // TODO
}

// Case 3: 立即打印进度条
ProgressBar pgs3 = ProgressBar.of("远程请求记录", size, 5000);
for (Request req: reqs) {
    pgs3.next();// 1)单纯递增步长
    pgs3.immediatePrint();// 2)忽略打印间隔时间,**立即**打印进度条!

    // TODO
}

使用注意事项:

  1. 不支持多线程。
  2. 合理打印。频繁打印同样会影响阅读日志,注意评估打印间隔时间。

(完)