1. 需求描述

前端通过正则识别出音频文件URL传给后端,后端打成zip文件给前端下载,需要考虑稳定性和下载速度。

2. 实现一:直接通过流读取压缩和返回前端

直接通过流接受并转为 zipOutStream 流写到 response 里给前端。

  1. /**
  2. * @param downloadFilename 下载压缩文件的名称
  3. * @param downloads 要下载的音频集合
  4. * @param response response
  5. * @description: 压缩包文件流下载
  6. */
  7. public void downloadZip(String downloadFilename, List<AudioDownloadDto> downloads, HttpServletResponse response) {
  8. if (ListUtils.isEmpty(downloads)) {
  9. return;
  10. }
  11. dealRepeatFileName(downloads);
  12. ZipOutputStream zos = null;
  13. try {
  14. downloadFilename = URLEncoder.encode(downloadFilename, "UTF-8");
  15. // 指明response的返回对象是文件流
  16. response.setContentType("application/octet-stream");
  17. // 设置在下载框默认显示的文件名
  18. response.setHeader("Content-Disposition", "attachment;filename=" + downloadFilename);
  19. zos = new ZipOutputStream(response.getOutputStream());
  20. for (AudioDownloadDto download : downloads) {
  21. packageZipOutPutStream(zos, download);
  22. }
  23. zos.finish();
  24. } catch (Exception e) {
  25. log.error("下载音频压缩包失败 :{}", e.getMessage());
  26. throw new ServiceException("下载音频压缩包失败");
  27. } finally {
  28. try {
  29. if (zos != null) {
  30. zos.close();
  31. }
  32. } catch (IOException e) {
  33. log.error("ZipOutputStream close fail :{}", e.getMessage());
  34. throw new ServiceException("ZipOutputStream close fail");
  35. }
  36. }
  37. }

packageZipOutPutStream 方法:

  1. /**
  2. * @param zos
  3. * @param download
  4. * @return
  5. * @throws IOException
  6. * @description: 转为压缩流
  7. */
  8. private boolean packageZipOutPutStream(ZipOutputStream zos, AudioDownloadDto download) throws IOException {
  9. String fileUrl = download.getUrl();
  10. ZipEntry zipEntry = new ZipEntry(getFileName(download.getQueId(), fileUrl));
  11. zos.putNextEntry(zipEntry);
  12. InputStream fis = FileUtils.getInputStreamByFileUrl(fileUrl);
  13. // 因为 URL 为 OSS 地址,故此处也可以直接从通过 OSS API 获取要下载的内容(走内网的话其两种方式的效率差不多)
  14. /*OSSClient ossClient= ossUtil.getOSSClient();
  15. OSSObject ossObject = ossClient.getObject(ossPropResource.getUserStorageOssBucketName(), StringUtils.substringAfterLast(fileUrl, ".com/"));
  16. InputStream fis = ossObject.getObjectContent();*/
  17. if (fis == null) {
  18. return true;
  19. }
  20. byte[] buffer = new byte[2048];
  21. int r;
  22. while ((r = fis.read(buffer)) != -1) {
  23. zos.write(buffer, 0, r);
  24. }
  25. fis.close();
  26. // 注意写完一个文件,需要关闭这个文件,再写下一个
  27. zos.closeEntry();
  28. zos.flush();
  29. return false;
  30. }

3. 实现二:先上传 OSS,把返回的压缩文件地址给前端

中间不需要落地文件,直接通过流写入 OSS 压缩文件,返回oss地址给前端下载,后续通过定时任务把生成的oss 压缩文件删除,节约oss空间资源

  1. /**
  2. * @param downloadFilename 下载压缩文件的名称
  3. * @param downloads 要下载的音频集合
  4. * @return
  5. * @description: 获取压缩包oss地址下载
  6. */
  7. public String queryDownloadZipOSSUrl(String downloadFilename, List<AudioDownloadDto> downloads) {
  8. if (ListUtils.isEmpty(downloads)) {
  9. return null;
  10. }
  11. dealRepeatFileName(downloads);
  12. ZipOutputStream zos = null;
  13. ByteArrayOutputStream bos = new ByteArrayOutputStream();
  14. try {
  15. zos = new ZipOutputStream(bos);
  16. for (AudioDownloadDto download : downloads) {
  17. packageZipOutPutStream(zos, download);
  18. }
  19. zos.finish();
  20. InputStream inputStream = new ByteArrayInputStream(bos.toByteArray());
  21. String key = UUID.randomUUID().toString().replace("-", "") + Constants.FileSuffix.ZIP;
  22. ObjectMetadata metadata = new ObjectMetadata();
  23. metadata.setContentType("application/octet-stream");
  24. metadata.setContentDisposition("attachment;filename=" + downloadFilename);
  25. ossService.pubObjectStream(ossPropResource.getTiKuBucketName(), SUB_BUCKET_NAME + key, inputStream, metadata);
  26. // 放入redis队列等待定时任务删除
  27. redisDao.putListObject(Constants.Cache.AUDIO_ZIP_OSS_KEY_LIST, key);
  28. return ossPropResource.getTiKuBucketEndpoint() + "/" + SUB_BUCKET_NAME + key;
  29. } catch (Exception e) {
  30. log.error("获取音频压缩文件的OSS地址失败 :{}", e.getMessage());
  31. throw new ServiceException("获取音频压缩文件的OSS地址失败");
  32. } finally {
  33. try {
  34. if (zos != null) {
  35. zos.close();
  36. }
  37. } catch (IOException e) {
  38. log.error("ZipOutputStream close fail :{}", e.getMessage());
  39. throw new ServiceException("ZipOutputStream close fail");
  40. }
  41. }
  42. }

涉及到的一些方法:

  1. /**
  2. * @description: 文件名重复问题
  3. * @author: zcq
  4. * @date: 2020/11/27 4:44 下午
  5. */
  6. public static void dealRepeatFileName(List<AudioDownloadDto> downloads) {
  7. Set<String> queIdSet = new HashSet();
  8. for (AudioDownloadDto download : downloads) {
  9. if (!queIdSet.add(download.getQueId())) {
  10. // 试题重复,通过增加时间戳来去重
  11. download.setQueId(download.getQueId() + "-" + UUID.randomUUID().toString().replace("-", ""));
  12. }
  13. }
  14. }
  15. /**
  16. * @description: 根据文件路径获取文件的扩展名
  17. * @author: zcq
  18. * @date: 2020/11/26 6:18 下午
  19. */
  20. private static String getFileName(String queId, String url) {
  21. return queId + Constants.Decollator.POINT_DECOLLATOR + StringUtils.substringAfterLast(url, Constants.Decollator.POINT_DECOLLATOR);
  22. }
  23. /**
  24. * @description: 删除OSS文件
  25. * @author: zcq
  26. * @date: 2020/12/3 3:33 下午
  27. */
  28. public void delOssFile(final String ossKey) {
  29. ossService.deleteObject(ossPropResource.getTiKuBucketName(), SUB_BUCKET_NAME + ossKey);
  30. }

4. 总结

第二种方式为更优的选择,oss通过内网上传效率很高,节省了流给前端传输的时间,并且稳定性也更好。其次,下载压力从服务器端移到了阿里云oss,降低了服务器的压力。

其实还可以通过队列异步处理下载压缩请求。前端请求下载传url集合,后端放到队列之后直接返回成功(每一次下载任务生成唯一ID返给前端)。然后后端队列任务异步处理,前端可以轮询获取拿着唯一ID获取处理成功返回的URL再去下载。

推荐文章: