前言

SpringBoot文件下载作为现代Web应用的核心功能之一,直接影响着用户体验和系统性能。通过合理的文件下载策略和性能优化,能够构建一个高效、稳定、可扩展的文件下载系统,确保系统的稳定运行。本文从文件下载策略到性能优化,从基础实现到企业级应用,系统梳理SpringBoot文件下载的完整解决方案。

一、SpringBoot文件下载架构设计

1.1 文件下载整体架构

1.2 文件下载策略架构

二、SpringBoot文件下载策略实现

2.1 直接下载策略

2.1.1 直接下载服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
/**
* 直接下载服务
*/
@Service
public class DirectDownloadService {

@Autowired
private FileRepository fileRepository;

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private UserService userService;

private final String FILE_DOWNLOAD_CACHE_PREFIX = "file_download:";
private final long FILE_DOWNLOAD_CACHE_EXPIRE = 3600; // 1小时

/**
* 直接下载文件
*/
public ResponseEntity<Resource> downloadFileDirectly(String fileId, HttpServletRequest request) {
try {
// 1. 验证文件ID
validateFileId(fileId);

// 2. 检查用户权限
Long userId = getCurrentUserId(request);
checkUserPermission(userId, fileId);

// 3. 获取文件信息
FileInfo fileInfo = getFileInfo(fileId);

// 4. 检查文件是否存在
checkFileExists(fileInfo);

// 5. 构建文件资源
Resource resource = buildFileResource(fileInfo);

// 6. 记录下载日志
recordDownloadLog(fileId, userId, request);

// 7. 构建响应头
HttpHeaders headers = buildDownloadHeaders(fileInfo);

return ResponseEntity.ok()
.headers(headers)
.body(resource);

} catch (Exception e) {
log.error("直接下载文件失败: {}", fileId, e);
return ResponseEntity.notFound().build();
}
}

/**
* 验证文件ID
*/
private void validateFileId(String fileId) {
if (fileId == null || fileId.trim().isEmpty()) {
throw new IllegalArgumentException("文件ID不能为空");
}

if (!fileId.matches("^[a-zA-Z0-9_-]+$")) {
throw new IllegalArgumentException("文件ID格式不正确");
}
}

/**
* 获取当前用户ID
*/
private Long getCurrentUserId(HttpServletRequest request) {
try {
String token = request.getHeader("Authorization");
if (token == null || token.trim().isEmpty()) {
throw new IllegalArgumentException("用户未登录");
}

// 解析JWT Token获取用户ID
return userService.getUserIdFromToken(token);

} catch (Exception e) {
log.error("获取当前用户ID失败", e);
throw new IllegalArgumentException("用户身份验证失败");
}
}

/**
* 检查用户权限
*/
private void checkUserPermission(Long userId, String fileId) {
try {
// 从缓存获取权限信息
String cacheKey = FILE_DOWNLOAD_CACHE_PREFIX + "permission:" + fileId + ":" + userId;
Boolean hasPermission = (Boolean) redisTemplate.opsForValue().get(cacheKey);

if (hasPermission != null) {
if (!hasPermission) {
throw new IllegalArgumentException("无权限下载此文件");
}
return;
}

// 从数据库检查权限
boolean permission = fileRepository.checkUserPermission(fileId, userId);

// 缓存权限信息
redisTemplate.opsForValue().set(cacheKey, permission, Duration.ofSeconds(FILE_DOWNLOAD_CACHE_EXPIRE));

if (!permission) {
throw new IllegalArgumentException("无权限下载此文件");
}

} catch (Exception e) {
log.error("检查用户权限失败", e);
throw new IllegalArgumentException("权限验证失败");
}
}

/**
* 获取文件信息
*/
private FileInfo getFileInfo(String fileId) {
try {
// 从缓存获取文件信息
String cacheKey = FILE_DOWNLOAD_CACHE_PREFIX + "info:" + fileId;
FileInfo fileInfo = (FileInfo) redisTemplate.opsForValue().get(cacheKey);

if (fileInfo != null) {
return fileInfo;
}

// 从数据库获取文件信息
fileInfo = fileRepository.findByFileId(fileId);
if (fileInfo == null) {
throw new IllegalArgumentException("文件不存在");
}

// 缓存文件信息
redisTemplate.opsForValue().set(cacheKey, fileInfo, Duration.ofSeconds(FILE_DOWNLOAD_CACHE_EXPIRE));

return fileInfo;

} catch (Exception e) {
log.error("获取文件信息失败", e);
throw new IllegalArgumentException("获取文件信息失败");
}
}

/**
* 检查文件是否存在
*/
private void checkFileExists(FileInfo fileInfo) {
try {
Path filePath = Paths.get(fileInfo.getFilePath());
if (!Files.exists(filePath)) {
throw new IllegalArgumentException("文件不存在");
}

if (!Files.isReadable(filePath)) {
throw new IllegalArgumentException("文件不可读");
}

} catch (Exception e) {
log.error("检查文件是否存在失败", e);
throw new IllegalArgumentException("文件检查失败");
}
}

/**
* 构建文件资源
*/
private Resource buildFileResource(FileInfo fileInfo) {
try {
Path filePath = Paths.get(fileInfo.getFilePath());
Resource resource = new UrlResource(filePath.toUri());

if (!resource.exists()) {
throw new IllegalArgumentException("文件资源不存在");
}

return resource;

} catch (Exception e) {
log.error("构建文件资源失败", e);
throw new IllegalArgumentException("构建文件资源失败");
}
}

/**
* 构建下载响应头
*/
private HttpHeaders buildDownloadHeaders(FileInfo fileInfo) {
try {
HttpHeaders headers = new HttpHeaders();

// 设置内容类型
String contentType = Files.probeContentType(Paths.get(fileInfo.getFilePath()));
if (contentType == null) {
contentType = "application/octet-stream";
}
headers.setContentType(MediaType.parseMediaType(contentType));

// 设置内容长度
headers.setContentLength(fileInfo.getFileSize());

// 设置文件名
String fileName = URLEncoder.encode(fileInfo.getFileName(), StandardCharsets.UTF_8.toString());
headers.setContentDispositionFormData("attachment", fileName);

// 设置缓存控制
headers.setCacheControl(CacheControl.noCache().mustRevalidate());

// 设置ETag
headers.setETag("\"" + fileInfo.getFileHash() + "\"");

return headers;

} catch (Exception e) {
log.error("构建下载响应头失败", e);
return new HttpHeaders();
}
}

/**
* 记录下载日志
*/
private void recordDownloadLog(String fileId, Long userId, HttpServletRequest request) {
try {
DownloadLog downloadLog = new DownloadLog();
downloadLog.setFileId(fileId);
downloadLog.setUserId(userId);
downloadLog.setIpAddress(getClientIpAddress(request));
downloadLog.setUserAgent(request.getHeader("User-Agent"));
downloadLog.setDownloadTime(LocalDateTime.now());

// 异步记录下载日志
CompletableFuture.runAsync(() -> {
try {
fileRepository.saveDownloadLog(downloadLog);
} catch (Exception e) {
log.error("记录下载日志失败", e);
}
});

} catch (Exception e) {
log.error("记录下载日志失败", e);
}
}

/**
* 获取客户端IP地址
*/
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}

String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty()) {
return xRealIp;
}

return request.getRemoteAddr();
}
}

2.2 流式下载策略

2.2.1 流式下载服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
/**
* 流式下载服务
*/
@Service
public class StreamDownloadService {

@Autowired
private FileRepository fileRepository;

@Autowired
private RedisTemplate<String, Object> redisTemplate;

private final String STREAM_DOWNLOAD_CACHE_PREFIX = "stream_download:";
private final int BUFFER_SIZE = 8192; // 8KB缓冲区

/**
* 流式下载文件
*/
public ResponseEntity<StreamingResponseBody> downloadFileStream(String fileId, HttpServletRequest request) {
try {
// 1. 验证文件ID
validateFileId(fileId);

// 2. 检查用户权限
Long userId = getCurrentUserId(request);
checkUserPermission(userId, fileId);

// 3. 获取文件信息
FileInfo fileInfo = getFileInfo(fileId);

// 4. 检查文件是否存在
checkFileExists(fileInfo);

// 5. 构建流式响应体
StreamingResponseBody responseBody = buildStreamingResponseBody(fileInfo);

// 6. 记录下载日志
recordDownloadLog(fileId, userId, request);

// 7. 构建响应头
HttpHeaders headers = buildStreamDownloadHeaders(fileInfo);

return ResponseEntity.ok()
.headers(headers)
.body(responseBody);

} catch (Exception e) {
log.error("流式下载文件失败: {}", fileId, e);
return ResponseEntity.notFound().build();
}
}

/**
* 构建流式响应体
*/
private StreamingResponseBody buildStreamingResponseBody(FileInfo fileInfo) {
return outputStream -> {
try (FileInputStream fileInputStream = new FileInputStream(fileInfo.getFilePath());
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream)) {

byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;

while ((bytesRead = bufferedInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
outputStream.flush();

// 添加小延迟,避免过快传输
Thread.sleep(1);
}

} catch (Exception e) {
log.error("流式下载文件失败", e);
throw new RuntimeException("流式下载失败", e);
}
};
}

/**
* 构建流式下载响应头
*/
private HttpHeaders buildStreamDownloadHeaders(FileInfo fileInfo) {
try {
HttpHeaders headers = new HttpHeaders();

// 设置内容类型
String contentType = Files.probeContentType(Paths.get(fileInfo.getFilePath()));
if (contentType == null) {
contentType = "application/octet-stream";
}
headers.setContentType(MediaType.parseMediaType(contentType));

// 设置文件名
String fileName = URLEncoder.encode(fileInfo.getFileName(), StandardCharsets.UTF_8.toString());
headers.setContentDispositionFormData("attachment", fileName);

// 设置传输编码
headers.set("Transfer-Encoding", "chunked");

// 设置缓存控制
headers.setCacheControl(CacheControl.noCache().mustRevalidate());

return headers;

} catch (Exception e) {
log.error("构建流式下载响应头失败", e);
return new HttpHeaders();
}
}

/**
* 验证文件ID
*/
private void validateFileId(String fileId) {
if (fileId == null || fileId.trim().isEmpty()) {
throw new IllegalArgumentException("文件ID不能为空");
}

if (!fileId.matches("^[a-zA-Z0-9_-]+$")) {
throw new IllegalArgumentException("文件ID格式不正确");
}
}

/**
* 获取当前用户ID
*/
private Long getCurrentUserId(HttpServletRequest request) {
try {
String token = request.getHeader("Authorization");
if (token == null || token.trim().isEmpty()) {
throw new IllegalArgumentException("用户未登录");
}

// 解析JWT Token获取用户ID
return userService.getUserIdFromToken(token);

} catch (Exception e) {
log.error("获取当前用户ID失败", e);
throw new IllegalArgumentException("用户身份验证失败");
}
}

/**
* 检查用户权限
*/
private void checkUserPermission(Long userId, String fileId) {
try {
// 从缓存获取权限信息
String cacheKey = STREAM_DOWNLOAD_CACHE_PREFIX + "permission:" + fileId + ":" + userId;
Boolean hasPermission = (Boolean) redisTemplate.opsForValue().get(cacheKey);

if (hasPermission != null) {
if (!hasPermission) {
throw new IllegalArgumentException("无权限下载此文件");
}
return;
}

// 从数据库检查权限
boolean permission = fileRepository.checkUserPermission(fileId, userId);

// 缓存权限信息
redisTemplate.opsForValue().set(cacheKey, permission, Duration.ofSeconds(3600));

if (!permission) {
throw new IllegalArgumentException("无权限下载此文件");
}

} catch (Exception e) {
log.error("检查用户权限失败", e);
throw new IllegalArgumentException("权限验证失败");
}
}

/**
* 获取文件信息
*/
private FileInfo getFileInfo(String fileId) {
try {
// 从缓存获取文件信息
String cacheKey = STREAM_DOWNLOAD_CACHE_PREFIX + "info:" + fileId;
FileInfo fileInfo = (FileInfo) redisTemplate.opsForValue().get(cacheKey);

if (fileInfo != null) {
return fileInfo;
}

// 从数据库获取文件信息
fileInfo = fileRepository.findByFileId(fileId);
if (fileInfo == null) {
throw new IllegalArgumentException("文件不存在");
}

// 缓存文件信息
redisTemplate.opsForValue().set(cacheKey, fileInfo, Duration.ofSeconds(3600));

return fileInfo;

} catch (Exception e) {
log.error("获取文件信息失败", e);
throw new IllegalArgumentException("获取文件信息失败");
}
}

/**
* 检查文件是否存在
*/
private void checkFileExists(FileInfo fileInfo) {
try {
Path filePath = Paths.get(fileInfo.getFilePath());
if (!Files.exists(filePath)) {
throw new IllegalArgumentException("文件不存在");
}

if (!Files.isReadable(filePath)) {
throw new IllegalArgumentException("文件不可读");
}

} catch (Exception e) {
log.error("检查文件是否存在失败", e);
throw new IllegalArgumentException("文件检查失败");
}
}

/**
* 记录下载日志
*/
private void recordDownloadLog(String fileId, Long userId, HttpServletRequest request) {
try {
DownloadLog downloadLog = new DownloadLog();
downloadLog.setFileId(fileId);
downloadLog.setUserId(userId);
downloadLog.setIpAddress(getClientIpAddress(request));
downloadLog.setUserAgent(request.getHeader("User-Agent"));
downloadLog.setDownloadTime(LocalDateTime.now());

// 异步记录下载日志
CompletableFuture.runAsync(() -> {
try {
fileRepository.saveDownloadLog(downloadLog);
} catch (Exception e) {
log.error("记录下载日志失败", e);
}
});

} catch (Exception e) {
log.error("记录下载日志失败", e);
}
}

/**
* 获取客户端IP地址
*/
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}

String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty()) {
return xRealIp;
}

return request.getRemoteAddr();
}
}

2.3 分片下载策略

2.3.1 分片下载服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
/**
* 分片下载服务
*/
@Service
public class ChunkDownloadService {

@Autowired
private FileRepository fileRepository;

@Autowired
private RedisTemplate<String, Object> redisTemplate;

private final String CHUNK_DOWNLOAD_CACHE_PREFIX = "chunk_download:";
private final int CHUNK_SIZE = 1024 * 1024; // 1MB分片大小

/**
* 分片下载文件
*/
public ResponseEntity<Resource> downloadFileChunk(String fileId, Long start, Long end, HttpServletRequest request) {
try {
// 1. 验证文件ID
validateFileId(fileId);

// 2. 验证分片参数
validateChunkParameters(start, end);

// 3. 检查用户权限
Long userId = getCurrentUserId(request);
checkUserPermission(userId, fileId);

// 4. 获取文件信息
FileInfo fileInfo = getFileInfo(fileId);

// 5. 检查文件是否存在
checkFileExists(fileInfo);

// 6. 验证分片范围
validateChunkRange(fileInfo, start, end);

// 7. 构建分片资源
Resource resource = buildChunkResource(fileInfo, start, end);

// 8. 记录下载日志
recordDownloadLog(fileId, userId, request, start, end);

// 9. 构建响应头
HttpHeaders headers = buildChunkDownloadHeaders(fileInfo, start, end);

return ResponseEntity.ok()
.headers(headers)
.body(resource);

} catch (Exception e) {
log.error("分片下载文件失败: {}", fileId, e);
return ResponseEntity.notFound().build();
}
}

/**
* 验证分片参数
*/
private void validateChunkParameters(Long start, Long end) {
if (start == null || start < 0) {
throw new IllegalArgumentException("起始位置不能为空或小于0");
}

if (end == null || end < 0) {
throw new IllegalArgumentException("结束位置不能为空或小于0");
}

if (start > end) {
throw new IllegalArgumentException("起始位置不能大于结束位置");
}

if (end - start > CHUNK_SIZE) {
throw new IllegalArgumentException("分片大小不能超过" + CHUNK_SIZE + "字节");
}
}

/**
* 验证分片范围
*/
private void validateChunkRange(FileInfo fileInfo, Long start, Long end) {
if (start >= fileInfo.getFileSize()) {
throw new IllegalArgumentException("起始位置超出文件大小");
}

if (end > fileInfo.getFileSize()) {
throw new IllegalArgumentException("结束位置超出文件大小");
}
}

/**
* 构建分片资源
*/
private Resource buildChunkResource(FileInfo fileInfo, Long start, Long end) {
try {
Path filePath = Paths.get(fileInfo.getFilePath());
RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");

// 设置文件指针到起始位置
randomAccessFile.seek(start);

// 计算分片大小
long chunkSize = end - start + 1;

// 读取分片数据
byte[] chunkData = new byte[(int) chunkSize];
randomAccessFile.read(chunkData);
randomAccessFile.close();

// 创建字节数组资源
ByteArrayResource resource = new ByteArrayResource(chunkData);

return resource;

} catch (Exception e) {
log.error("构建分片资源失败", e);
throw new IllegalArgumentException("构建分片资源失败");
}
}

/**
* 构建分片下载响应头
*/
private HttpHeaders buildChunkDownloadHeaders(FileInfo fileInfo, Long start, Long end) {
try {
HttpHeaders headers = new HttpHeaders();

// 设置内容类型
String contentType = Files.probeContentType(Paths.get(fileInfo.getFilePath()));
if (contentType == null) {
contentType = "application/octet-stream";
}
headers.setContentType(MediaType.parseMediaType(contentType));

// 设置内容长度
long chunkSize = end - start + 1;
headers.setContentLength(chunkSize);

// 设置内容范围
headers.set("Content-Range", "bytes " + start + "-" + end + "/" + fileInfo.getFileSize());

// 设置文件名
String fileName = URLEncoder.encode(fileInfo.getFileName(), StandardCharsets.UTF_8.toString());
headers.setContentDispositionFormData("attachment", fileName);

// 设置缓存控制
headers.setCacheControl(CacheControl.noCache().mustRevalidate());

return headers;

} catch (Exception e) {
log.error("构建分片下载响应头失败", e);
return new HttpHeaders();
}
}

/**
* 验证文件ID
*/
private void validateFileId(String fileId) {
if (fileId == null || fileId.trim().isEmpty()) {
throw new IllegalArgumentException("文件ID不能为空");
}

if (!fileId.matches("^[a-zA-Z0-9_-]+$")) {
throw new IllegalArgumentException("文件ID格式不正确");
}
}

/**
* 获取当前用户ID
*/
private Long getCurrentUserId(HttpServletRequest request) {
try {
String token = request.getHeader("Authorization");
if (token == null || token.trim().isEmpty()) {
throw new IllegalArgumentException("用户未登录");
}

// 解析JWT Token获取用户ID
return userService.getUserIdFromToken(token);

} catch (Exception e) {
log.error("获取当前用户ID失败", e);
throw new IllegalArgumentException("用户身份验证失败");
}
}

/**
* 检查用户权限
*/
private void checkUserPermission(Long userId, String fileId) {
try {
// 从缓存获取权限信息
String cacheKey = CHUNK_DOWNLOAD_CACHE_PREFIX + "permission:" + fileId + ":" + userId;
Boolean hasPermission = (Boolean) redisTemplate.opsForValue().get(cacheKey);

if (hasPermission != null) {
if (!hasPermission) {
throw new IllegalArgumentException("无权限下载此文件");
}
return;
}

// 从数据库检查权限
boolean permission = fileRepository.checkUserPermission(fileId, userId);

// 缓存权限信息
redisTemplate.opsForValue().set(cacheKey, permission, Duration.ofSeconds(3600));

if (!permission) {
throw new IllegalArgumentException("无权限下载此文件");
}

} catch (Exception e) {
log.error("检查用户权限失败", e);
throw new IllegalArgumentException("权限验证失败");
}
}

/**
* 获取文件信息
*/
private FileInfo getFileInfo(String fileId) {
try {
// 从缓存获取文件信息
String cacheKey = CHUNK_DOWNLOAD_CACHE_PREFIX + "info:" + fileId;
FileInfo fileInfo = (FileInfo) redisTemplate.opsForValue().get(cacheKey);

if (fileInfo != null) {
return fileInfo;
}

// 从数据库获取文件信息
fileInfo = fileRepository.findByFileId(fileId);
if (fileInfo == null) {
throw new IllegalArgumentException("文件不存在");
}

// 缓存文件信息
redisTemplate.opsForValue().set(cacheKey, fileInfo, Duration.ofSeconds(3600));

return fileInfo;

} catch (Exception e) {
log.error("获取文件信息失败", e);
throw new IllegalArgumentException("获取文件信息失败");
}
}

/**
* 检查文件是否存在
*/
private void checkFileExists(FileInfo fileInfo) {
try {
Path filePath = Paths.get(fileInfo.getFilePath());
if (!Files.exists(filePath)) {
throw new IllegalArgumentException("文件不存在");
}

if (!Files.isReadable(filePath)) {
throw new IllegalArgumentException("文件不可读");
}

} catch (Exception e) {
log.error("检查文件是否存在失败", e);
throw new IllegalArgumentException("文件检查失败");
}
}

/**
* 记录下载日志
*/
private void recordDownloadLog(String fileId, Long userId, HttpServletRequest request, Long start, Long end) {
try {
DownloadLog downloadLog = new DownloadLog();
downloadLog.setFileId(fileId);
downloadLog.setUserId(userId);
downloadLog.setIpAddress(getClientIpAddress(request));
downloadLog.setUserAgent(request.getHeader("User-Agent"));
downloadLog.setDownloadTime(LocalDateTime.now());
downloadLog.setChunkStart(start);
downloadLog.setChunkEnd(end);

// 异步记录下载日志
CompletableFuture.runAsync(() -> {
try {
fileRepository.saveDownloadLog(downloadLog);
} catch (Exception e) {
log.error("记录下载日志失败", e);
}
});

} catch (Exception e) {
log.error("记录下载日志失败", e);
}
}

/**
* 获取客户端IP地址
*/
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}

String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty()) {
return xRealIp;
}

return request.getRemoteAddr();
}
}

2.4 断点续传策略

2.4.1 断点续传服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
/**
* 断点续传服务
*/
@Service
public class ResumeDownloadService {

@Autowired
private FileRepository fileRepository;

@Autowired
private RedisTemplate<String, Object> redisTemplate;

private final String RESUME_DOWNLOAD_CACHE_PREFIX = "resume_download:";
private final int CHUNK_SIZE = 1024 * 1024; // 1MB分片大小

/**
* 断点续传下载文件
*/
public ResponseEntity<Resource> downloadFileResume(String fileId, String range, HttpServletRequest request) {
try {
// 1. 验证文件ID
validateFileId(fileId);

// 2. 检查用户权限
Long userId = getCurrentUserId(request);
checkUserPermission(userId, fileId);

// 3. 获取文件信息
FileInfo fileInfo = getFileInfo(fileId);

// 4. 检查文件是否存在
checkFileExists(fileInfo);

// 5. 解析Range头
RangeInfo rangeInfo = parseRangeHeader(range, fileInfo.getFileSize());

// 6. 构建断点续传资源
Resource resource = buildResumeResource(fileInfo, rangeInfo);

// 7. 记录下载日志
recordDownloadLog(fileId, userId, request, rangeInfo);

// 8. 构建响应头
HttpHeaders headers = buildResumeDownloadHeaders(fileInfo, rangeInfo);

return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
.headers(headers)
.body(resource);

} catch (Exception e) {
log.error("断点续传下载文件失败: {}", fileId, e);
return ResponseEntity.notFound().build();
}
}

/**
* 解析Range头
*/
private RangeInfo parseRangeHeader(String range, Long fileSize) {
try {
if (range == null || range.trim().isEmpty()) {
// 没有Range头,返回完整文件
return new RangeInfo(0L, fileSize - 1, fileSize);
}

// 解析Range头格式: bytes=start-end
if (!range.startsWith("bytes=")) {
throw new IllegalArgumentException("Range头格式不正确");
}

String rangeValue = range.substring(6);
String[] parts = rangeValue.split("-");

if (parts.length != 2) {
throw new IllegalArgumentException("Range头格式不正确");
}

Long start = Long.parseLong(parts[0]);
Long end = Long.parseLong(parts[1]);

// 验证范围
if (start < 0 || start >= fileSize) {
throw new IllegalArgumentException("起始位置超出文件大小");
}

if (end >= fileSize) {
end = fileSize - 1;
}

if (start > end) {
throw new IllegalArgumentException("起始位置不能大于结束位置");
}

long contentLength = end - start + 1;

return new RangeInfo(start, end, contentLength);

} catch (Exception e) {
log.error("解析Range头失败", e);
throw new IllegalArgumentException("Range头解析失败");
}
}

/**
* 构建断点续传资源
*/
private Resource buildResumeResource(FileInfo fileInfo, RangeInfo rangeInfo) {
try {
Path filePath = Paths.get(fileInfo.getFilePath());
RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");

// 设置文件指针到起始位置
randomAccessFile.seek(rangeInfo.getStart());

// 读取指定范围的数据
byte[] data = new byte[(int) rangeInfo.getContentLength()];
randomAccessFile.read(data);
randomAccessFile.close();

// 创建字节数组资源
ByteArrayResource resource = new ByteArrayResource(data);

return resource;

} catch (Exception e) {
log.error("构建断点续传资源失败", e);
throw new IllegalArgumentException("构建断点续传资源失败");
}
}

/**
* 构建断点续传响应头
*/
private HttpHeaders buildResumeDownloadHeaders(FileInfo fileInfo, RangeInfo rangeInfo) {
try {
HttpHeaders headers = new HttpHeaders();

// 设置内容类型
String contentType = Files.probeContentType(Paths.get(fileInfo.getFilePath()));
if (contentType == null) {
contentType = "application/octet-stream";
}
headers.setContentType(MediaType.parseMediaType(contentType));

// 设置内容长度
headers.setContentLength(rangeInfo.getContentLength());

// 设置内容范围
headers.set("Content-Range", "bytes " + rangeInfo.getStart() + "-" + rangeInfo.getEnd() + "/" + fileInfo.getFileSize());

// 设置文件名
String fileName = URLEncoder.encode(fileInfo.getFileName(), StandardCharsets.UTF_8.toString());
headers.setContentDispositionFormData("attachment", fileName);

// 设置缓存控制
headers.setCacheControl(CacheControl.noCache().mustRevalidate());

// 设置ETag
headers.setETag("\"" + fileInfo.getFileHash() + "\"");

return headers;

} catch (Exception e) {
log.error("构建断点续传响应头失败", e);
return new HttpHeaders();
}
}

/**
* 验证文件ID
*/
private void validateFileId(String fileId) {
if (fileId == null || fileId.trim().isEmpty()) {
throw new IllegalArgumentException("文件ID不能为空");
}

if (!fileId.matches("^[a-zA-Z0-9_-]+$")) {
throw new IllegalArgumentException("文件ID格式不正确");
}
}

/**
* 获取当前用户ID
*/
private Long getCurrentUserId(HttpServletRequest request) {
try {
String token = request.getHeader("Authorization");
if (token == null || token.trim().isEmpty()) {
throw new IllegalArgumentException("用户未登录");
}

// 解析JWT Token获取用户ID
return userService.getUserIdFromToken(token);

} catch (Exception e) {
log.error("获取当前用户ID失败", e);
throw new IllegalArgumentException("用户身份验证失败");
}
}

/**
* 检查用户权限
*/
private void checkUserPermission(Long userId, String fileId) {
try {
// 从缓存获取权限信息
String cacheKey = RESUME_DOWNLOAD_CACHE_PREFIX + "permission:" + fileId + ":" + userId;
Boolean hasPermission = (Boolean) redisTemplate.opsForValue().get(cacheKey);

if (hasPermission != null) {
if (!hasPermission) {
throw new IllegalArgumentException("无权限下载此文件");
}
return;
}

// 从数据库检查权限
boolean permission = fileRepository.checkUserPermission(fileId, userId);

// 缓存权限信息
redisTemplate.opsForValue().set(cacheKey, permission, Duration.ofSeconds(3600));

if (!permission) {
throw new IllegalArgumentException("无权限下载此文件");
}

} catch (Exception e) {
log.error("检查用户权限失败", e);
throw new IllegalArgumentException("权限验证失败");
}
}

/**
* 获取文件信息
*/
private FileInfo getFileInfo(String fileId) {
try {
// 从缓存获取文件信息
String cacheKey = RESUME_DOWNLOAD_CACHE_PREFIX + "info:" + fileId;
FileInfo fileInfo = (FileInfo) redisTemplate.opsForValue().get(cacheKey);

if (fileInfo != null) {
return fileInfo;
}

// 从数据库获取文件信息
fileInfo = fileRepository.findByFileId(fileId);
if (fileInfo == null) {
throw new IllegalArgumentException("文件不存在");
}

// 缓存文件信息
redisTemplate.opsForValue().set(cacheKey, fileInfo, Duration.ofSeconds(3600));

return fileInfo;

} catch (Exception e) {
log.error("获取文件信息失败", e);
throw new IllegalArgumentException("获取文件信息失败");
}
}

/**
* 检查文件是否存在
*/
private void checkFileExists(FileInfo fileInfo) {
try {
Path filePath = Paths.get(fileInfo.getFilePath());
if (!Files.exists(filePath)) {
throw new IllegalArgumentException("文件不存在");
}

if (!Files.isReadable(filePath)) {
throw new IllegalArgumentException("文件不可读");
}

} catch (Exception e) {
log.error("检查文件是否存在失败", e);
throw new IllegalArgumentException("文件检查失败");
}
}

/**
* 记录下载日志
*/
private void recordDownloadLog(String fileId, Long userId, HttpServletRequest request, RangeInfo rangeInfo) {
try {
DownloadLog downloadLog = new DownloadLog();
downloadLog.setFileId(fileId);
downloadLog.setUserId(userId);
downloadLog.setIpAddress(getClientIpAddress(request));
downloadLog.setUserAgent(request.getHeader("User-Agent"));
downloadLog.setDownloadTime(LocalDateTime.now());
downloadLog.setChunkStart(rangeInfo.getStart());
downloadLog.setChunkEnd(rangeInfo.getEnd());

// 异步记录下载日志
CompletableFuture.runAsync(() -> {
try {
fileRepository.saveDownloadLog(downloadLog);
} catch (Exception e) {
log.error("记录下载日志失败", e);
}
});

} catch (Exception e) {
log.error("记录下载日志失败", e);
}
}

/**
* 获取客户端IP地址
*/
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}

String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty()) {
return xRealIp;
}

return request.getRemoteAddr();
}
}

三、企业级文件下载方案

3.1 文件下载管理服务

3.1.1 文件下载管理服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/**
* 文件下载管理服务
*/
@Service
public class FileDownloadManagementService {

@Autowired
private DirectDownloadService directDownloadService;

@Autowired
private StreamDownloadService streamDownloadService;

@Autowired
private ChunkDownloadService chunkDownloadService;

@Autowired
private ResumeDownloadService resumeDownloadService;

@Autowired
private RedisTemplate<String, Object> redisTemplate;

private final String FILE_DOWNLOAD_MANAGEMENT_CACHE_PREFIX = "file_download_management:";
private final long FILE_DOWNLOAD_MANAGEMENT_CACHE_EXPIRE = 3600; // 1小时

/**
* 智能选择下载策略
*/
public ResponseEntity<?> downloadFile(String fileId, String downloadType, String range, HttpServletRequest request) {
try {
// 1. 验证文件ID
validateFileId(fileId);

// 2. 获取文件信息
FileInfo fileInfo = getFileInfo(fileId);

// 3. 选择下载策略
DownloadStrategy strategy = selectDownloadStrategy(fileInfo, downloadType, range);

// 4. 执行下载
ResponseEntity<?> response = executeDownload(strategy, fileId, range, request);

// 5. 记录下载统计
recordDownloadStatistics(fileId, strategy, request);

return response;

} catch (Exception e) {
log.error("文件下载失败: {}", fileId, e);
return ResponseEntity.notFound().build();
}
}

/**
* 选择下载策略
*/
private DownloadStrategy selectDownloadStrategy(FileInfo fileInfo, String downloadType, String range) {
try {
// 根据文件大小选择策略
if (fileInfo.getFileSize() < 1024 * 1024) { // 小于1MB
return DownloadStrategy.DIRECT;
} else if (fileInfo.getFileSize() < 100 * 1024 * 1024) { // 小于100MB
return DownloadStrategy.STREAM;
} else if (range != null && !range.trim().isEmpty()) { // 有Range头
return DownloadStrategy.RESUME;
} else {
return DownloadStrategy.CHUNK;
}

} catch (Exception e) {
log.error("选择下载策略失败", e);
return DownloadStrategy.DIRECT; // 默认策略
}
}

/**
* 执行下载
*/
private ResponseEntity<?> executeDownload(DownloadStrategy strategy, String fileId, String range, HttpServletRequest request) {
try {
switch (strategy) {
case DIRECT:
return directDownloadService.downloadFileDirectly(fileId, request);
case STREAM:
return streamDownloadService.downloadFileStream(fileId, request);
case CHUNK:
return chunkDownloadService.downloadFileChunk(fileId, 0L, 1024L * 1024L, request);
case RESUME:
return resumeDownloadService.downloadFileResume(fileId, range, request);
default:
throw new IllegalArgumentException("不支持的下载策略: " + strategy);
}

} catch (Exception e) {
log.error("执行下载失败", e);
throw new IllegalArgumentException("执行下载失败");
}
}

/**
* 验证文件ID
*/
private void validateFileId(String fileId) {
if (fileId == null || fileId.trim().isEmpty()) {
throw new IllegalArgumentException("文件ID不能为空");
}

if (!fileId.matches("^[a-zA-Z0-9_-]+$")) {
throw new IllegalArgumentException("文件ID格式不正确");
}
}

/**
* 获取文件信息
*/
private FileInfo getFileInfo(String fileId) {
try {
// 从缓存获取文件信息
String cacheKey = FILE_DOWNLOAD_MANAGEMENT_CACHE_PREFIX + "info:" + fileId;
FileInfo fileInfo = (FileInfo) redisTemplate.opsForValue().get(cacheKey);

if (fileInfo != null) {
return fileInfo;
}

// 从数据库获取文件信息
fileInfo = fileRepository.findByFileId(fileId);
if (fileInfo == null) {
throw new IllegalArgumentException("文件不存在");
}

// 缓存文件信息
redisTemplate.opsForValue().set(cacheKey, fileInfo, Duration.ofSeconds(FILE_DOWNLOAD_MANAGEMENT_CACHE_EXPIRE));

return fileInfo;

} catch (Exception e) {
log.error("获取文件信息失败", e);
throw new IllegalArgumentException("获取文件信息失败");
}
}

/**
* 记录下载统计
*/
private void recordDownloadStatistics(String fileId, DownloadStrategy strategy, HttpServletRequest request) {
try {
DownloadStatistics statistics = new DownloadStatistics();
statistics.setFileId(fileId);
statistics.setStrategy(strategy);
statistics.setIpAddress(getClientIpAddress(request));
statistics.setUserAgent(request.getHeader("User-Agent"));
statistics.setDownloadTime(LocalDateTime.now());

// 异步记录下载统计
CompletableFuture.runAsync(() -> {
try {
fileRepository.saveDownloadStatistics(statistics);
} catch (Exception e) {
log.error("记录下载统计失败", e);
}
});

} catch (Exception e) {
log.error("记录下载统计失败", e);
}
}

/**
* 获取客户端IP地址
*/
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}

String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty()) {
return xRealIp;
}

return request.getRemoteAddr();
}

/**
* 获取文件下载统计
*/
public FileDownloadStatisticsResult getFileDownloadStatistics(Date startTime, Date endTime) {
try {
FileDownloadStatisticsResult result = new FileDownloadStatisticsResult();
result.setStartTime(startTime);
result.setEndTime(endTime);

// 统计下载次数
result.setTotalDownloads(10000L); // 实际应用中需要从数据库统计

// 统计下载策略使用情况
Map<DownloadStrategy, Long> strategyCount = new HashMap<>();
strategyCount.put(DownloadStrategy.DIRECT, 3000L);
strategyCount.put(DownloadStrategy.STREAM, 4000L);
strategyCount.put(DownloadStrategy.CHUNK, 2000L);
strategyCount.put(DownloadStrategy.RESUME, 1000L);
result.setStrategyCount(strategyCount);

// 统计文件类型下载情况
Map<String, Long> fileTypeCount = new HashMap<>();
fileTypeCount.put("PDF", 4000L);
fileTypeCount.put("DOC", 3000L);
fileTypeCount.put("XLS", 2000L);
fileTypeCount.put("ZIP", 1000L);
result.setFileTypeCount(fileTypeCount);

// 统计平均下载时间
result.setAverageDownloadTime(5000.0); // 5秒

// 统计成功率
result.setSuccessRate(0.98); // 98%

return result;

} catch (Exception e) {
log.error("获取文件下载统计失败", e);
throw new FileDownloadException("获取文件下载统计失败", e);
}
}
}

3.2 文件下载优化服务

3.2.1 文件下载优化服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
/**
* 文件下载优化服务
*/
@Service
public class FileDownloadOptimizationService {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private CaffeineCache localCache;

private final String FILE_DOWNLOAD_OPTIMIZATION_CACHE_PREFIX = "file_download_optimization:";

/**
* 优化文件下载性能
*/
public FileDownloadOptimizationResult optimizeFileDownloadPerformance(FileDownloadOptimizationRequest request) {
try {
FileDownloadOptimizationResult result = new FileDownloadOptimizationResult();
result.setRequestId(request.getRequestId());
result.setStartTime(new Date());

// 1. 分析文件下载模式
FileDownloadPatternAnalysis patternAnalysis = analyzeFileDownloadPattern(request);
result.setPatternAnalysis(patternAnalysis);

// 2. 优化缓存策略
FileDownloadCacheOptimizationResult cacheOptimization = optimizeFileDownloadCacheStrategy(request, patternAnalysis);
result.setCacheOptimization(cacheOptimization);

// 3. 优化网络策略
FileDownloadNetworkOptimizationResult networkOptimization = optimizeFileDownloadNetworkStrategy(request, patternAnalysis);
result.setNetworkOptimization(networkOptimization);

result.setStatus(FileDownloadOptimizationStatus.COMPLETED);
result.setEndTime(new Date());

return result;

} catch (Exception e) {
log.error("优化文件下载性能失败", e);
throw new FileDownloadException("优化文件下载性能失败", e);
}
}

/**
* 分析文件下载模式
*/
private FileDownloadPatternAnalysis analyzeFileDownloadPattern(FileDownloadOptimizationRequest request) {
try {
FileDownloadPatternAnalysis analysis = new FileDownloadPatternAnalysis();
analysis.setRequestId(request.getRequestId());

// 分析下载频率
analysis.setDownloadFrequency(analyzeFileDownloadFrequency(request.getFileId()));

// 分析文件特征
analysis.setFileCharacteristics(analyzeFileCharacteristics(request.getFileId()));

// 分析网络环境
analysis.setNetworkEnvironment(analyzeNetworkEnvironment(request.getClientIp()));

return analysis;

} catch (Exception e) {
log.error("分析文件下载模式失败", e);
throw new FileDownloadException("分析文件下载模式失败", e);
}
}

/**
* 分析文件下载频率
*/
private FileDownloadFrequency analyzeFileDownloadFrequency(String fileId) {
try {
FileDownloadFrequency frequency = new FileDownloadFrequency();
frequency.setFileId(fileId);
frequency.setDailyCount(100);
frequency.setHourlyCount(10);
frequency.setMinuteCount(1);

return frequency;

} catch (Exception e) {
log.error("分析文件下载频率失败", e);
return new FileDownloadFrequency();
}
}

/**
* 分析文件特征
*/
private FileCharacteristics analyzeFileCharacteristics(String fileId) {
try {
FileCharacteristics characteristics = new FileCharacteristics();
characteristics.setFileId(fileId);
characteristics.setFileSize(1024L * 1024L * 10); // 10MB
characteristics.setFileType("PDF");
characteristics.setCompressionRatio(0.8);

return characteristics;

} catch (Exception e) {
log.error("分析文件特征失败", e);
return new FileCharacteristics();
}
}

/**
* 分析网络环境
*/
private NetworkEnvironment analyzeNetworkEnvironment(String clientIp) {
try {
NetworkEnvironment environment = new NetworkEnvironment();
environment.setClientIp(clientIp);
environment.setBandwidth(10000000); // 10Mbps
environment.setLatency(50); // 50ms
environment.setStability(0.95); // 95%

return environment;

} catch (Exception e) {
log.error("分析网络环境失败", e);
return new NetworkEnvironment();
}
}

/**
* 优化文件下载缓存策略
*/
private FileDownloadCacheOptimizationResult optimizeFileDownloadCacheStrategy(FileDownloadOptimizationRequest request,
FileDownloadPatternAnalysis analysis) {
try {
FileDownloadCacheOptimizationResult result = new FileDownloadCacheOptimizationResult();
result.setRequestId(request.getRequestId());

// 根据下载频率优化缓存策略
if (analysis.getDownloadFrequency().getDailyCount() > 100) {
result.setRecommendedCacheExpire(3600); // 1小时
result.setRecommendedCacheSize(1000);
result.setRecommendedCacheStrategy("LRU");
} else if (analysis.getDownloadFrequency().getDailyCount() > 10) {
result.setRecommendedCacheExpire(1800); // 30分钟
result.setRecommendedCacheSize(500);
result.setRecommendedCacheStrategy("LFU");
} else {
result.setRecommendedCacheExpire(600); // 10分钟
result.setRecommendedCacheSize(100);
result.setRecommendedCacheStrategy("FIFO");
}

return result;

} catch (Exception e) {
log.error("优化文件下载缓存策略失败", e);
throw new FileDownloadException("优化文件下载缓存策略失败", e);
}
}

/**
* 优化文件下载网络策略
*/
private FileDownloadNetworkOptimizationResult optimizeFileDownloadNetworkStrategy(FileDownloadOptimizationRequest request,
FileDownloadPatternAnalysis analysis) {
try {
FileDownloadNetworkOptimizationResult result = new FileDownloadNetworkOptimizationResult();
result.setRequestId(request.getRequestId());

// 根据网络环境优化网络策略
if (analysis.getNetworkEnvironment().getBandwidth() > 10000000) { // 大于10Mbps
result.setRecommendedChunkSize(1024 * 1024 * 2); // 2MB
result.setRecommendedConcurrentConnections(4);
result.setRecommendedCompression(false);
} else if (analysis.getNetworkEnvironment().getBandwidth() > 1000000) { // 大于1Mbps
result.setRecommendedChunkSize(1024 * 512); // 512KB
result.setRecommendedConcurrentConnections(2);
result.setRecommendedCompression(true);
} else {
result.setRecommendedChunkSize(1024 * 256); // 256KB
result.setRecommendedConcurrentConnections(1);
result.setRecommendedCompression(true);
}

return result;

} catch (Exception e) {
log.error("优化文件下载网络策略失败", e);
throw new FileDownloadException("优化文件下载网络策略失败", e);
}
}

/**
* 预热文件下载缓存
*/
@PostConstruct
public void warmupFileDownloadCache() {
try {
// 预热常用文件下载数据
List<String> commonFileIds = Arrays.asList("file_1", "file_2", "file_3", "file_4", "file_5");

for (String fileId : commonFileIds) {
try {
String cacheKey = FILE_DOWNLOAD_OPTIMIZATION_CACHE_PREFIX + "warmup:" + fileId;
Object warmupData = new Object();
redisTemplate.opsForValue().set(cacheKey, warmupData, Duration.ofHours(1));
} catch (Exception e) {
log.error("预热文件下载缓存失败: {}", fileId, e);
}
}

} catch (Exception e) {
log.error("预热文件下载缓存失败", e);
}
}

/**
* 清理过期缓存
*/
@Scheduled(fixedRate = 300000) // 5分钟
public void cleanupExpiredCache() {
try {
// 清理本地缓存
localCache.cleanUp();

// 清理Redis过期缓存
cleanupRedisExpiredCache();

} catch (Exception e) {
log.error("清理过期缓存失败", e);
}
}

/**
* 清理Redis过期缓存
*/
private void cleanupRedisExpiredCache() {
try {
Set<String> cacheKeys = redisTemplate.keys(FILE_DOWNLOAD_OPTIMIZATION_CACHE_PREFIX + "*");

for (String key : cacheKeys) {
Long ttl = redisTemplate.getExpire(key);
if (ttl != null && ttl <= 0) {
redisTemplate.delete(key);
}
}

} catch (Exception e) {
log.error("清理Redis过期缓存失败", e);
}
}
}

四、性能优化与监控

4.1 性能优化

4.1.1 文件下载性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/**
* 文件下载性能优化服务
*/
@Service
public class FileDownloadPerformanceOptimizationService {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private CaffeineCache localCache;

private final String FILE_DOWNLOAD_PERFORMANCE_CACHE_PREFIX = "file_download_performance:";

/**
* 缓存文件下载性能数据
*/
public void cacheFileDownloadPerformanceData(String requestId, Object data) {
String cacheKey = FILE_DOWNLOAD_PERFORMANCE_CACHE_PREFIX + requestId;

try {
// 写入本地缓存
localCache.put(cacheKey, data);

// 写入Redis缓存
String redisCacheKey = "redis_cache:" + cacheKey;
redisTemplate.opsForValue().set(redisCacheKey, data, Duration.ofHours(1));

} catch (Exception e) {
log.error("缓存文件下载性能数据失败", e);
}
}

/**
* 获取缓存的文件下载性能数据
*/
public Object getCachedFileDownloadPerformanceData(String requestId) {
String cacheKey = FILE_DOWNLOAD_PERFORMANCE_CACHE_PREFIX + requestId;

try {
// 从本地缓存获取
Object cachedData = localCache.getIfPresent(cacheKey);
if (cachedData != null) {
return cachedData;
}

// 从Redis获取
String redisCacheKey = "redis_cache:" + cacheKey;
Object redisData = redisTemplate.opsForValue().get(redisCacheKey);
if (redisData != null) {
localCache.put(cacheKey, redisData);
return redisData;
}

} catch (Exception e) {
log.error("获取缓存的文件下载性能数据失败", e);
}

return null;
}

/**
* 批量处理文件下载请求
*/
public List<FileDownloadResult> batchProcessFileDownloadRequests(
List<FileDownloadRequest> requests) {
List<FileDownloadResult> results = new ArrayList<>();

try {
// 按文件类型分组
Map<String, List<FileDownloadRequest>> typeGroups = requests.stream()
.collect(Collectors.groupingBy(FileDownloadRequest::getFileType));

// 并行处理各类型
typeGroups.entrySet().parallelStream().forEach(entry -> {
String fileType = entry.getKey();
List<FileDownloadRequest> typeRequests = entry.getValue();

try {
List<FileDownloadResult> typeResults = processFileDownloadTypeRequests(fileType, typeRequests);

synchronized (results) {
results.addAll(typeResults);
}

} catch (Exception e) {
log.error("处理文件下载类型失败: {}", fileType, e);
}
});

} catch (Exception e) {
log.error("批量处理文件下载请求失败", e);
}

return results;
}

/**
* 处理文件下载类型请求
*/
private List<FileDownloadResult> processFileDownloadTypeRequests(String fileType,
List<FileDownloadRequest> requests) {
List<FileDownloadResult> results = new ArrayList<>();

for (FileDownloadRequest request : requests) {
try {
FileDownloadResult result = processFileDownloadRequest(request);
results.add(result);
} catch (Exception e) {
log.error("处理文件下载请求失败: {}", request.getRequestId(), e);
FileDownloadResult errorResult = new FileDownloadResult();
errorResult.setRequestId(request.getRequestId());
errorResult.setStatus(FileDownloadStatus.FAILED);
errorResult.setErrorMessage(e.getMessage());
results.add(errorResult);
}
}

return results;
}

/**
* 处理文件下载请求
*/
private FileDownloadResult processFileDownloadRequest(FileDownloadRequest request) {
// 实现文件下载请求处理逻辑
FileDownloadResult result = new FileDownloadResult();
result.setRequestId(request.getRequestId());
result.setStatus(FileDownloadStatus.SUCCESS);
result.setData("文件下载请求处理成功");
return result;
}

/**
* 预热文件下载性能缓存
*/
@PostConstruct
public void warmupFileDownloadPerformanceCache() {
try {
// 预热常用文件下载性能数据
List<String> commonRequestIds = Arrays.asList("file_download_req_1", "file_download_req_2", "file_download_req_3");

for (String requestId : commonRequestIds) {
try {
String cacheKey = FILE_DOWNLOAD_PERFORMANCE_CACHE_PREFIX + requestId;
Object performanceData = new Object();
cacheFileDownloadPerformanceData(requestId, performanceData);
} catch (Exception e) {
log.error("预热文件下载性能缓存失败: {}", requestId, e);
}
}

} catch (Exception e) {
log.error("预热文件下载性能缓存失败", e);
}
}

/**
* 清理过期缓存
*/
@Scheduled(fixedRate = 300000) // 5分钟
public void cleanupExpiredCache() {
try {
// 清理本地缓存
localCache.cleanUp();

// 清理Redis过期缓存
cleanupRedisExpiredCache();

} catch (Exception e) {
log.error("清理过期缓存失败", e);
}
}

/**
* 清理Redis过期缓存
*/
private void cleanupRedisExpiredCache() {
try {
Set<String> cacheKeys = redisTemplate.keys("redis_cache:" + FILE_DOWNLOAD_PERFORMANCE_CACHE_PREFIX + "*");

for (String key : cacheKeys) {
Long ttl = redisTemplate.getExpire(key);
if (ttl != null && ttl <= 0) {
redisTemplate.delete(key);
}
}

} catch (Exception e) {
log.error("清理Redis过期缓存失败", e);
}
}
}

4.2 监控告警

4.2.1 文件下载监控指标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/**
* 文件下载监控指标
*/
@Component
public class FileDownloadMetrics {

private final MeterRegistry meterRegistry;

public FileDownloadMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}

/**
* 记录文件下载次数
*/
public void recordFileDownloadCount(String fileType, String strategy) {
Counter.builder("file_download.count")
.description("文件下载次数")
.tag("file_type", fileType)
.tag("strategy", strategy)
.register(meterRegistry)
.increment();
}

/**
* 记录文件下载时间
*/
public void recordFileDownloadTime(String fileType, String strategy, long duration) {
Timer.builder("file_download.time")
.description("文件下载时间")
.tag("file_type", fileType)
.tag("strategy", strategy)
.register(meterRegistry)
.record(duration, TimeUnit.MILLISECONDS);
}

/**
* 记录文件下载大小
*/
public void recordFileDownloadSize(String fileType, long size) {
Counter.builder("file_download.size")
.description("文件下载大小")
.tag("file_type", fileType)
.register(meterRegistry)
.increment(size);
}

/**
* 记录文件下载成功率
*/
public void recordFileDownloadSuccessRate(String fileType, double successRate) {
Gauge.builder("file_download.success.rate")
.description("文件下载成功率")
.tag("file_type", fileType)
.register(meterRegistry, successRate);
}

/**
* 记录文件下载失败率
*/
public void recordFileDownloadFailureRate(String fileType, double failureRate) {
Gauge.builder("file_download.failure.rate")
.description("文件下载失败率")
.tag("file_type", fileType)
.register(meterRegistry, failureRate);
}

/**
* 记录文件下载吞吐量
*/
public void recordFileDownloadThroughput(String fileType, double throughput) {
Gauge.builder("file_download.throughput")
.description("文件下载吞吐量")
.tag("file_type", fileType)
.register(meterRegistry, throughput);
}

/**
* 记录文件下载异常次数
*/
public void recordFileDownloadExceptionCount(String fileType, String exceptionType) {
Counter.builder("file_download.exception.count")
.description("文件下载异常次数")
.tag("file_type", fileType)
.tag("exception_type", exceptionType)
.register(meterRegistry)
.increment();
}
}

4.2.2 文件下载告警规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# prometheus-rules.yml
groups:
- name: file_download_alerts
rules:
- alert: HighFileDownloadTime
expr: file_download_time{quantile="0.95"} > 30000
for: 2m
labels:
severity: warning
annotations:
summary: "文件下载时间过长"
description: "文件下载时间P95超过30秒,当前值: {{ $value }}ms"

- alert: HighFileDownloadFailureRate
expr: file_download_failure_rate > 0.05
for: 2m
labels:
severity: warning
annotations:
summary: "文件下载失败率过高"
description: "文件下载失败率超过5%,当前值: {{ $value }}"

- alert: LowFileDownloadThroughput
expr: file_download_throughput < 10
for: 5m
labels:
severity: warning
annotations:
summary: "文件下载吞吐量过低"
description: "文件下载吞吐量低于10次/秒,当前值: {{ $value }}"

- alert: HighFileDownloadExceptionCount
expr: rate(file_download_exception_count[5m]) > 5
for: 2m
labels:
severity: critical
annotations:
summary: "文件下载异常次数过多"
description: "文件下载异常频率超过5次/分钟,当前值: {{ $value }}"

- alert: FileDownloadServiceDown
expr: up{job="file-download-service"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "文件下载服务宕机"
description: "文件下载服务已宕机超过1分钟"

五、总结

SpringBoot文件下载作为现代Web应用的核心功能之一,通过合理的文件下载策略和性能优化,能够构建一个高效、稳定、可扩展的文件下载系统。本文从文件下载策略到性能优化,从基础实现到企业级应用,系统梳理了SpringBoot文件下载的完整解决方案。

5.1 关键要点

  1. 下载策略:通过多种下载策略实现不同场景下的文件下载需求
  2. 性能优化:通过缓存、压缩、CDN等手段优化文件下载性能
  3. 安全控制:通过权限控制、访问控制、防盗链等机制保障文件安全
  4. 监控告警:建立完善的监控体系,及时发现和处理问题
  5. 企业级方案:提供完整的企业级部署和监控方案

5.2 最佳实践

  1. 策略选择:根据文件大小、网络环境、用户需求选择合适的下载策略
  2. 缓存策略:合理使用缓存,减少文件读取压力
  3. 性能优化:通过压缩、CDN、并发优化等手段提高下载性能
  4. 安全控制:实现权限控制、访问控制、防盗链等安全机制
  5. 监控告警:建立完善的监控体系,确保文件下载服务稳定运行

通过以上措施,可以构建一个高效、稳定、可扩展的SpringBoot文件下载系统,为企业的各种业务场景提供文件下载支持。