Unduh File Dari URL di Java

1. Perkenalan

Dalam tutorial ini, kita akan melihat beberapa metode yang dapat kita gunakan untuk mengunduh file.

Kami akan membahas contoh mulai dari penggunaan dasar Java IO hingga paket NIO, dan beberapa pustaka umum seperti Async Http Client dan Apache Commons IO.

Terakhir, kita akan berbicara tentang bagaimana kita dapat melanjutkan pengunduhan jika koneksi kita gagal sebelum seluruh file dibaca.

2. Menggunakan Java IO

API paling dasar yang dapat kita gunakan untuk mendownload file adalah Java IO. Kita dapat menggunakan kelas URL untuk membuka koneksi ke file yang ingin kita unduh. Untuk membaca file secara efektif, kita akan menggunakan metode openStream () untuk mendapatkan InputStream:

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

Saat membaca dari InputStream , disarankan untuk menggabungkannya dalam BufferedInputStream untuk meningkatkan performa.

Peningkatan kinerja berasal dari buffering. Saat membaca satu byte pada satu waktu menggunakan metode read () , setiap panggilan metode menyiratkan panggilan sistem ke sistem file yang mendasarinya. Ketika JVM memanggil panggilan sistem read () , konteks eksekusi program beralih dari mode pengguna ke mode kernel dan sebaliknya.

Sakelar konteks ini mahal dari sudut pandang kinerja. Saat kita membaca byte dalam jumlah besar, kinerja aplikasi akan menjadi buruk, karena banyaknya switch konteks yang terlibat.

Untuk menulis byte yang dibaca dari URL ke file lokal kita, kita akan menggunakan metode write () dari kelas FileOutputStream :

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 }

Saat menggunakan BufferedInputStream, metode read () akan membaca byte sebanyak yang kita setel untuk ukuran buffer. Dalam contoh kami, kami sudah melakukan ini dengan membaca blok 1024 byte sekaligus, jadi BufferedInputStream tidak diperlukan.

Contoh di atas sangat bertele-tele, tetapi untungnya, pada Java 7, kami memiliki kelas File yang berisi metode pembantu untuk menangani operasi IO. Kita bisa menggunakan metode Files.copy () untuk membaca semua byte dari InputStream dan menyalinnya ke file lokal:

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

Kode kami berfungsi dengan baik tetapi dapat ditingkatkan. Kelemahan utamanya adalah kenyataan bahwa byte di-buffer ke dalam memori.

Untungnya, Java menawarkan kepada kami paket NIO yang memiliki metode untuk mentransfer byte secara langsung antara 2 Saluran tanpa buffering.

Kami akan membahas detail di bagian selanjutnya.

3. Menggunakan NIO

Paket Java NIO menawarkan kemungkinan untuk mentransfer byte antara 2 Saluran tanpa melakukan buffering ke dalam memori aplikasi.

Untuk membaca file dari URL kami, kami akan membuat ReadableByteChannel baru dari aliran URL :

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

Byte yang dibaca dari ReadableByteChannel akan ditransfer ke FileChannel yang sesuai dengan file yang akan diunduh:

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

Kami akan menggunakan metode transferFrom () dari kelas ReadableByteChannel untuk mengunduh byte dari URL yang diberikan ke FileChannel kami :

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

Metode transferTo () dan transferFrom () lebih efisien daripada sekadar membaca dari aliran menggunakan buffer. Bergantung pada sistem operasi yang mendasarinya, data dapat ditransfer langsung dari cache sistem file ke file kami tanpa menyalin byte apa pun ke dalam memori aplikasi .

Pada sistem Linux dan UNIX, metode ini menggunakan teknik zero-copy yang mengurangi jumlah sakelar konteks antara mode kernel dan mode pengguna.

4. Menggunakan Perpustakaan

Kami telah melihat dalam contoh di atas bagaimana kami dapat mengunduh konten dari URL hanya dengan menggunakan fungsionalitas inti Java. Kami juga dapat memanfaatkan fungsionalitas pustaka yang ada untuk memudahkan pekerjaan kami, ketika penyesuaian kinerja tidak diperlukan.

Misalnya, dalam skenario dunia nyata, kami memerlukan kode unduhan kami untuk menjadi asinkron.

Kita bisa menggabungkan semua logika ke dalam Callable , atau kita bisa menggunakan perpustakaan yang ada untuk ini.

4.1. Klien HTTP Asinkron

AsyncHttpClient adalah pustaka populer untuk menjalankan permintaan HTTP asinkron menggunakan kerangka kerja Netty. Kita dapat menggunakannya untuk menjalankan permintaan GET ke URL file dan mendapatkan konten file.

Pertama, kita perlu membuat klien HTTP:

AsyncHttpClient client = Dsl.asyncHttpClient();

Konten yang diunduh akan ditempatkan ke dalam FileOutputStream :

FileOutputStream stream = new FileOutputStream(FILE_NAME);

Selanjutnya, kami membuat permintaan HTTP GET dan mendaftarkan penangan AsyncCompletionHandler untuk memproses konten yang diunduh:

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

Perhatikan bahwa kita telah mengganti metode onBodyPartReceived () . Implementasi default mengakumulasikan potongan HTTP yang diterima ke dalam ArrayList . Hal ini dapat menyebabkan konsumsi memori yang tinggi, atau pengecualian OutOfMemory saat mencoba mengunduh file besar.

Alih-alih mengumpulkan setiap HttpResponseBodyPart ke dalam memori, kami menggunakan FileChannel untuk menulis byte ke file lokal kami secara langsung . Kami akan menggunakan metode getBodyByteBuffer () untuk mengakses konten bagian tubuh melalui ByteBuffer .

ByteBuffers memiliki keuntungan bahwa memori dialokasikan di luar heap JVM, sehingga tidak memengaruhi memori aplikasi.

4.2. Apache Commons IO

Pustaka lain yang sangat digunakan untuk operasi IO adalah Apache Commons IO. Kita dapat melihat dari Javadoc bahwa ada kelas utilitas bernama FileUtils yang digunakan untuk tugas manipulasi file umum.

Untuk mengunduh file dari URL, kita dapat menggunakan satu baris ini:

FileUtils.copyURLToFile( new URL(FILE_URL), new File(FILE_NAME), CONNECT_TIMEOUT, READ_TIMEOUT);

Dari sudut pandang kinerja, kode ini sama dengan yang telah kami contohkan di bagian 2.

Kode yang mendasari menggunakan konsep yang sama untuk membaca dalam loop beberapa byte dari InputStream dan menuliskannya ke OutputStream .

Satu perbedaan adalah fakta bahwa di sini kelas URLConnection digunakan untuk mengontrol waktu tunggu koneksi sehingga unduhan tidak memblokir untuk waktu yang lama:

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

5. Download Lanjutkan

Mengingat koneksi internet gagal dari waktu ke waktu, ada gunanya bagi kita untuk dapat melanjutkan pengunduhan, daripada mengunduh file lagi dari byte nol.

Mari tulis ulang contoh pertama dari sebelumnya, untuk menambahkan fungsionalitas ini.

Hal pertama yang harus kita ketahui adalah kita dapat membaca ukuran file dari URL tertentu tanpa benar-benar mengunduhnya dengan menggunakan metode HTTP HEAD:

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

Sekarang setelah kami memiliki ukuran konten total file, kami dapat memeriksa apakah file kami diunduh sebagian. Jika demikian, kami akan melanjutkan pengunduhan dari byte terakhir yang direkam di disk:

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

What happens here is that we've configured the URLConnection to request the file bytes in a specific range. The range will start from the last downloaded byte and will end at the byte corresponding to the size of the remote file.

Another common way to use the Range header is for downloading a file in chunks by setting different byte ranges. For example, to download 2 KB file, we can use the range 0 – 1024 and 1024 – 2048.

Another subtle difference from the code at section 2. is that the FileOutputStream is opened with the append parameter set to true:

OutputStream os = new FileOutputStream(FILE_NAME, true);

After we've made this change the rest of the code is identical to the one we've seen in section 2.

6. Conclusion

Kami telah melihat di artikel ini beberapa cara di mana kami dapat mengunduh file dari URL di Java.

Implementasi yang paling umum adalah yang kami buffer byte saat melakukan operasi baca / tulis. Implementasi ini aman digunakan bahkan untuk file besar karena kami tidak memuat seluruh file ke dalam memori.

Kami juga telah melihat bagaimana kami dapat mengimplementasikan unduhan tanpa salinan menggunakan Java NIO Channels . Ini berguna karena meminimalkan jumlah sakelar konteks yang dilakukan saat membaca dan menulis byte dan dengan menggunakan buffer langsung, byte tidak dimuat ke dalam memori aplikasi.

Selain itu, karena biasanya mengunduh file dilakukan melalui HTTP, kami telah menunjukkan bagaimana kami dapat melakukannya menggunakan pustaka AsyncHttpClient.

Kode sumber artikel tersedia di GitHub.