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再去下载。
推荐文章:
