Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Получение полного файла без разбиения на части
При работе с файлами в Java существует несколько подходов для получения полного файла, избегая необходимости обработки его по частям (chunks). Это особенно важно при загрузке больших файлов на сервер.
1. Буферизация в памяти (для малых файлов)
@RestController
@RequestMapping("/api/upload")
public class FileUploadController {
private final FileService fileService;
public FileUploadController(FileService fileService) {
this.fileService = fileService;
}
// Способ 1: Простая загрузка весь файл в память
@PostMapping("/simple")
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
// MultipartFile автоматически содержит весь файл в памяти
byte[] fileContent = file.getBytes(); // Получаем полный файл
String fileName = file.getOriginalFilename();
fileService.saveFile(fileName, fileContent);
return ResponseEntity.ok("File uploaded: " + fileName);
}
}
Проблемы:
- Весь файл загружается в памяти сервера
- Для больших файлов = OutOfMemoryError
- Неэффективно по ресурсам
2. Настройка максимального размера файла
Ограничить размер загружаемого файла в конфигурации:
# application.yml
spring:
servlet:
multipart:
max-file-size: 100MB # Максимальный размер одного файла
max-request-size: 200MB # Максимальный размер всего запроса
file-size-threshold: 2MB # Размер, выше которого файл пишется на диск
@Configuration
public class FileUploadConfig {
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setMaxFileSize(DataSize.ofMegabytes(100)); // 100MB
factory.setMaxRequestSize(DataSize.ofMegabytes(200)); // 200MB
factory.setFileSizeThreshold(DataSize.ofMegabytes(2)); // 2MB threshold
return factory.createMultipartConfig();
}
}
3. Потоковая обработка (Stream) - РЕКОМЕНДУЕТСЯ
Записывать файл на диск по мере получения, не загружая весь в память:
@RestController
@RequestMapping("/api/upload")
public class FileUploadController {
private final FileService fileService;
public FileUploadController(FileService fileService) {
this.fileService = fileService;
}
// Способ 2: Потоковая обработка через InputStream
@PostMapping("/stream")
public ResponseEntity<String> uploadFileStream(@RequestParam("file") MultipartFile file)
throws IOException {
String fileName = file.getOriginalFilename();
// Получаем входной поток, не загружая весь файл в память
try (InputStream inputStream = file.getInputStream()) {
fileService.saveFileStream(fileName, inputStream);
}
return ResponseEntity.ok("File uploaded: " + fileName);
}
}
@Service
public class FileService {
private final String uploadDir = "/var/uploads";
public void saveFileStream(String fileName, InputStream inputStream) throws IOException {
Path filePath = Paths.get(uploadDir, fileName);
// Читаем и пишем файл буферами (обычно 8KB)
Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING);
}
}
4. Ручная потоковая обработка с контролем буфера
@Service
public class FileService {
private static final int BUFFER_SIZE = 8192; // 8KB буфер
private final String uploadDir = "/var/uploads";
public void saveFileWithBuffer(String fileName, InputStream inputStream) throws IOException {
Path filePath = Paths.get(uploadDir, fileName);
try (OutputStream outputStream = Files.newOutputStream(filePath)) {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
// Читаем по частям и сразу пишем на диск
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
}
}
5. Использование RandomAccessFile для больших файлов
@Service
public class FileService {
public void saveFileWithRandomAccess(String fileName, InputStream inputStream,
long totalSize) throws IOException {
Path filePath = Paths.get("/var/uploads", fileName);
try (RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "rw")) {
byte[] buffer = new byte[65536]; // 64KB буфер
int bytesRead;
long totalWritten = 0;
while ((bytesRead = inputStream.read(buffer)) != -1) {
randomAccessFile.write(buffer, 0, bytesRead);
totalWritten += bytesRead;
// Прогресс
System.out.printf("Uploaded: %.2f%%\n", (totalWritten * 100.0) / totalSize);
}
}
}
}
6. Chunked Upload с восстановлением (для надёжности)
Если соединение может разорваться, используй resumable upload:
@RestController
@RequestMapping("/api/upload")
public class ChunkedUploadController {
private final ChunkedFileService chunkedFileService;
public ChunkedUploadController(ChunkedFileService chunkedFileService) {
this.chunkedFileService = chunkedFileService;
}
@PostMapping("/chunk")
public ResponseEntity<String> uploadChunk(
@RequestParam("file") MultipartFile chunk,
@RequestParam("uploadId") String uploadId,
@RequestParam("chunkIndex") int chunkIndex,
@RequestParam("totalChunks") int totalChunks) throws IOException {
chunkedFileService.saveChunk(uploadId, chunkIndex, chunk.getInputStream());
// Если все части загружены, объединяем файл
if (chunkedFileService.isComplete(uploadId, totalChunks)) {
File completeFile = chunkedFileService.mergeChunks(uploadId);
return ResponseEntity.ok("Upload complete: " + completeFile.getName());
}
return ResponseEntity.ok("Chunk " + chunkIndex + " received");
}
}
@Service
public class ChunkedFileService {
private final String tempDir = "/var/uploads/temp";
private final ConcurrentHashMap<String, Set<Integer>> uploadProgress = new ConcurrentHashMap<>();
public void saveChunk(String uploadId, int chunkIndex, InputStream inputStream)
throws IOException {
Path chunkPath = Paths.get(tempDir, uploadId, "chunk_" + chunkIndex);
Files.createDirectories(chunkPath.getParent());
Files.copy(inputStream, chunkPath, StandardCopyOption.REPLACE_EXISTING);
// Отслеживаем загруженные части
uploadProgress.computeIfAbsent(uploadId, k -> ConcurrentHashMap.newKeySet())
.add(chunkIndex);
}
public boolean isComplete(String uploadId, int totalChunks) {
Set<Integer> chunks = uploadProgress.getOrDefault(uploadId, Collections.emptySet());
return chunks.size() == totalChunks;
}
public File mergeChunks(String uploadId) throws IOException {
Path uploadPath = Paths.get(tempDir, uploadId);
File completeFile = new File("/var/uploads/", uploadId + ".bin");
try (OutputStream outputStream = new FileOutputStream(completeFile)) {
// Объединяем части в правильном порядке
Files.list(uploadPath)
.sorted(Comparator.comparing(p -> {
String name = p.getFileName().toString();
return Integer.parseInt(name.replace("chunk_", ""));
}))
.forEach(chunkPath -> {
try (InputStream inputStream = Files.newInputStream(chunkPath)) {
inputStream.transferTo(outputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
// Очищаем временные файлы
Files.walk(uploadPath)
.sorted(Comparator.reverseOrder())
.forEach(path -> {
try {
Files.delete(path);
} catch (IOException e) {
System.err.println("Failed to delete: " + path);
}
});
uploadProgress.remove(uploadId);
return completeFile;
}
}
7. Использование NIO для эффективной передачи
@Service
public class FileService {
public void saveFileWithNio(String fileName, InputStream inputStream) throws IOException {
Path filePath = Paths.get("/var/uploads", fileName);
try (ReadableByteChannel readableChannel = Channels.newChannel(inputStream);
FileChannel fileChannel = FileChannel.open(
filePath,
StandardOpenOption.WRITE,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING)) {
ByteBuffer buffer = ByteBuffer.allocate(8192);
while (readableChannel.read(buffer) > 0) {
buffer.flip();
fileChannel.write(buffer);
buffer.clear();
}
}
}
}
8. Валидация и безопасность
@RestController
@RequestMapping("/api/upload")
public class SecureFileUploadController {
private final FileService fileService;
private static final long MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
private static final Set<String> ALLOWED_TYPES = Set.of(
"application/pdf",
"image/jpeg",
"image/png",
"text/plain"
);
@PostMapping("/secure")
public ResponseEntity<String> uploadSecure(@RequestParam("file") MultipartFile file)
throws IOException {
// Проверка размера
if (file.getSize() > MAX_FILE_SIZE) {
return ResponseEntity.badRequest()
.body("File size exceeds limit: " + MAX_FILE_SIZE);
}
// Проверка типа
if (!ALLOWED_TYPES.contains(file.getContentType())) {
return ResponseEntity.badRequest()
.body("File type not allowed: " + file.getContentType());
}
// Проверка имени файла (защита от path traversal)
String fileName = file.getOriginalFilename();
if (fileName == null || fileName.contains("..")) {
return ResponseEntity.badRequest()
.body("Invalid file name");
}
// Используем UUID вместо оригинального имени
String secureFileName = UUID.randomUUID() + "_" +
fileName.replaceAll("[^a-zA-Z0-9._-]", "");
try (InputStream inputStream = file.getInputStream()) {
fileService.saveFileStream(secureFileName, inputStream);
}
return ResponseEntity.ok("File uploaded: " + secureFileName);
}
}
Сравнение подходов
| Подход | Память | Скорость | Надёжность | Когда использовать |
|---|---|---|---|---|
| В памяти | Высокое потребление | Быстро | Низкая для больших файлов | Файлы < 10MB |
| Потоковая обработка | Минимальное | Средняя | Средняя | Файлы > 10MB |
| Chunked upload | Минимальное | Медленнее | Высокая (возобновляемая) | Большие файлы, плохая сеть |
| NIO | Минимальное | Быстро | Средняя | Очень большие файлы |
Рекомендации
- Для файлов < 10MB: потоковая обработка (способ 3)
- Для файлов > 10MB: chunked upload с возобновлением (способ 6)
- Для максимальной производительности: NIO (способ 7)
- ВСЕГДА: валидируйте размер, тип и имя файла (способ 8)
Потоковая обработка и chunked upload гарантируют, что вы не получите OutOfMemoryError даже при загрузке файлов размером в гигабайты.