← Назад к вопросам

Что поможет не получать файлы по частям

2.0 Middle🔥 71 комментариев
#Основы Java

Комментарии (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 даже при загрузке файлов размером в гигабайты.

Что поможет не получать файлы по частям | PrepBro