欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java开发笔记(一百一十三)HttpClient实现下载与上传

程序员文章站 2022-06-28 20:47:12
前面介绍了通过HttpClient实现HTTP接口的GET方式调用和POST方式调用,那么文件下载与文件上传又该如何操作呢?其实在HttpClient看来,文件下载属于特殊的GET调用,只不过应答报文由字符串形式变成了文件形式;同样文件上传属于特殊的POST调用,只不过请求报文也由字符串形式变成了文 ......

前面介绍了通过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开发笔记(序)章节目录