大文件下载防内存溢出简单实现

摘要:利用StreamingResponseBody来防止内存溢出,实现大文件下载。
一、配置异步请求超时时间
 1 /**
 2  * @Description: 异步请求配置类
 3  * @Date: Created in 17:19 2025/1/5
 4  * @Author: Cenobitor
 5  * @Modified By:
 6  * @since 0.1.0
 7  */
 8 @Configuration
 9 @EnableWebMvc
10 public class WebConfig implements WebMvcConfigurer {
11 
12     @Override
13     public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
14         configurer.setTaskExecutor(taskExecutor());
15         // 超时时间设置为6000s
16         configurer.setDefaultTimeout(TimeUnit.SECONDS.toMillis(6000));
17     }
18 
19     private AsyncTaskExecutor taskExecutor() {
20         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
21         executor.setCorePoolSize(10); // 核心线程数
22         executor.setMaxPoolSize(20);  // 最大线程数
23         executor.setQueueCapacity(500); // 队列容量
24         executor.setThreadNamePrefix("Async-");
25         executor.initialize();
26         return executor;
27     }
28 }

二、本地、远程文件下载请求接口

 1 private static final int MAX_CONCURRENT_DOWNLOADS = 5; // 最大并发下载数
 2     private final Semaphore semaphore = new Semaphore(MAX_CONCURRENT_DOWNLOADS);
 3 
 4     @RequestMapping( "/downloadFile")
 5     public ResponseEntity<StreamingResponseBody> downloadFile() {
 6         long start = System.currentTimeMillis();
 7         try {
 8             Path filePath = Paths.get("E:\\macOS Catalina 10.15 正式版 19A583 macOShome.com.dmg");
 9             StreamingResponseBody responseBody = outputStream -> {
10                 try (InputStream inputStream = Files.newInputStream(filePath)) {
11                     StreamUtils.copy(inputStream, outputStream);
12                 }
13             };
14             HttpHeaders header = new HttpHeaders();
15             header.setContentDispositionFormData("attachment", "macOS.dmg");
16             header.setContentType(MediaType.APPLICATION_OCTET_STREAM);
17             return new ResponseEntity<>(responseBody,header, HttpStatus.OK);
18         } catch (Exception e) {
19             throw new RuntimeException(e);
20         } finally {
21             long end = System.currentTimeMillis();
22             System.out.println("downloadFile cost: " + (end - start) + "ms");
23         }
24     }
25 
26     @RequestMapping( "/downloadRemoteFile")
27     public ResponseEntity<StreamingResponseBody> downloadRemoteFile() {
28         long start = System.currentTimeMillis();
29         try {
30             StreamingResponseBody responseBody = outputStream -> {
31                 URL resourceUrl = new URL("https://cdn.mysql.com/Downloads/MySQL-9.1/mysql-9.1.0-winx64-debug-test.zip");
32                 try (InputStream inputStream = new UrlResource(resourceUrl).getInputStream()) {
33                     StreamUtils.copy(inputStream, outputStream);
34                 }
35             };
36             HttpHeaders header = new HttpHeaders();
37             header.setContentDispositionFormData("attachment", "mysql.zip");
38             header.setContentType(MediaType.APPLICATION_OCTET_STREAM);
39             return new ResponseEntity<>(responseBody,header, HttpStatus.OK);
40         } catch (Exception e) {
41             throw new RuntimeException(e);
42         } finally {
43             long end = System.currentTimeMillis();
44             System.out.println("downloadFile cost: " + (end - start) + "ms");
45         }
46     }
47 
48     @RequestMapping( "/downloadRemoteFileLimit")
49     public ResponseEntity<StreamingResponseBody> downloadRemoteFileLimit() {
50 
51         if (!semaphore.tryAcquire()) {
52             // 如果无法获取信号量,则返回503服务不可用状态
53             return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE);
54         }
55 
56         try {
57             StreamingResponseBody responseBody = outputStream -> {
58                 URL resourceUrl = new URL("https://cdn.mysql.com/Downloads/MySQL-9.1/mysql-9.1.0-winx64-debug-test.zip");
59                 try (InputStream inputStream = new UrlResource(resourceUrl).getInputStream()) {
60                     byte[] buffer = new byte[1024];
61                     int bytesRead;
62                     Instant lastWrite = Instant.now();
63                     long bytesPerSecond = 1024 * 100; // 例如,限制为100KB/s
64 
65                     while ((bytesRead = inputStream.read(buffer)) != -1) {
66                         outputStream.write(buffer, 0, bytesRead);
67 
68                         long now = Instant.now().toEpochMilli();
69                         long elapsed = now - lastWrite.toEpochMilli();
70                         long expectedDelay = (bytesRead * 1000L / bytesPerSecond) - elapsed;
71 
72                         if (expectedDelay > 0) {
73                             TimeUnit.MILLISECONDS.sleep(expectedDelay);
74                         }
75 
76                         lastWrite = Instant.now();
77                     }
78                 } catch (Exception e) {
79                     // 处理异常
80                     throw new RuntimeException("下载失败", e);
81                 }finally {
82                     semaphore.release(); // 释放信号量
83                 }
84             };
85 
86             HttpHeaders header = new HttpHeaders();
87             header.setContentDispositionFormData("attachment", "mysql.zip");
88             header.setContentType(MediaType.APPLICATION_OCTET_STREAM);
89             return new ResponseEntity<>(responseBody, header, HttpStatus.OK);
90         } catch (Exception e) {
91             // 处理其他可能的异常,并释放信号量(如果需要)
92             semaphore.release();
93             throw e;
94         }
95     }

三、前端页面代码

 1 <template>
 2   <div class="hello">
 3     <h1>{{ msg }}</h1>
 4     <p>
 5       For a guide and recipes on how to configure / customize this project,<br>
 6       check out the
 7       <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
 8     </p>
 9   </div>
10   <div class="download-wrap">
11     <el-button type="primary" icon="el-icon-download" size="sm>ll" @click="downloadFile">下载文件</el-button>
12     <el-button type="primary" icon="el-icon-download" size="sm>ll" @click="downloadRemoteFile">下载远程文件</el-button>
13     <el-button type="primary" icon="el-icon-download" size="sm>ll" @click="downloadRemoteFileLimit">下载远程文件限速</el-button>
14   </div>
15 </template>
16 
17 <script>
18 import axios from "axios";
19 
20 export default {
21   name: 'HelloWorld',
22   props: {
23     msg: String
24   },
25 
26   methods:{
27     downloadFile() {
28       console.log('下载文件开始');
29       const downloadUrl = 'http://localhost:8080/downloadFile'; // 文件下载的链接
30       window.open(downloadUrl);
31       console.log('下载文件完成');
32     },
33     downloadRemoteFile() {
34       console.log('下载文件开始');
35       const downloadUrl = 'http://localhost:8080/downloadRemoteFile'; // 文件下载的链接
36       window.open(downloadUrl);
37       console.log('下载文件完成');
38     },
39     downloadRemoteFileLimit() {
40       console.log('下载文件开始');
41       const downloadUrl = 'http://localhost:8080/downloadRemoteFileLimit'; // 文件下载的链接
42       window.open(downloadUrl);
43       console.log('下载文件完成');
44     },
45   },
46   mounted() {
47     axios.get('http://localhost:8080/hello').then(res => {
48       console.log(res);
49     });
50   },
51 }
52 </script>
53 
54 <!-- Add "scoped" attribute to limit CSS to this component only -->
55 <style scoped>
56 h3 {
57   margin: 40px 0 0;
58 }
59 ul {
60   list-style-type: none;
61   padding: 0;
62 }
63 li {
64   display: inline-block;
65   margin: 0 10px;
66 }
67 a {
68   color: #42b983;
69 }
70 </style>

 

posted @ 2025-01-05 22:04  gdwkong  阅读(177)  评论(0)    收藏  举报