JavaのURLからファイルをダウンロードする

JavaのURLからファイルをダウンロードする

1. 前書き

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

Java IOの基本的な使用法からNIOパッケージまでの例と、Async HttpClientやApacheCommonsIOなどのいくつかの一般的なライブラリについて説明します。

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

2. JavaIOの使用

ファイルのダウンロードに使用できる最も基本的なAPIはJava IOです。 URL classを使用して、ダウンロードするファイルへの接続を開くことができます。 ファイルを効果的に読み取るために、openStream()メソッドを使用してInputStream:を取得します

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

InputStreamから読み取る場合は、パフォーマンスを向上させるために、BufferedInputStreamでラップすることをお勧めします。

パフォーマンスの向上は、バッファリングによるものです。 read()メソッドを使用して一度に1バイトを読み取る場合、各メソッド呼び出しは、基になるファイルシステムへのシステムコールを意味します。 JVMがread()システムコールを呼び出すと、プログラム実行コンテキストがユーザーモードからカーネルモードに切り替わり、元に戻ります。

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

URLから読み取ったバイトをローカルファイルに書き込むために、FileOutputStream classの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クラスがあります。 InputStreamからすべてのバイトを読み取り、それらをローカルファイルにコピーするWe can use the Files.copy()メソッド:

InputStream in = new URL(FILE_URL).openStream();
Files.copy(in, Paths.get(FILE_NAME), StandardCopyOption.REPLACE_EXISTING);

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

幸い、Javaは、バッファリングせずに2Channelsの間で直接バイトを転送するメソッドを備えたNIOパッケージを提供しています。

次のセクションで詳細を説明します。

3. NIOの使用

Java NIOパッケージは、アプリケーションメモリにバッファリングせずに2Channelsの間でバイトを転送する可能性を提供します。

URLからファイルを読み取るために、URL streamから新しい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()メソッドは、バッファを使用してストリームから単に読み取るよりも効率的です。 基盤となるオペレーティングシステムに応じて、the data can be transferred directly from the filesystem cache to our file without copying any bytes into the application memory

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() {

    @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()メソッドをオーバーライドしたことに注意してください。 The default implementation accumulates the HTTP chunks received into an ArrayList.これにより、メモリ消費量が多くなるか、大きなファイルをダウンロードしようとしたときにOutOfMemory例外が発生する可能性があります。

HttpResponseBodyPartをメモリに蓄積する代わりに、we use a FileChannel to write the bytes to our local file directlygetBodyByteBuffer()メソッドを使用して、ByteBufferを介して身体部分のコンテンツにアクセスします。

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

4.2. Apache Commons IO

IO操作によく使用されるもう1つのライブラリは、Apache Commons IOです。 Javadocから、一般的なファイル操作タスクに使用される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からファイルを再度ダウンロードする代わりに、ダウンロードを再開できると便利です。

この機能を追加するために、前の最初の例を書き直してみましょう。

最初に知っておくべきことは、we can read the size of a file from a given URL without actually downloading it by using the HTTP HEAD method:

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
    );
}

ここで何が起こるかというと、we’ve configured the URLConnection to request the file bytes in a specific rangeです。 範囲は、最後にダウンロードしたバイトから始まり、リモートファイルのサイズに対応するバイトで終わります。

Rangeヘッダーを使用する別の一般的な方法は、さまざまなバイト範囲を設定してファイルをチャンクでダウンロードすることです。 たとえば、2 KBファイルをダウンロードするには、0〜1024および1024〜2048の範囲を使用できます。

セクション2のコードとは別の微妙な違いがあります。 FileOutputStream is opened with the append parameter set to true

OutputStream os = new FileOutputStream(FILE_NAME, true);

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

6. 結論

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

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

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

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

記事のソースコードはover on GitHubで入手できます。