1. 需求描述
前端通过正则识别出音频文件URL传给后端,后端打成zip文件给前端下载,需要考虑稳定性和下载速度。
2. 实现一:直接通过流读取压缩和返回前端
直接通过流接受并转为 zipOutStream 流写到 response 里给前端。
/**
* @param downloadFilename 下载压缩文件的名称
* @param downloads 要下载的音频集合
* @param response response
* @description: 压缩包文件流下载
*/
public void downloadZip(String downloadFilename, List<AudioDownloadDto> downloads, HttpServletResponse response) {
if (ListUtils.isEmpty(downloads)) {
return;
}
dealRepeatFileName(downloads);
ZipOutputStream zos = null;
try {
downloadFilename = URLEncoder.encode(downloadFilename, "UTF-8");
// 指明response的返回对象是文件流
response.setContentType("application/octet-stream");
// 设置在下载框默认显示的文件名
response.setHeader("Content-Disposition", "attachment;filename=" + downloadFilename);
zos = new ZipOutputStream(response.getOutputStream());
for (AudioDownloadDto download : downloads) {
packageZipOutPutStream(zos, download);
}
zos.finish();
} catch (Exception e) {
log.error("下载音频压缩包失败 :{}", e.getMessage());
throw new ServiceException("下载音频压缩包失败");
} finally {
try {
if (zos != null) {
zos.close();
}
} catch (IOException e) {
log.error("ZipOutputStream close fail :{}", e.getMessage());
throw new ServiceException("ZipOutputStream close fail");
}
}
}
packageZipOutPutStream
方法:
/**
* @param zos
* @param download
* @return
* @throws IOException
* @description: 转为压缩流
*/
private boolean packageZipOutPutStream(ZipOutputStream zos, AudioDownloadDto download) throws IOException {
String fileUrl = download.getUrl();
ZipEntry zipEntry = new ZipEntry(getFileName(download.getQueId(), fileUrl));
zos.putNextEntry(zipEntry);
InputStream fis = FileUtils.getInputStreamByFileUrl(fileUrl);
// 因为 URL 为 OSS 地址,故此处也可以直接从通过 OSS API 获取要下载的内容(走内网的话其两种方式的效率差不多)
/*OSSClient ossClient= ossUtil.getOSSClient();
OSSObject ossObject = ossClient.getObject(ossPropResource.getUserStorageOssBucketName(), StringUtils.substringAfterLast(fileUrl, ".com/"));
InputStream fis = ossObject.getObjectContent();*/
if (fis == null) {
return true;
}
byte[] buffer = new byte[2048];
int r;
while ((r = fis.read(buffer)) != -1) {
zos.write(buffer, 0, r);
}
fis.close();
// 注意写完一个文件,需要关闭这个文件,再写下一个
zos.closeEntry();
zos.flush();
return false;
}
3. 实现二:先上传 OSS,把返回的压缩文件地址给前端
中间不需要落地文件,直接通过流写入 OSS 压缩文件,返回oss地址给前端下载,后续通过定时任务把生成的oss 压缩文件删除,节约oss空间资源
/**
* @param downloadFilename 下载压缩文件的名称
* @param downloads 要下载的音频集合
* @return
* @description: 获取压缩包oss地址下载
*/
public String queryDownloadZipOSSUrl(String downloadFilename, List<AudioDownloadDto> downloads) {
if (ListUtils.isEmpty(downloads)) {
return null;
}
dealRepeatFileName(downloads);
ZipOutputStream zos = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
zos = new ZipOutputStream(bos);
for (AudioDownloadDto download : downloads) {
packageZipOutPutStream(zos, download);
}
zos.finish();
InputStream inputStream = new ByteArrayInputStream(bos.toByteArray());
String key = UUID.randomUUID().toString().replace("-", "") + Constants.FileSuffix.ZIP;
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType("application/octet-stream");
metadata.setContentDisposition("attachment;filename=" + downloadFilename);
ossService.pubObjectStream(ossPropResource.getTiKuBucketName(), SUB_BUCKET_NAME + key, inputStream, metadata);
// 放入redis队列等待定时任务删除
redisDao.putListObject(Constants.Cache.AUDIO_ZIP_OSS_KEY_LIST, key);
return ossPropResource.getTiKuBucketEndpoint() + "/" + SUB_BUCKET_NAME + key;
} catch (Exception e) {
log.error("获取音频压缩文件的OSS地址失败 :{}", e.getMessage());
throw new ServiceException("获取音频压缩文件的OSS地址失败");
} finally {
try {
if (zos != null) {
zos.close();
}
} catch (IOException e) {
log.error("ZipOutputStream close fail :{}", e.getMessage());
throw new ServiceException("ZipOutputStream close fail");
}
}
}
涉及到的一些方法:
/**
* @description: 文件名重复问题
* @author: zcq
* @date: 2020/11/27 4:44 下午
*/
public static void dealRepeatFileName(List<AudioDownloadDto> downloads) {
Set<String> queIdSet = new HashSet();
for (AudioDownloadDto download : downloads) {
if (!queIdSet.add(download.getQueId())) {
// 试题重复,通过增加时间戳来去重
download.setQueId(download.getQueId() + "-" + UUID.randomUUID().toString().replace("-", ""));
}
}
}
/**
* @description: 根据文件路径获取文件的扩展名
* @author: zcq
* @date: 2020/11/26 6:18 下午
*/
private static String getFileName(String queId, String url) {
return queId + Constants.Decollator.POINT_DECOLLATOR + StringUtils.substringAfterLast(url, Constants.Decollator.POINT_DECOLLATOR);
}
/**
* @description: 删除OSS文件
* @author: zcq
* @date: 2020/12/3 3:33 下午
*/
public void delOssFile(final String ossKey) {
ossService.deleteObject(ossPropResource.getTiKuBucketName(), SUB_BUCKET_NAME + ossKey);
}
4. 总结
第二种方式为更优的选择,oss通过内网上传效率很高,节省了流给前端传输的时间,并且稳定性也更好。其次,下载压力从服务器端移到了阿里云oss,降低了服务器的压力。
其实还可以通过队列异步处理下载压缩请求。前端请求下载传url集合,后端放到队列之后直接返回成功(每一次下载任务生成唯一ID返给前端)。然后后端队列任务异步处理,前端可以轮询获取拿着唯一ID获取处理成功返回的URL再去下载。