JavaのURLからファイルをダウンロードする
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で利用可能]です。