Java

URLからファイルを非同期ダウンロードする

本稿では、URLからファイルを非同期でダウンロードする方法について解説します。

こんな人におすすめ
  • ファイルのダウンロードを自動化したい
  • 初期処理等のため、プログラム上でファイルをダウンロードする必要がある
  • 複数ファイルを並列でダウンロードしたい

Javaの標準ライブラリによる実装方法やApache HttpComponents、AsyncHttpClient などの外部ライブラリを用いた実装方法についていくつかサンプルを掲載します。

非同期を考慮しないシンプルな実装方法についてはこちらを参照ください。

URLからファイルをダウンロードする本稿では、URLからファイルをダウンロードする方法について解説します。 Javaの標準ライブラリであるIOパッケージやNI...

Java標準ライブラリで行う方法

Executorフレームワークは、スレッドプールを利用した並列処理実行を行えるようJava 1.5 で導入されたもので、WorkerThreadパターンをシンプルに実装することができます。

以下のサンプルではファイルをダウンロードするタスクをExecutorServiceに送信して、スレッドプール内で実行しています。ここでは戻り値を気にしていませんが、Futureクラスを得るとタスクを追跡することができます。サンプルでは完了までの時間を考慮していませんが、ダウンロードにある程度時間がかかる見込みがあれば、CompletableFutureを使用してすべてのタスクの完了を待ってから終了しましょう。

Java

//取得対象データ(サンプルはロト7の抽選結果)
String[] urlStrings = {
        "https://www.mizuhobank.co.jp/retail/takarakuji/loto/loto7/csv/A1030001.CSV",
        "https://www.mizuhobank.co.jp/retail/takarakuji/loto/loto7/csv/A1030002.CSV",
        "https://www.mizuhobank.co.jp/retail/takarakuji/loto/loto7/csv/A1030003.CSV",
        ...
};

//最大nスレッド(サンプルでは5)のスレッドプールを作成する
ExecutorService executor = Executors.newFixedThreadPool(5);

//複数のダウンロードを並列に実行する例
for(String s : urlStrings) {

    URL url = URI.create(s).toURL();
    String path = url.getPath();
    String name = path.substring(path.lastIndexOf("/") + 1);

    //スレッドプール内でタスクを実行する
    executor.execute(() -> {
        try {
            System.out.println("start. " + url);
            //NIO2を使ってファイル保存する例
            long size = Files.copy(url.openStream(),
                    Paths.get(name),
                    REPLACE_EXISTING);
            System.out.println("complete. " + name + " - " + size + " bytes");
        } catch (IOException e) {
            //error handling
        }
    });
}

//シャットダウンを開始して、これ以降のタスクは受け付けない
executor.shutdown();
//全タスクが完了するかタイムアウトするまでブロックする
executor.awaitTermination(10, TimeUnit.SECONDS);

Apache HttpComponents を使う方法

org.apache.http パッケージを使用してファイルをダウンロードします。

GETやPOSTなどHTTPメソッドを簡単に使い分けできます。また、FutureCallbackではデータ取得完了時、失敗時、キャンセル時の挙動を制御できます。サンプルではリクエストが完了した時にレスポンスを読み取ってファイルに保存しています。

セットアップ

Mavenを使用する場合は、pom.xmlに以下の依存性を追加してください。

pom.xml

<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.10</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/fluent-hc -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>fluent-hc</artifactId>
    <version>4.5.10</version>
</dependency>

特にプロジェクト管理ツールを使わない場合はより公式よりzipをダウンロードして、プロジェクトにjarを追加してください。

ソースコード

Java

//取得対象データ(サンプルはロト7の抽選結果)
String[] inUrl = {
        "https://www.mizuhobank.co.jp/retail/takarakuji/loto/loto7/csv/A1030001.CSV",
        "https://www.mizuhobank.co.jp/retail/takarakuji/loto/loto7/csv/A1030002.CSV",
        "https://www.mizuhobank.co.jp/retail/takarakuji/loto/loto7/csv/A1030003.CSV",
        ...
};

//最大nスレッド(サンプルでは5)のスレッドプールを作成する
ExecutorService executor = Executors.newFixedThreadPool(5);

//複数のダウンロードを並列に実行する例
for (String url : inUrl) {

    String path = URI.create(url).getPath();
    String name = path.substring(path.lastIndexOf("/") + 1);

    Async async = Async.newInstance().use(executor);

    //対象のURLに接続するタスクを送信する
    Future future = async.execute(Request.Get(url), new FutureCallback() {

        //対象のURLからのデータ取得が完了した場合の挙動
        @Override
        public void completed(Content content) {
            try {
                //NIO2を使ってファイル保存する例
                long size = Files.copy(content.asStream(),
                        Paths.get(name),
                        StandardCopyOption.REPLACE_EXISTING);
                System.out.println("completed. " + name + " - " + size + " bytes");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void failed(Exception e) {
            System.out.println("failed.");
        }

        @Override
        public void cancelled() {
            System.out.println("cancelled.");
        }
    });

    System.out.println("started. " + url);
}

//シャットダウンを開始して、これ以降のタスクは受け付けない
executor.shutdown();
//全タスクが完了するかタイムアウトするまでブロックする
executor.awaitTermination(10, TimeUnit.SECONDS);

補足

ホストが見つからない場合もファイルが見つからない場合も、FutureCallback#failedが呼ばれます。

AsyncHttpClient を使う方法

AsyncHttpClientライブラリにより、HTTP要求を簡単に実行しHTTP応答を非同期的に処理できます。コールバックを定義することで完了時にファイルを保存するよう制御できます。

以下のサンプルでは、CompletableFutureを使用して、すべてのタスクが完了してからクライアントをクローズするようにしています。

セットアップ

Mavenを使用する場合は、pom.xmlに以下の依存性を追加してください。

pom.xml

<!-- https://mvnrepository.com/artifact/org.asynchttpclient/async-http-client -->
<dependency>
    <groupId>org.asynchttpclient</groupId>
    <artifactId>async-http-client</artifactId>
    <version>2.10.4</version>
</dependency>

特にプロジェクト管理ツールを使わない場合はMavenのCentralリポジトリよりjarをダウンロードしてください。
async-http-client-2.10.4.jar

ソースコード

Java

//取得対象データ(サンプルはロト7の抽選結果)
String[] inUrl = {
        "https://www.mizuhobank.co.jp/retail/takarakuji/loto/loto7/csv/A1030001.CSV",
        "https://www.mizuhobank.co.jp/retail/takarakuji/loto/loto7/csv/A1030002.CSV",
        "https://www.mizuhobank.co.jp/retail/takarakuji/loto/loto7/csv/A1030003.CSV",
        ...
};

AsyncHttpClient client = Dsl.asyncHttpClient();
List futures = new ArrayList();

for (String url : inUrl) {

    String path = URI.create(url).getPath();
    String name = path.substring(path.lastIndexOf("/") + 1);

    //対象URLへ接続するリクエスト
    Request request = Dsl.request(HttpConstants.Methods.GET, url).build();
    
    //リクエストが完了した場合のハンドラを指定して非同期で実行
    ListenableFuture f = client.executeRequest(request,
            new AsyncCompletionHandler() {
                @Override
                public Long onCompleted(Response response) throws Exception {
                    //リクエストが完了した時はレスポンスをファイルに保存
                    long size = Files.copy(response.getResponseBodyAsStream(),
                            Paths.get(name),
                            StandardCopyOption.REPLACE_EXISTING);
                    System.out.println("completed. " + name + " - " + size + " bytes");
                    return size;
                }
            });
    System.out.println("started. " + url);

    //後述のCompletableFutureで使用
    futures.add(f.toCompletableFuture());
}

//すべてのタスクが完了してからClientを閉じる
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
        .whenComplete((r, e) -> {
            try {
                client.close();
            } catch (IOException ex) {
                //error handling
            }
        });

注意

ファイルが存在しない場合でも、接続先のサーバがステータスコード404を返してくれるような場合はonCompleteが呼ばれます。処理の中で適当に扱う必要があります。

if (response.getStatusCode() != 200) {
    //Not HTTP_OK
}

ホスト自体が見つからない場合や応答がない場合はonCompleteは呼ばれません。

また、応答なしを考慮してクライアント生成時にタイムアウトを設定したほうがよいでしょう。

AsyncHttpClientConfig config = Dsl.config()
        .setConnectTimeout(10)
        .setReadTimeout(15)
        .build();
AsyncHttpClient client = Dsl.asyncHttpClient(config);
ABOUT ME
sasakiyu
ひ弱で優しい少年だったが、デーモンと合体。 強大な力を得つつも人間の心を失わないデベルマンとなる。