1前書き

このチュートリアルでは、ファイルをダウンロードするために使用できるいくつかの方法について説明します。

Java IOの基本的な使い方からNIOパッケージ、そしてAsync Http ClientやApache Commons IOのような一般的なライブラリまで、さまざまな例を取り上げます。

最後に、ファイル全体が読み込まれる前に接続が失敗した場合にダウンロードを再開する方法について説明します。


2 Java IO

を使用する

ファイルをダウンロードするために使用できる最も基本的なAPIはhttps://docs.oracle.com/javase/8/docs/api/java/io/package-summary.html[Java IO]です。

__URL


classを使用して、ダウンロードしたいファイルへの接続を開くことができます。ファイルを効率的に読み取るために、

openStream()

メソッドを使用して

InputStreamを取得します。

BufferedInputStream in = new BufferedInputStream(new URL(FILE__URL).openStream())


  • InputStream

    から読み取るときは、パフォーマンスを向上させるために

    BufferedInputStream

    でラップすることをお勧めします。**

パフォーマンスの向上はバッファリングによるものです。

read()

メソッドを使用して一度に1バイトを読み込むとき、各メソッド呼び出しは、基礎となるファイルシステムへのシステム呼び出しを意味します。 JVMが

read()

システムコールを呼び出すと、プログラム実行コンテキストはユーザーモードからカーネルモードに切り替わります。

このコンテキスト切り替えは、パフォーマンスの観点からは高価です。大量のバイトを読み取ると、多数のコンテキストスイッチが関係するため、アプリケーションのパフォーマンスが低下します。

URLから読み込んだバイトをローカルファイルに書き込むには、

__ FileOutputStream


クラスの

write()__メソッドを使用します。

try (BufferedInputStream in = new BufferedInputStream(new URL(FILE__URL).openStream());
  FileOutputStream fileOutputStream new FileOutputStream(FILE__NAME)) {
    byte dataBuffer[]= new byte[1024];
    int bytesRead;
    while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) {
        fileOutputStream.write(dataBuffer, 0, bytesRead);
    }
} catch (IOException e) {
   //handle exception
}


BufferedInputStreamを使用する場合、

read()

メソッドはバッファサイズに設定したバイト数を読み取ります。この例では、すでに1024バイトのブロックを一度に読み取ることでこれを実行しているので、

BufferedInputStream__は必要ありません。

上の例は非常に冗長ですが、幸運なことに、Java 7の時点で、IO操作を処理するためのヘルパーメソッドを含む

Files

クラスがあります。


Files.copy()


メソッドを使用して

InputStream

からすべてのバイトを読み取り、それらをローカルファイルにコピーすることができます。

InputStream in = new URL(FILE__URL).openStream();
Files.copy(in, Paths.get(FILE__NAME), StandardCopyOption.REPLACE__EXISTING);

私たちのコードはうまく機能しますが、改善することができます。その主な欠点はバイトがメモリにバッファされるという事実です。

幸いなことに、Javaはバッファリングなしで2

チャンネル

間で直接バイトを転送するメソッドを持つNIOパッケージを提供しています。

次のセクションで詳細に入ります。


3 NIO

を使う


Java NIO

パッケージを使用すると、2バイトの間でバイトをアプリケーションメモリにバッファすることなく転送できます。

URLからファイルを読み取るために、

__URL


ストリームから新しい

ReadableByteChannel__を作成します。

ReadableByteChannel readableByteChannel = Channels.newChannel(url.openStream());


ReadableByteChannel

から読み取られたバイトは、ダウンロードされるファイルに対応する

FileChannel

に転送されます。

FileOutputStream fileOutputStream = new FileOutputStream(FILE__NAME);
FileChannel fileChannel = fileOutputStream.getChannel();


ReadableByteChannel

クラスの

transferFrom()

メソッドを使用して、指定されたURLから

FileChannel

にバイトをダウンロードします。

fileOutputStream.getChannel()
  .transferFrom(readableByteChannel, 0, Long.MAX__VALUE);


transferTo()

メソッドと

transferFrom()

メソッドは、バッファを使用してストリームから単純に読み取るよりも効率的です。基盤となるオペレーティングシステムによっては、データをバイト単位でアプリケーションメモリにコピーすることなく、ファイルシステムのキャッシュからファイルに直接転送することができます。

LinuxおよびUNIXシステムでは、これらの方法はカーネルモードとユーザーモードの間のコンテキストスイッチの数を減らす

zero-copy

手法を使用します。


4ライブラリを使う

上記の例では、Javaコア機能を使用してURLからコンテンツをダウンロードする方法を説明しました。パフォーマンスの調整が不要な場合は、既存のライブラリの機能を活用して作業を容易にすることもできます。

たとえば、現実のシナリオでは、ダウンロードコードを非同期にする必要があります。

すべてのロジックを

Callable

にラップすることも、既存のライブラリを使うこともできます。


4.1. 非同期HTTPクライアント


AsyncHttpClient

は、Nettyフレームワークを使用して非同期HTTP要求を実行するための一般的なライブラリです。これを使用してファイルのURLに対してGETリクエストを実行し、ファイルの内容を取得することができます。

まず、HTTPクライアントを作成する必要があります。

AsyncHttpClient client = Dsl.asyncHttpClient();

ダウンロードしたコンテンツは

FileOutputStream

に配置されます。

FileOutputStream stream = new FileOutputStream(FILE__NAME);

次に、HTTP GETリクエストを作成し、ダウンロードしたコンテンツを処理するための

AsyncCompletionHandler

ハンドラを登録します。

client.prepareGet(FILE__URL).execute(new AsyncCompletionHandler<FileOutputStream>() {

    @Override
    public State onBodyPartReceived(HttpResponseBodyPart bodyPart)
      throws Exception {
        stream.getChannel().write(bodyPart.getBodyByteBuffer());
        return State.CONTINUE;
    }

    @Override
    public FileOutputStream onCompleted(Response response)
      throws Exception {
        return stream;
    }
})


onBodyPartReceived()

メソッドがオーバーライドされたことに注意してください。

デフォルトの実装は受け取ったHTTPチャンクを

ArrayList

に蓄積します。

これはメモリ消費量の増加、または大きなファイルをダウンロードしようとしたときの

OutOfMemory

例外につながる可能性があります。

それぞれの

HttpResponseBodyPart

をメモリに蓄積する代わりに、バイトをローカルファイルに直接書き込むために

FileChannel

を使用します。

ByteBuffer

を介して本文部分のコンテンツにアクセスするには、

getBodyByteBuffer()

メソッドを使用します。


  • ByteBuffers

    には、メモリがJVMヒープの外側に割り当てられるという利点があるため、アプリケーションメモリには影響しません。


4.2. Apache Commons IO

IO操作によく使われるもう1つのライブラリはhttps://commons.apache.org/proper/commons-io/[Apache Commons IO]です。 Javadocから、https://commons.apache.org/proper/commons-io/apidocs/index.html?org/apache/commons/io/FileUtils.html[

FileUtils

]というユーティリティクラスがあることがわかります。一般的なファイル操作タスクに使用されます。

URLからファイルをダウンロードするには、このワンライナーを使用できます。

FileUtils.copyURLToFile(
  new URL(FILE__URL),
  new File(FILE__NAME),
  CONNECT__TIMEOUT,
  READ__TIMEOUT);

パフォーマンスの観点からは、このコードはセクション2で例示したものと同じです。

基礎となるコードは、

InputStream

からいくつかのバイトをループで読み込み、それらを

OutputStream

に書き込むという同じ概念を使用します。

1つの違いは、ダウンロードが長時間ブロックされないように、ここでは

URLConnection

クラスを使用して接続タイムアウトを制御することです。

URLConnection connection = source.openConnection();
connection.setConnectTimeout(connectionTimeout);
connection.setReadTimeout(readTimeout);


5再開可能ダウンロード

インターネット接続が時々失敗することを考えると、バイト0からファイルを再度ダウンロードするのではなく、ダウンロードを再開できると便利です。

この機能を追加するために、先ほどの最初の例を書き換えてみましょう。

最初に知っておくべきことは、HTTP HEADメソッドを使用することで、実際にはダウンロードせずに、特定のURLからファイルのサイズを読み取ることができるということです。

URL url = new URL(FILE__URL);
HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.setRequestMethod("HEAD");
long removeFileSize = httpConnection.getContentLengthLong();

ファイルの合計コンテンツサイズがわかったので、ファイルが部分的にダウンロードされたかどうかを確認できます。その場合は、ディスクに記録されている最後のバイトからダウンロードを再開します。

long existingFileSize = outputFile.length();
if (existingFileSize < fileLength) {
    httpFileConnection.setRequestProperty(
      "Range",
      "bytes=" + existingFileSize + "-" + fileLength
    );
}

ここで起こることは、** 特定の範囲のファイルバイトを要求するように

URLConnection

を設定したことです。範囲は最後にダウンロードされたバイトから始まり、リモートファイルのサイズに対応するバイトで終わります。


Range

ヘッダーを使用するもう1つの一般的な方法は、さまざまなバイト範囲を設定してファイルをまとめてダウンロードすることです。たとえば、2 KBのファイルをダウンロードするには、0 – 1024と1024 – 2048の範囲を使用できます。

セクション2のコードとのもう1つの微妙な違いは、**

FileOutputStream



append

パラメータをtrueに設定して開かれることです。

OutputStream os = new FileOutputStream(FILE__NAME, true);

この変更を加えた後のコードの残りの部分は、セクション2で見たものと同じです。


6. 結論

この記事では、JavaのURLからファイルをダウンロードする方法をいくつか紹介しました。

最も一般的な実装は、読み書き操作を実行するときにバイトをバッファリングする実装です。この実装は、ファイル全体をメモリにロードしないため、大きなファイルに対しても安全に使用できます。

また、Java NIO

Channels

を使用してゼロコピーダウンロードを実装する方法についても説明しました。これは、バイトを読み書きするときに行われるコンテキスト切り替えの数が最小限に抑えられ、ダイレクトバッファを使用することによって、アプリケーションメモリに読み込まれないので便利です。

また、通常ファイルのダウンロードはHTTP経由で行われるため、AsyncHttpClientライブラリを使用してこれを実現する方法を示しました。

この記事のソースコードはhttps://github.com/eugenp/tutorials/tree/master/core-java-io[GitHubで利用可能]です。