java實(shí)現(xiàn)高效下載文件的方法
本文實(shí)例為大家分享了java實(shí)現(xiàn)下載文件的方法,供大家參考,具體內(nèi)容如下
本文我們介紹幾種方法下載文件。從基本JAVA IO 到 NIO包,也介紹第三方庫的一些方法,如Async Http Client 和 Apache Commons IO.
最后我們還討論在連接斷開后如何恢復(fù)下載。
使用java IO
下載文件最基本的方法是java IO,使用URL類打開待下載文件的連接。為有效讀取文件,我們使用openStream() 方法獲取 InputStream:
BufferedInputStream in = new BufferedInputStream(new URL(FILE_URL).openStream())
當(dāng)從InputStream讀取文件時(shí),強(qiáng)烈建議使用BufferedInputStream去包裝InputStream,用于提升性能。
使用緩存可以提升性能。read方法每次讀一個(gè)字節(jié),每次方法調(diào)用意味著系統(tǒng)調(diào)用底層的文件系統(tǒng)。當(dāng)JVM調(diào)用read()方法時(shí),程序執(zhí)行上下文將從用戶模式切換到內(nèi)核模式并返回。
從性能的角度來看,這種上下文切換非常昂貴。當(dāng)我們讀取大量字節(jié)時(shí),由于涉及大量上下文切換,應(yīng)用程序性能將會(huì)很差。
為了讀取URL的字節(jié)并寫至本地文件,需要使用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方法按照我們?cè)O(shè)置緩沖器大小讀取文件。示例中我們?cè)O(shè)置一次讀取1024字節(jié),所以BufferedInputStream 是必要的。
上述示例代碼冗長,幸運(yùn)的是在Java7中Files類包含處理IO操作的助手方法??梢允褂肍ile.copy()方法從InputStream中讀取所有字節(jié),然后復(fù)制至本地文件:
InputStream in = new URL(FILE_URL).openStream(); Files.copy(in, Paths.get(FILE_NAME), StandardCopyOption.REPLACE_EXISTING);
上述代碼可以正常工作,但缺點(diǎn)是字節(jié)被緩沖到內(nèi)存中。Java為我們提供了NIO包,它有方法在兩個(gè)通道之間直接傳輸字節(jié),而無需緩沖。下面我們會(huì)詳細(xì)講解。
使用NIO
java NIO包提供了無緩沖情況下在兩個(gè)通道之間直接傳輸字節(jié)的可能。
為了讀來自URL的文件,需從URL流創(chuàng)建ReadableByteChannel :
ReadableByteChannel readableByteChannel = Channels.newChannel(url.openStream());
從ReadableByteChannel 讀取字節(jié)將被傳輸至FileChannel:
FileOutputStream fileOutputStream = new FileOutputStream(FILE_NAME); FileChannel fileChannel = fileOutputStream.getChannel();
然后使用transferFrom方法,從ReadableByteChannel 類下載來自URL的字節(jié)傳輸?shù)紽ileChannel:
fileOutputStream.getChannel() .transferFrom(readableByteChannel, 0, Long.MAX_VALUE);
transferTo() 和 transferFrom() 方法比簡單使用緩存從流中讀更有效。依據(jù)不同的底層操作系統(tǒng),數(shù)據(jù)可以直接從文件系統(tǒng)緩存?zhèn)鬏數(shù)轿覀兊奈募?,而不必將任何字?jié)復(fù)制到應(yīng)用程序內(nèi)存中。
在Linux和UNIX系統(tǒng)上,這些方法使用零拷貝技術(shù),減少了內(nèi)核模式和用戶模式之間的上下文切換次數(shù)。
使用第三方庫
上面我們已經(jīng)使用java 核心功能實(shí)現(xiàn)從URL下載文件。當(dāng)無需調(diào)整性能是,我們也可以利用第三方庫輕松實(shí)現(xiàn)。舉例,在實(shí)際場(chǎng)景中,需要實(shí)現(xiàn)異步下載,我們可以封裝邏輯至Callable,下面看已有庫實(shí)現(xiàn)。
Async HTTP Client
Async HTTP Client是使用Netty框架執(zhí)行異步HTTP請(qǐng)求的流行庫。我們使用它對(duì)URL文件執(zhí)行GET請(qǐng)求并獲取其內(nèi)容。首先需要?jiǎng)?chuàng)建HTTP client:
AsyncHttpClient client = Dsl.asyncHttpClient();
下面內(nèi)容放到FileOutputStream:
FileOutputStream stream = new FileOutputStream(FILE_NAME);
接下來,創(chuàng)建HTTP GET請(qǐng)求并注冊(cè)AsyncCompletionHandler 處理器去處理下載內(nèi)容:
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() 方法。缺省實(shí)現(xiàn)是累加HTTP 接收塊至ArrayList,這可能導(dǎo)致耗費(fèi)高內(nèi)存或下載大文件時(shí)OutOfMemory 異常。
代替累加每個(gè)HttpResponseBodyPart 至內(nèi)存,我們使用FileChannel寫字節(jié)至本地文件。getBodyByteBuffer()方法通過ByteBuffer訪問bodyPart內(nèi)容。ByteBuffers的優(yōu)勢(shì)是把內(nèi)存分配到JVM堆之外,所以不會(huì)影響應(yīng)用程序的內(nèi)存。
Apache Commons IO
另一個(gè)高可用的IO操作庫是Apache Commons IO。我們從其Javadoc看到FileUtils實(shí)用類,用于一般的文件操作任務(wù)。從URL下載文件,僅需一行代碼:
FileUtils.copyURLToFile( new URL(FILE_URL), new File(FILE_NAME), CONNECT_TIMEOUT, READ_TIMEOUT);
從性能角度看,與前面JAVA IO示例相同。底層代碼使用相同的概念,從InputStream讀取一些字節(jié)并將它們寫入OutputStream。不同之處在于,URLConnection類在這里用于控制連接超時(shí),這樣下載就不會(huì)阻塞很長時(shí)間:
URLConnection connection = source.openConnection(); connection.setConnectTimeout(connectionTimeout); connection.setReadTimeout(readTimeout);
恢復(fù)下載
考慮到internet連接的不確定性,失敗時(shí)我們可以重新下載文件,但不是再次從字節(jié)0位置下載文件。
讓我們重寫前面的第一個(gè)示例,以添加這個(gè)功能。
我們首先要知道的是,我們可以使用HTTP HEAD方法從給定URL讀取文件的大小,而無需實(shí)際下載它:
URL url = new URL(FILE_URL); HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection(); httpConnection.setRequestMethod("HEAD"); long removeFileSize = httpConnection.getContentLengthLong();
現(xiàn)在我們有了文件內(nèi)容的總大小,可以檢查文件是否已下載了部分內(nèi)容。如果是,我們將繼續(xù)從磁盤上記錄的最后一個(gè)字節(jié)開始下載:
long existingFileSize = outputFile.length(); if (existingFileSize < fileLength) { httpFileConnection.setRequestProperty( "Range", "bytes=" + existingFileSize + "-" + fileLength ); }
上述代碼我們配置了URLConnection以在一定范圍內(nèi)請(qǐng)求文件內(nèi)容。范圍將從最后下載的字節(jié)位置開始,并以遠(yuǎn)程文件大小的字節(jié)長度為結(jié)束。
使用范圍HEAD標(biāo)識(shí)的另一種常見方法是通過設(shè)置不同的字節(jié)范圍以塊形式下載文件。例如,要下載2KB文件,我們可以使用范圍0 - 1024和1024 - 2048。
與前節(jié)代碼稍微不同的是設(shè)置FileOutputStream 方法append參數(shù)為true:
OutputStream os = new FileOutputStream(FILE_NAME, true);
在我們做了這個(gè)更改之后,其余的代碼與我們前面看到的代碼一樣。
總結(jié)
在本文中,我們已經(jīng)看到Java中從URL下載文件的幾種實(shí)現(xiàn)方式。
最常見的實(shí)現(xiàn)是在執(zhí)行讀/寫操作時(shí)使用緩沖區(qū)。這個(gè)實(shí)現(xiàn)即使對(duì)于大文件也是安全的,因?yàn)槲覀儧]有將整個(gè)文件加載到內(nèi)存中。
我們還了解了如何使用Java NIO通道實(shí)現(xiàn)零拷貝下載。這很有用,因?yàn)樗钚』嗽谧x取和寫入字節(jié)時(shí)執(zhí)行的上下文切換的次數(shù),并且通過使用直接緩沖區(qū)字節(jié)不會(huì)加載到應(yīng)用程序內(nèi)存中。另外,由于下載文件通常是通過HTTP完成的,我們也說明如何使用AsyncHttpClient庫實(shí)現(xiàn)這一點(diǎn)。
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot集成Hadoop對(duì)HDFS的文件操作方法
這篇文章主要介紹了SpringBoot集成Hadoop對(duì)HDFS的文件操作方法,本文給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-07-07Springboot2 session設(shè)置超時(shí)時(shí)間無效的解決
這篇文章主要介紹了Springboot2 session設(shè)置超時(shí)時(shí)間無效的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07springBoot接入阿里云oss的實(shí)現(xiàn)步驟
這篇文章主要介紹了springBoot接入阿里云oss的實(shí)現(xiàn)步驟,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01詳解OAuth2 Token 一定要放在請(qǐng)求頭中嗎
這篇文章主要介紹了詳解OAuth2 Token 一定要放在請(qǐng)求頭中嗎,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07Java Listener監(jiān)聽器使用規(guī)范詳細(xì)介紹
監(jiān)聽器是一個(gè)專門用于對(duì)其他對(duì)象身上發(fā)生的事件或狀態(tài)改變進(jìn)行監(jiān)聽和相應(yīng)處理的對(duì)象,當(dāng)被監(jiān)視的對(duì)象發(fā)生情況時(shí),立即采取相應(yīng)的行動(dòng)。監(jiān)聽器其實(shí)就是一個(gè)實(shí)現(xiàn)特定接口的普通java程序,這個(gè)程序?qū)iT用于監(jiān)聽另一個(gè)java對(duì)象的方法調(diào)用或?qū)傩愿淖?/div> 2023-01-01最新評(píng)論