前面介紹了經過HttpClient實現HTTP接口的GET方式調用和POST方式調用,那麼文件下載與文件上傳又該如何操做呢?其實在HttpClient看來,文件下載屬於特殊的GET調用,只不過應答報文由字符串形式變成了文件形式;一樣文件上傳屬於特殊的POST調用,只不過請求報文也由字符串形式變成了文件形式。那麼文件下載與普通的GET調用相比,在代碼上的區別僅僅是發送請求send方法的第二個參數,以前演示普通GET調用的時候,send方法第二個輸入參數爲BodyHandlers.ofString(),具體調用代碼以下所示:html
// 客戶端傳遞請求信息,且返回字符串形式的應答報文 HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
上面代碼裏的BodyHandlers名叫報文體處理器,它會將服務端返回的應答數據轉換爲指定形式,好比調用ofString方法表示自動把應答數據轉成字符串。除了字符串,BodyHandlers還支持把應答數據轉爲其它格式,它支持的轉換格式及其設置方法說明以下:
ofString:把應答數據轉換爲字符串。
ofByteArray:把應答數據轉換爲字節數組。
ofFile:把應答數據轉換爲文件(Path類型)。
ofInputStream:把應答數據轉換爲輸入流。
ofLines:把應答數據轉換爲分行的字符串流(Stream<String>類型)。
就文件下載而言,無疑使用ofFile方法最合適,由於該方法可將應答數據保存到本地文件,省去了繁瑣的I/O操做。因而對普通的GET調用代碼稍加改造,就變成了如下的文件下載代碼:apache
// 從指定url下載文件到本地(同步方式) private static void testSyncDownload(String path, String downloadUrl) { // 從下載地址中獲取文件名 String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/")); // 建立默認的HTTP客戶端對象 HttpClient client = HttpClient.newHttpClient(); // 建立默認的HTTP請求對象(默認GET調用) HttpRequest request = HttpRequest.newBuilder(URI.create(downloadUrl)).build(); try { // 客戶端傳遞請求信息,且返回文件形式的應答報文 HttpResponse<Path> response = client.send(request, BodyHandlers.ofFile(Paths.get(path + fileName))); // 獲取應答的全部頭部屬性 HttpHeaders headers = response.headers(); // 打印HTTP下載的應答內容長度、內容類型、編碼方式 System.out.println( String.format("應答內容長度=%s, 內容類型=%s, 編碼方式=%s", headers.firstValue("Content-Length").orElse(null), headers.firstValue("Content-Type").orElse(null), headers.firstValue("Content-Encoding").orElse(null)) ); // 打印HTTP下載的應答狀態碼和應答報文 System.out.println( String.format("應答狀態碼=%d, 文件路徑=%s", response.statusCode(), response.body().toString()) ); } catch (Exception e) { e.printStackTrace(); } }
而後在外部調用以上的testSyncDownload方法,準備下載某張網絡圖片,圖片下載的調用代碼以下:數組
testSyncDownload("D:/", "https://img-blog.csdnimg.cn/2018112123554364.png");
運行以上的圖片下載代碼,觀察到如下的下載日誌,可見不費吹灰之力便獲得下載好的圖片文件。網絡
應答內容長度=123109, 內容類型=image/png, 編碼方式=null 應答狀態碼=200, 文件路徑=D:\2018112123554364.png
因爲網絡文件可能很大,下載過程也較耗時,所以文件下載操做每每須要另起線程處理。假若採起傳統的HttpURLConnection+Thread組合,對初學者而言宛如天書,敲起鍵盤不禁得戰戰兢兢。現在有了HttpClient,它自己支持異步方式的調用,所謂異步指的就是開分線程處理,主要事務在主線程中運行,耗時任務在分線程中運行,兩條任務線交錯並行,步伐相異故而稱之爲「異步」。相對應的,假若主要事務與耗時任務都在主線程當中運行,則必然存在前後次序關係,如此方能保持一致的步調,故而此時可稱做「同步」。多線程
HttpClient客戶端的send方法默認採起同步方式,一直等到HTTP調用結束才能繼續執行後面的代碼,它還有另外一個異步的請求方法名叫sendAsync,調用該方法後返回的是進行中任務對象CompletableFuture。這個進行中任務CompletableFuture,相似於多線程裏面的將來任務FutureTask,它們都表示一個正在運行的異步任務,調用cancel方法能夠中途取消該任務,調用isDone方法能夠判斷該任務是否已經執行完畢,而調用get方法能夠獲取該任務的執行結果。經過CompletableFuture的協助,HttpClient得以從容實如今分線程中運行的異步文件傳輸,須要開發者完成的編碼工做僅僅是把原來的send方法改爲sendAsync方法,就像如下代碼示範的那樣:異步
// 異步方式調用。sendAsync返回值類型爲CompletableFuture<HttpResponse<T>> CompletableFuture<Path> result = client // 客戶端發送異步請求,且返回文件形式的應答報文 .sendAsync(request, BodyHandlers.ofFile(Paths.get(path + fileName))) // 把CompletableFuture<HttpResponse<T>>類型映射爲CompletableFuture<Path>類型 .thenApply(HttpResponse::body); // 打印下載完的本地文件路徑 System.out.println("下載完的本地文件路徑="+result.get().toString());
運行更改後的文件下載代碼,觀察到以下正常輸出的下載日誌:工具
下載完的本地文件路徑=D:\2018112123554364.png
使用HttpClient實現文件的上傳功能則略微複雜,緣於Java官方還沒有提供分段數據的轉換工具,所以還得藉助於Apache的HttpEntity實體類。這樣一來又要引入第三方的兩個jar包,分別是httpcore-***.jar和httpmime-***.jar,它倆個原本就是Apache推出的HttpClient開發包。提及來真是使人啼笑皆非,Java本身搞了一套HttpClient,結果功能不夠完備,到頭來又得撿回Apache的衣裳來狗尾續貂。這個問題只好留待Java的後續版本予以改進了,無論怎樣,當前的HttpClient稍加修補也能知足文件上傳的要求,下面是實現文件上傳的完整代碼例子:ui
// 把本地文件上傳給指定url(同步方式) private static void testSyncUpload(String filename, String uploadUrl) { // 建立默認的HTTP客戶端對象 HttpClient client = HttpClient.newHttpClient(); // 官方的HttpClient並無提供相似WebClient那種現成的BodyInserters.fromMultipartData方法,所以這裏須要本身轉換 // Apache推出的HttpClient的下載頁面是 http://hc.apache.org/downloads.cgi // 根據指定文件建立二進制形式的文件體對象 FileBody fileBody = new FileBody(new File(filename), ContentType.DEFAULT_BINARY); String boundary = "WUm4580jbtwfJhNp7zi1djFEO3wNNm"; // 邊界字符串 // 建立用於網絡傳輸的HTTP實體對象 HttpEntity entity = MultipartEntityBuilder.create() // 分段實體 .addPart("file", fileBody) // 添加文件體 .setBoundary(boundary) // 設置邊界字符串 .build(); // 建立字節數組輸出流 try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { entity.writeTo(baos); // 把HTTP實體對象寫入字節數組輸出流 // 建立一個自定義的HTTP請求對象 HttpRequest request = HttpRequest.newBuilder(URI.create(uploadUrl)) // 待上傳的url地址 // 設置頭部參數,要求分段傳輸,而且各段之間以邊界字符串隔開 .header("Content-Type", "multipart/form-data; boundary=" + boundary) // 調用方式爲POST,且請求報文爲字節數組 .POST(BodyPublishers.ofByteArray(baos.toByteArray())).build(); // 客戶端傳遞請求信息,且返回字符串形式的應答報文 HttpResponse<String> response = client.send(request, BodyHandlers.ofString()); // 打印HTTP上傳的應答狀態碼和應答報文 System.out.println( String.format("應答狀態碼=%d, 應答報文=%s", response.statusCode(), response.body()) ); } catch (Exception e) { e.printStackTrace(); } }
接着由外部調用上面的testSyncUpload方法,這裏訪問的是本機的上傳服務,具體代碼以下所示:編碼
testSyncUpload("E:/bliss.jpg", "http://localhost:8080/NetServer/uploadServlet");
運行上面的文件上傳代碼,從如下的上傳日誌可知成功完成了上傳操做。url
應答狀態碼=200, 應答報文=文件上傳成功,文件大小爲1912K
與文件下載同樣,HttpClient的文件上傳也支持異步方式,仍然是把請求的send方法改成sendAsync方法便可,修改後的代碼片斷以下所示:
// 異步方式調用。sendAsync返回值類型爲CompletableFuture<HttpResponse<T>> CompletableFuture<String> result = client // 客戶端發送異步請求,且返回字符串形式的應答報文 .sendAsync(request, BodyHandlers.ofString()) // 把CompletableFuture<HttpResponse<T>>類型映射爲CompletableFuture<Path>類型 .thenApply(HttpResponse::body); // 打印上傳完的應答報文內容 System.out.println("文件上傳的應答報文="+result.get());
運行更改後的文件上傳代碼,觀察到以下正常輸出的上傳日誌:
文件上傳的應答報文=文件上傳成功,文件大小爲1912K
更多Java技術文章參見《Java開發筆記(序)章節目錄》