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

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

相关文章
相关标签/搜索