Java:ファイル書き込みの進捗を取得する

2019年1月21日

長時間かかる処理について、何らのフィードバックも無いと誰しも不安になるものだ。本当に動いているのか、それともハングアップしてしまっているのではないかと。

できないと思いこんでいたのだが、考えてみれば可能だったのだ、少々不確かではあるが。

サンプルコード

まずは実際にこれを行うサンプルコードを示す。

INに指定されたファイルをOUTにGZIP圧縮して書き込むものだ。
進捗状況を1秒ごとに表示し、進捗としては0から1の間のdoubleを表示する。1は完了を示す。

本来であれば、結果のファイルサイズが予めわからないのだが、ここでは元のファイルサイズを指定する。
終了時には1はなく、0.2とか0.3とかになるのだろうが、ユーザにとっては動作していることがわかり、
さらに、0.3だったのが、いきなり完了するという特典もついてくる。

import java.io.*;
import java.nio.file.*;
import java.util.zip.*;

public class FileSizeNotifierTest {

  private static Path IN = Paths.get("....");
  private static Path OUT = Paths.get("...");

  public static void main(String[]args) throws Exception  {
    FileSizeNotifier.executeNotifyProgress(1000, OUT, Files.size(IN), 
    ()-> {
      try (OutputStream output = new GZIPOutputStream(Files.newOutputStream(OUT))) {
          Files.copy(IN,  output);
          return (Object)null;
        }
    }, n-> {
      System.out.println("" + n);
    });

  }
}

FileSizeNotifier

上の例で使ったFileSizeNotifierは以下である。

import java.io.*;
import java.nio.file.*;
import java.util.concurrent.*;
import java.util.function.*;


public class FileSizeNotifier {

  /**
   * 指定された{@link Callable}を別スレッドで動作させる。このスレッドで、対象ファイルに書き込みがされるものとする。
   * この増加するファイルサイズのトータルサイズに対する割合を0から1の間で通知する。
   * 通知する間隔としては、{@link interval}に指定されたミリ秒時間になる。
   * 
   * @param interval 通知間隔ミリ秒
   * @param targetFile 監視対象ファイル
   * @param totalSize 目標とするファイルサイズ
   * @param callable ファイル書き込み処理。これは別スレッドで実行される。
   * @param progressNotify ファイルサイズ変更通知
   * @return {@link callable}が返した値
   * @throws Exception
   */
  public static <T>T executeNotifyProgress(int interval, Path targetFile, long totalSize,
        Callable<T>callable, Consumer<Double>progressNotify) throws Exception {
    ExecutorService service = Executors.newSingleThreadExecutor();
    Future<T>future = service.submit(callable);
    service.shutdown();
    notifyProgress(interval, targetFile, totalSize, future, progressNotify);
    return future.get();
  }

  public static <T> void notifyProgress(int interval, Path targetFile, long totalSize, 
        Future<T>future, Consumer<Double>progressNotify) {
    notifyProgress(interval, targetFile, totalSize, ()->future.isDone(), progressNotify);
  }

  /**
   * ファイルサイズ増加のパーセンテージを通知する。
   * 指定サイズを仮のトータルとする。
   * @param interval 通知間隔、ミリ秒
   * @param target 監視ファイル
   * @param totalSize 仮の全体サイズ、このサイズになったらフルとする。
   * @param finishedCheck 終了チェック。
   * @param progressNotify パーセンテージ通知先
   */
  public static void notifyProgress(int interval, Path target, long totalSize, Supplier<Boolean>finishedCheck, Consumer<Double>progressNotify) {
    long[]notified = new long[] { -1 };
    notifyFileSize(interval, target, finishedCheck, fileSize-> {
      fileSize = Math.min(fileSize,  totalSize);
      if (notified[0] == fileSize) return;
      notified[0] = fileSize;
      double progress = (double)fileSize / totalSize;
      progressNotify.accept(progress);
    });    
  }

  /**
   * 現在のスレッドで監視ファイルサイズの変化を通知する。
   * 終了チェックがtrueを返したときに、制御側に処理を戻す。
   * @param interval 通知間隔、ミリ秒
   * @param target 監視ファイル
   * @param finishedCheck 終了チェック。
   * @param sizeNotify ファイルサイズ通知報告先
   */
  public static void notifyFileSize(int interval, Path target, Supplier<Boolean>finishedCheck, Consumer<Long>sizeNotify) {
    while (!finishedCheck.get()) {
      long fileSize = 0;
      try {
        fileSize = Files.size(target);
      } catch (IOException ex) {
      }
      sizeNotify.accept(fileSize);
      try {
        Thread.sleep(interval);
      } catch (InterruptedException ex) {        
      }
    }
  }
}