本文目录导读:

我来详细说明Java文件下载流程的规整化方案:
基础下载流程架构
核心流程步骤
请求接收 → 权限验证 → 文件定位 → 流处理 → 响应输出 → 清理资源
完整实现示例
1 Controller层(REST API)
@RestController
@RequestMapping("/api/file")
public class FileDownloadController {
@Autowired
private FileDownloadService fileDownloadService;
@GetMapping("/download/{fileId}")
public ResponseEntity<Resource> downloadFile(
@PathVariable String fileId,
@RequestHeader(value = "Range", required = false) String rangeHeader,
HttpServletRequest request) {
try {
// 1. 获取文件信息
FileInfo fileInfo = fileDownloadService.getFileInfo(fileId);
// 2. 检查文件是否存在
if (fileInfo == null) {
return ResponseEntity.notFound().build();
}
// 3. 加载文件资源
Resource resource = fileDownloadService.loadFileAsResource(fileInfo);
// 4. 判断文件类型
String contentType = determineContentType(request, resource);
// 5. 构建响应
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + fileInfo.getFileName() + "\"")
.header(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileInfo.getSize()))
.body(resource);
} catch (FileNotFoundException e) {
return ResponseEntity.notFound().build();
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
2 Service层(业务逻辑)
@Service
@Transactional
public class FileDownloadService {
@Autowired
private FileRepository fileRepository;
@Autowired
private FileStorageService storageService;
@Autowired
private AuditLogService auditLogService;
// 获取文件信息
public FileInfo getFileInfo(String fileId) {
return fileRepository.findById(fileId)
.orElseThrow(() -> new FileNotFoundException("File not found: " + fileId));
}
// 加载文件资源
public Resource loadFileAsResource(FileInfo fileInfo) {
Path filePath = storageService.getFilePath(fileInfo.getStoragePath());
Resource resource = new UrlResource(filePath.toUri());
if (!resource.exists() || !resource.isReadable()) {
throw new FileNotFoundException("File not accessible: " + fileInfo.getFileName());
}
return resource;
}
// 大文件分片下载
public void downloadInChunks(FileInfo fileInfo, HttpServletResponse response,
String rangeHeader) throws IOException {
Path filePath = storageService.getFilePath(fileInfo.getStoragePath());
File file = filePath.toFile();
long fileLength = file.length();
// 解析Range头
Range range = parseRangeHeader(rangeHeader, fileLength);
// 设置响应头
response.setContentType("application/octet-stream");
response.setHeader("Accept-Ranges", "bytes");
if (range == null) {
// 完整文件下载
response.setHeader("Content-Length", String.valueOf(fileLength));
try (InputStream is = new FileInputStream(file);
OutputStream os = response.getOutputStream()) {
IOUtils.copy(is, os);
}
} else {
// 分片下载
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setHeader("Content-Range",
"bytes " + range.start + "-" + range.end + "/" + fileLength);
response.setHeader("Content-Length",
String.valueOf(range.end - range.start + 1));
try (RandomAccessFile raf = new RandomAccessFile(file, "r");
OutputStream os = response.getOutputStream()) {
raf.seek(range.start);
byte[] buffer = new byte[8192];
long remaining = range.end - range.start + 1;
while (remaining > 0) {
int bytesRead = raf.read(buffer, 0, (int) Math.min(buffer.length, remaining));
if (bytesRead == -1) break;
os.write(buffer, 0, bytesRead);
remaining -= bytesRead;
}
}
}
}
// 记录下载日志
private void logDownload(FileInfo fileInfo, String userId) {
AuditLog log = new AuditLog();
log.setFileId(fileInfo.getId());
log.setFileName(fileInfo.getFileName());
log.setUserId(userId);
log.setDownloadTime(LocalDateTime.now());
log.setIpAddress(getClientIp());
auditLogService.save(log);
}
}
3 文件存储服务
@Component
public class FileStorageService {
@Value("${file.upload-dir}")
private String uploadDir;
@Value("${file.max-chunk-size:10485760}") // 10MB默认分片大小
private long maxChunkSize;
public Path getFilePath(String storagePath) {
return Paths.get(uploadDir).resolve(storagePath).normalize();
}
// 创建临时文件(用于压缩等)
public Path createTempFile(String prefix, String suffix) throws IOException {
return Files.createTempFile(Paths.get(uploadDir), prefix, suffix);
}
// 清除临时文件
public void cleanupTempFile(Path tempFile) {
try {
Files.deleteIfExists(tempFile);
} catch (IOException e) {
log.warn("Failed to delete temp file: {}", tempFile);
}
}
}
4 工具类
@Component
public class FileDownloadUtils {
// 确定内容类型
public static String determineContentType(HttpServletRequest request, Resource resource) {
String contentType = null;
try {
contentType = request.getServletContext()
.getMimeType(resource.getFile().getAbsolutePath());
} catch (IOException ex) {
log.warn("Could not determine file type");
}
// 默认类型
if (contentType == null) {
contentType = "application/octet-stream";
}
return contentType;
}
// 解析Range头
public static Range parseRangeHeader(String rangeHeader, long fileLength) {
if (rangeHeader == null || rangeHeader.isEmpty()) {
return null;
}
try {
// 格式: bytes=start-end
String[] parts = rangeHeader.replace("bytes=", "").split("-");
long start = Long.parseLong(parts[0]);
long end = parts.length > 1 && !parts[1].isEmpty() ?
Long.parseLong(parts[1]) : fileLength - 1;
// 验证范围
if (start < 0 || end >= fileLength || start > end) {
return null;
}
return new Range(start, end);
} catch (NumberFormatException e) {
return null;
}
}
// Range内部类
@Data
@AllArgsConstructor
public static class Range {
private long start;
private long end;
}
}
高级特性实现
1 压缩下载
public void downloadAsZip(List<String> fileIds, HttpServletResponse response) {
response.setContentType("application/zip");
response.setHeader("Content-Disposition",
"attachment; filename=\"download.zip\"");
try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
for (String fileId : fileIds) {
FileInfo fileInfo = fileRepository.findById(fileId).orElse(null);
if (fileInfo == null) continue;
Path filePath = storageService.getFilePath(fileInfo.getStoragePath());
ZipEntry zipEntry = new ZipEntry(fileInfo.getFileName());
zos.putNextEntry(zipEntry);
Files.copy(filePath, zos);
zos.closeEntry();
}
} catch (IOException e) {
throw new RuntimeException("Error creating zip file", e);
}
}
2 断点续传
public void resumeDownload(FileInfo fileInfo, HttpServletResponse response,
long startByte) throws IOException {
Path filePath = storageService.getFilePath(fileInfo.getStoragePath());
File file = filePath.toFile();
long fileLength = file.length();
// 验证起始位置
if (startByte >= fileLength) {
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return;
}
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("Content-Range",
"bytes " + startByte + "-" + (fileLength - 1) + "/" + fileLength);
response.setHeader("Content-Length",
String.valueOf(fileLength - startByte));
try (RandomAccessFile raf = new RandomAccessFile(file, "r");
OutputStream os = response.getOutputStream()) {
raf.seek(startByte);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = raf.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
}
}
3 速率限制
@Component
public class RateLimitingFileDownloadService {
private final RateLimiter rateLimiter = RateLimiter.create(50.0); // 50 MB/s
public void downloadWithRateLimit(FileInfo fileInfo, HttpServletResponse response) {
Path filePath = storageService.getFilePath(fileInfo.getStoragePath());
response.setContentType("application/octet-stream");
response.setHeader("Content-Length", String.valueOf(fileInfo.getSize()));
try (InputStream is = Files.newInputStream(filePath);
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
long totalBytesRead = 0;
while ((bytesRead = is.read(buffer)) != -1) {
// 速率限制
rateLimiter.acquire(bytesRead);
os.write(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
}
} catch (IOException e) {
throw new RuntimeException("Download failed", e);
}
}
}
配置与安全
1 配置文件
# application.yml
file:
upload-dir: ./uploads
max-chunk-size: 10485760
download:
rate-limit: 50MB
buffer-size: 8192
temp-dir: ./temp
2 安全措施
@Configuration
public class FileDownloadSecurityConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/file/**")
.allowedOrigins("https://trusted-domain.com")
.allowedMethods("GET", "HEAD")
.allowedHeaders("Range", "Authorization");
}
};
}
@Bean
public Filter pathTraversalFilter() {
return (request, response, chain) -> {
String fileId = request.getParameter("fileId");
if (fileId != null && fileId.contains("..")) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
chain.doFilter(request, response);
};
}
}
最佳实践
1 错误处理
@ControllerAdvice
public class FileDownloadExceptionHandler {
@ExceptionHandler(FileNotFoundException.class)
public ResponseEntity<?> handleFileNotFound(FileNotFoundException e) {
return ResponseEntity.notFound().build();
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<?> handleAccessDenied(AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleGeneralException(Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Download failed: " + e.getMessage());
}
}
2 监控指标
@Component
public class DownloadMetrics {
private final MeterRegistry meterRegistry;
private final Counter downloadCounter;
private final DistributionSummary downloadSize;
public DownloadMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.downloadCounter = Counter.builder("file.downloads.total")
.description("Total file downloads")
.register(meterRegistry);
this.downloadSize = DistributionSummary.builder("file.download.size")
.description("Download file size distribution")
.register(meterRegistry);
}
public void recordDownload(long fileSize) {
downloadCounter.increment();
downloadSize.record(fileSize);
}
}
这个规整的下载流程涵盖了:
- 完整性:完整的请求生命周期
- 可扩展性:模块化设计,便于扩展
- 性能优化:支持分片、断点续传
- 安全考虑:路径遍历防护、CORS、限流
- 可观测性:日志、监控指标
- 错误处理:统一的异常处理机制
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播