本文目录导读:

我来详细说明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、限流
- 可观测性:日志、监控指标
- 错误处理:统一的异常处理机制