持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第22天,点击查看活动详情
背景
最近项目中有个需求通过浏览器实现远程文件下载功能,关于浏览器下载实现方案有两种:
- 1.下载远程文件到本地服务器,浏览器在从本地服务器下载。
- 2.采用文件流,浏览器直接下载远程文件。
具体实现
方案1:下载远程文件到本地
核心类实现
@GetMapping("downloadFileToLocal")
public void downloadFileToLocal(String url, String targetDir,
Map<String, String> params)
{
//当前时间
Instant startTime = Instant.now();
String completeUrl = addGetQueryParam(url, params);
logger.info("download file to local");
try
{
String path = createDownloadDir(url, targetDir);
logger.info("donwload file dest path:{}",path);
ResponseEntity<byte[]> rsp = restTemplate.getForEntity(completeUrl, byte[].class);
Files.write(Paths.get(path), Objects.requireNonNull(rsp.getBody(),
"未获取到下载文件"));
}
catch (IOException e)
{
logger.error("下载文件失败:", e);
}
logger.info("下载文件完成总耗时:{}",
ChronoUnit.MILLIS.between(startTime, Instant.now()));
}
相关工具类
// 构造参数
private String addGetQueryParam(String url, Map<String, String> params)
{
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder
.fromHttpUrl(url);
if (!CollectionUtils.isEmpty(params))
{
for (Map.Entry<String, ?> varEntry : params.entrySet())
{
uriComponentsBuilder.queryParam(varEntry.getKey(),
varEntry.getValue());
}
}
return uriComponentsBuilder.build().encode().toString();
}
//创建或获取下载文件夹的路径
public String createDownloadDir(String url, String targetDir)
throws IOException
{
String filename = url.substring(url.lastIndexOf("/") + 1);
int i = 0;
if ((i = url.indexOf("?")) != -1)
{
filename = filename.substring(0, i);
}
if (!Files.exists(Paths.get(targetDir)))
{
Files.createDirectories(Paths.get(targetDir));
}
String destPath= targetDir.endsWith("/") ? targetDir + filename : targetDir + "/"
+ filename;
File file =new File(destPath);
if(file.exists())
{
logger.error("file path:{} is exist",file.getPath());
file.delete();
}
return destPath;
}
说明:实现文件下载的方式有多种,本文采用的是RestTemplate的方式进行远程文件下载,调用接口返回文件字节流下载到本地,如果对于RestTemplate不熟悉的朋友,可以参考我之前的文章。
前端实现
前端实现比较简单,传入相关参数提交form表单即可。
function downloadLogs(oid)
{
var frm = $("#logExportFrom");
$("#oid_exp").val(oid);
var url = xxx";
frm.attr("action", url);
frm.submit();
}
小结
- 1.此方案需要先将文件下载到本地,然后浏览器在直接下载,经过多次请求,时间消耗长。
- 2.文件存储在本地占用了本地硬盘空间,浪费你硬盘资源。
- 3.如果文件比较大或者文件下载并发量比较大,容易造成内存的大量占用,从而降低应用的运行效率,此方案适合于下载文件大小比较小的场景。
方案2:浏览器直接下载
核心类实现
@GetMapping("downloadBigFile")
public void downloadBigFile(HttpServletResponse response)
{
logger.info("Down load file start");
try
{
//当前时间
Instant now = Instant.now();
String completeUrl = "http://xxx.com/image/test.png";
try
{
// 定义请求头的接收类型
RequestCallback requestCallback = request -> request.getHeaders()
.setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM,
MediaType.ALL));
// 针对大文件,采用的是 ResponseExtractor方式
ResponseExtractor<ResponseEntity<Void>> responseExtractor = new ResponseExtractor<ResponseEntity<Void>>()
{
@Override
public ResponseEntity<Void> extractData(ClientHttpResponse clientHttpResponse) throws IOException {
String fileName=getFileName(completeUrl);
response.setContentType("multipart/form-data");
response.setHeader("Content-Disposition", "attachment; filename=\""
+ fileName + "\"");
InputStream ins = clientHttpResponse.getBody();
//拷贝文件流
StreamUtils.copy(ins, response.getOutputStream());
return null;
}
};
restTemplate.execute(completeUrl, HttpMethod.GET,
requestCallback, responseExtractor);
}
catch (Exception e)
{
logger.error("[下载文件] 写入失败:", e);
}
logger.info("[下载文件] 完成,耗时:{}",
ChronoUnit.MILLIS.between(now, Instant.now()));
}
catch (Exception e)
{
logger.error("Down load file Error", e);
}
}
特别说明:
- 1.设置了请求头APPLICATION_OCTET_STREAM,表示以流的形式进行数据加载
- 2.采用RequestCallback和文件加流拷贝的方式,不是将全部文件的内容加载到磁盘文件中,而是每次只加载一部分内容,提升文件下载的效率,此方案适合大文件下载。
总结
本文讲解了SpringBoot 采用RestTemplate实现文件下载,实现的方案有几种,我们需要结合具体的业务场景进行分析,选择合适的方案。