Java文件下载流程如何规整

wen java案例 24

本文目录导读:

Java文件下载流程如何规整

  1. 基础下载流程架构
  2. 完整实现示例
  3. 高级特性实现
  4. 配置与安全
  5. 最佳实践

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

抱歉,评论功能暂时关闭!