1. 需求描述
前端通過(guò)正則識(shí)別出音頻文件URL傳給后端葫盼,后端打成zip文件給前端下載,需要考慮穩(wěn)定性和下載速度瓦灶。
2. 實(shí)現(xiàn)一:直接通過(guò)流讀取壓縮和返回前端
直接通過(guò)流接受并轉(zhuǎn)為 zipOutStream 流寫(xiě)到 response 里給前端鸠删。
/**
* @param downloadFilename 下載壓縮文件的名稱(chēng)
* @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的返回對(duì)象是文件流
response.setContentType("application/octet-stream");
// 設(shè)置在下載框默認(rèn)顯示的文件名
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: 轉(zhuǎn)為壓縮流
*/
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);
// 因?yàn)?URL 為 OSS 地址,故此處也可以直接從通過(guò) OSS API 獲取要下載的內(nèi)容(走內(nèi)網(wǎng)的話(huà)其兩種方式的效率差不多)
/*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();
// 注意寫(xiě)完一個(gè)文件倚搬,需要關(guān)閉這個(gè)文件冶共,再寫(xiě)下一個(gè)
zos.closeEntry();
zos.flush();
return false;
}
3. 實(shí)現(xiàn)二:先上傳 OSS,把返回的壓縮文件地址給前端
中間不需要落地文件每界,直接通過(guò)流寫(xiě)入 OSS 壓縮文件捅僵,返回oss地址給前端下載,后續(xù)通過(guò)定時(shí)任務(wù)把生成的oss 壓縮文件刪除眨层,節(jié)約oss空間資源
/**
* @param downloadFilename 下載壓縮文件的名稱(chēng)
* @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隊(duì)列等待定時(shí)任務(wù)刪除
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: 文件名重復(fù)問(wèn)題
* @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())) {
// 試題重復(fù)庙楚,通過(guò)增加時(shí)間戳來(lái)去重
download.setQueId(download.getQueId() + "-" + UUID.randomUUID().toString().replace("-", ""));
}
}
}
/**
* @description: 根據(jù)文件路徑獲取文件的擴(kuò)展名
* @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. 總結(jié)
第二種方式為更優(yōu)的選擇,oss通過(guò)內(nèi)網(wǎng)上傳效率很高趴樱,節(jié)省了流給前端傳輸?shù)臅r(shí)間馒闷,并且穩(wěn)定性也更好。其次叁征,下載壓力從服務(wù)器端移到了阿里云oss纳账,降低了服務(wù)器的壓力。
其實(shí)還可以通過(guò)隊(duì)列異步處理下載壓縮請(qǐng)求捺疼。前端請(qǐng)求下載傳url集合疏虫,后端放到隊(duì)列之后直接返回成功(每一次下載任務(wù)生成唯一ID返給前端)。然后后端隊(duì)列任務(wù)異步處理啤呼,前端可以輪詢(xún)獲取拿著唯一ID獲取處理成功返回的URL再去下載卧秘。