前面介绍了通过HttpClient实现HTTP接口的GET方式调用和POST方式调用,那么文件下载与文件上传又该如何操作呢?其实在HttpClient看来,文件下载属于特殊的GET调用,只不过应答报文由字符串形式变成了文件形式;同样文件上传属于特殊的POST调用,只不过请求报文也由字符串形式变成了文件形式。那么文件下载与普通的GET调用相比,在代码上的区别仅仅是发送请求send方法的第二个参数,之前演示普通GET调用的时候,send方法第二个输入参数为BodyHandlers.ofString(),具体调用代码如下所示:

			// 客户端传递请求信息,且返回字符串形式的应答报文
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());

上面代码里的BodyHandlers名叫报文体处理器,它会将服务端返回的应答数据转换为指定形式,比如调用ofString方法表示自动把应答数据转成字符串。除了字符串,BodyHandlers还支持把应答数据转为其它格式,它支持的转换格式及其设置方法说明如下:
ofString:把应答数据转换为字符串。
ofByteArray:把应答数据转换为字节数组。
ofFile:把应答数据转换为文件(Path类型)。
ofInputStream:把应答数据转换为输入流。
ofLines:把应答数据转换为分行的字符串流(Stream<String>类型)。
就文件下载而言,无疑使用ofFile方法最合适,因为该方法可将应答数据保存到本地文件,省去了繁琐的I/O操作。于是对普通的GET调用代码稍加改造,就变成了以下的文件下载代码:

	// 从指定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稍加修补也能满足文件上传的要求,下面是实现文件上传的完整代码例子:

	// 把本地文件上传给指定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");

运行上面的文件上传代码,从以下的上传日志可知成功完成了上传操作。

应答状态码=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开发笔记(序)章节目录

Java开发笔记(一百一十三)HttpClient实现下载与上传的更多相关文章

  1. Java开发笔记(八十三)利用注解技术检查空指针

    注解属于比较高级的Java开发技术,前面介绍的内置注解专用于编译器检查代码,另外一些注解则由各大框架定义与调用,像Web开发常见的Spring框架.Mybatis框架,Android开发常见的Butt ...

  2. Java开发笔记(二十三)数组工具Arrays

    数组作为一种组合形式的数据类型,必然要求提供一些处理数组的简便办法,包括数组比较.数组复制.数组排序等等.为此Java专门设计了Arrays工具,该工具包含了几个常用方法,方便程序员对数组进行加工操作 ...

  3. Java开发笔记(三十三)字符包装类型

    正如整型int有对应的包装整型Integer那样,字符型char也有对应的包装字符型Character.初始化字符包装变量也有三种方式,分别是:直接用等号赋值.调用包装类型的valueOf方法.使用关 ...

  4. Java开发笔记(四十三)更好用的本地日期时间

    话说Java一连设计了两套时间工具,分别是日期类型Date,以及日历类型Calendar,按理说用在编码开发中绰绰有余了.然而随着Java的日益广泛使用,人们还是发现了它们的种种弊端.且不说先天不良的 ...

  5. Java开发笔记(五十三)关键字final的用法

    前面介绍了多态的相关用法,可以看到一个子类从父类继承之后,便能假借父类的名义到处晃悠.这种机制在正常情况之下没啥问题,但有时为了预防意外发生,往往只接受当事人来处理,不希望它的儿子乃至孙子来瞎掺和.可 ...

  6. Java开发笔记(六十三)双冒号标记的方法引用

    前面介绍了如何自己定义函数式接口,本文接续函数式接口的实现原理,阐述它在数组处理中的实际应用.数组工具Arrays提供了sort方法用于数组元素排序,可是并未提供更丰富的数组加工操作,比如从某个字符串 ...

  7. Java开发笔记(七十三)常见的程序异常

    一个程序开发出来之后,无论是用户还是程序员,都希望它稳定地运行,然而程序毕竟是人写的,人无完人哪能不犯点错误呢?就算事先考虑得天衣无缝,揣着一笔巨款跑去岛国买了栋抗震性能良好的海边别墅,谁料人算不如天 ...

  8. Java开发笔记(序)章节目录

    现将本博客的Java学习文章整理成以下笔记目录,方便查阅. 第一章 初识JavaJava开发笔记(一)第一个Java程序Java开发笔记(二)Java工程的帝国区划Java开发笔记(三)Java帝国的 ...

  9. Java开发笔记(一百一十二)Java11新增的HttpClient

    前面介绍了基于HttpURLConnection的网络访问请求,包括GET方式调用接口.POST方式调用接口.下载网络文件.上传本地文件这四种HTTP操作.虽然通过HttpURLConnection能 ...

随机推荐

  1. 14-Flutter移动电商实战-ADBanner组件的编写

    拨打电话的功能在app里也很常见,比如一般的外卖app都会有这个才做.其实Flutter本身是没给我们提供拨打电话的能力的,那我们如何来拨打电话那? 1.编写店长电话模块 这个小伙伴们一定轻车熟路了, ...

  2. WinDbg常用命令系列---显示局部变量dv

    dv (Display Local Variables) dv命令显示当前作用域中所有局部变量的名称和值. dv [Flags] [Pattern] 参数: Flags显示其他信息.可以包括以下任何区 ...

  3. P5589 【小猪佩奇玩游戏】

    这题还是比较妙妙套路的,复杂度为\(O(log^2N)\),可以卡掉\(\sqrt n\)的做法 首先我们可以把原数列分成很多个集合,集合之间肯定是两两独立的,考虑分别计算答案 我们定义\(f_i\) ...

  4. 用户价值和RFM模型

    什么是用户价值? 用户价值就是对公司来说有用的地方,比如有的公司看中用户的消费能力,有的公司则看中用户的忠诚度 .各公司的业务目的不同,用户价值的体现自然也不同.这里主要说一下适用于电商的RFM模型. ...

  5. C#访问https地址实例

    程序访问的网址从http换成了https,安全性更高了,程序对网址的访问也要改一下 C#访问https地址实例如下 namespace ConsoleApp2 { public class Progr ...

  6. P2052 [NOI2011]道路修建——树形结构(水题,大佬勿进)

    P2052 [NOI2011]道路修建 这个题其实在dfs里面就可以把事干完的,(我一开始还拿出来求了一把)…… 一条边的贡献就是儿子的大小和n-siz[v]乘上边权: #include<cma ...

  7. [JLOI 2015]骗我呢

    传送门 Description 求给\(n*m\)的矩阵填数的方案数 满足: \[ 1\leq x_{i,j}\leq m \] \[ x_{i,j}<x_{i,j+1} \] \[ x_{i, ...

  8. MQTT 与 RocketMQ 的应用场景对比

    本文主要在什么是微消息队列 MQTT?的基础上介绍微消息队列 MQTT 和传统消息中间件的关联和区别,并针对实际应用场景下的产品选型给出建议. 背景信息 传统的消息中间件,例如消息队列 RocketM ...

  9. flask上传文件到指定路径

    flask上传文件到指定路径 项目结构如下: 首先是:视图函数uload_file.py,代码如下: #!/usr/bin/env python # -*- coding: utf-8 -*- fro ...

  10. k8s 传参给docker env command、args和dockerfile中的entrypoint、cmd之间的关系

    [k8s]args指令案例-彻底理解docker entrypoint     需求: 搞个镜像,可以运行java -jar xxx.jar包,xxx.jar包名称要用参数传 思路1: 打对应运行ja ...