Java开发笔记(一百一十三)HttpClient实现下载与上传
前面介绍了通过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开发笔记(序)章节目录》
上一篇: AMR开发学习底层库函数寄存器
下一篇: ASP.NET中的请求验证