Проблема с обработкой файла в многопоточном режиме

Проблема с обработкой файла в многопоточном режиме

Есть код контроллера и сервиса, который обрабатывает файл и выводит список email в консоль

import com.example.file_multithreading_problem.services.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@RestController
@RequestMapping
public class FileController {

    private final FileService fileService;

    @Autowired
    public FileController(FileService fileService) {
        this.fileService = fileService;
    }

    @PostMapping(value = "/upload-from-files", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<Void> saveFromFiles(@RequestPart(value = "files", required = false) List<MultipartFile> files) {
        fileService.parseValuesFromFileToDTO(files);
        return new ResponseEntity<>(HttpStatus.OK);
    }
}
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;

@Service
public class FileService {
    static final Logger log = LoggerFactory.getLogger(FileService.class);
    public void parseValuesFromFileToDTO(List<MultipartFile> files) {
        CompletableFuture<Void> filesDtoFromFile = CompletableFuture.runAsync(() ->
        {
            for(MultipartFile file : files) {
                final Integer cellIndex = 0;
                final Integer sheetIndex = 0;
                final Integer limitValues = 100;
                try {
                    Set<String> emailsFromFile = parseCellValueByCellIndexAndSheetIndexWithLimitValues(cellIndex,
                            sheetIndex,
                            limitValues,
                            file.getBytes());
                    for (String email:emailsFromFile) {
                        log.info(email);
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

        }, Executors.newSingleThreadExecutor()).exceptionally((e) -> {
            log.error("Ошибка парсинга значений из файла(ов)", e);
            return null;
        });
        filesDtoFromFile.thenRun(() -> log.info("Чтение данных из файла(ов) завершено"));
    }

    /***
     * Получение списка уникальных значений из файла из заданного листа и колонки по индексу с ограничением количества успешно считанных значений
     *
     * @param cellIndex  - индекс колонки файла.
     * @param sheetIndex - индекс листа файла.
     * @param limit      - ограничение по количеству значений итогового списка.
     *
     * @return Коллекция уникальных записей из файла.
     */
    public static Set<String> parseCellValueByCellIndexAndSheetIndexWithLimitValues(Integer cellIndex, Integer sheetIndex,
                                                                                    Integer limit, byte[] fileBytes) throws IOException {
        Set<String> values = new HashSet<>();

        try (Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(fileBytes))) {
            Sheet sheet = workbook.getSheetAt(sheetIndex);
            for (Row row : sheet) {
                Cell cell = row.getCell(cellIndex);
                if (!ObjectUtils.isEmpty(cell) && Objects.equals(cell.getCellType(), CellType.STRING)) {
                    String value = cell.getStringCellValue();
                    if (!ObjectUtils.isEmpty(value)) {
                        values.add(value.toLowerCase());
                    }
                    if (Objects.nonNull(limit) && values.size() >= limit) {
                        break;
                    }
                }
            }
        }
        return values;
    }
}

Если отправить один файл в запросе, то всё работает

2025-04-03T22:03:51.803+03:00  INFO 15940 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService               : [email protected]
2025-04-03T22:03:51.804+03:00  INFO 15940 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService               : [email protected]
2025-04-03T22:03:51.804+03:00  INFO 15940 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService               : [email protected]
2025-04-03T22:03:51.804+03:00  INFO 15940 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService               : [email protected]
2025-04-03T22:03:51.804+03:00  INFO 15940 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService               : [email protected]
2025-04-03T22:03:51.804+03:00  INFO 15940 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService               : Чтение данных из файла(ов) завершено

Если отправить два файла, то в сервис файлы попадают, а дальше при обработки в многопоточном режиме падают с ошибкой

2025-04-04T20:43:33.524+03:00  INFO 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService               : [email protected]
2025-04-04T20:43:33.524+03:00  INFO 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService               : [email protected]
2025-04-04T20:43:33.524+03:00  INFO 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService               : [email protected]
2025-04-04T20:43:33.524+03:00  INFO 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService               : [email protected]
2025-04-04T20:43:33.524+03:00  INFO 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService               : [email protected]
2025-04-04T20:43:33.525+03:00 ERROR 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService               : Ошибка парсинга значений из файла(ов)

java.util.concurrent.CompletionException: java.lang.RuntimeException: java.nio.file.NoSuchFileException: C:\Users\Knd-a-aaosetskiy\AppData\Local\Temp\tomcat.8080.4482325861160796176\work\Tomcat\localhost\ROOT\upload_8740c826_186b_49e1_b8e7_a358cde48630_00000001.tmp
    at java.base/java.util.concurrent.CompletableFuture.wrapInCompletionException(CompletableFuture.java:323) ~[na:na]
    at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:359) ~[na:na]
    at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:364) ~[na:na]
    at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1851) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[na:na]
    at java.base/java.lang.Thread.run(Thread.java:1575) ~[na:na]
Caused by: java.lang.RuntimeException: java.nio.file.NoSuchFileException: C:\Users\Knd-a-aaosetskiy\AppData\Local\Temp\tomcat.8080.4482325861160796176\work\Tomcat\localhost\ROOT\upload_8740c826_186b_49e1_b8e7_a358cde48630_00000001.tmp
    at com.example.file_multithreading_problem.services.FileService.lambda$parseValuesFromFileToDTO$0(FileService.java:43) ~[classes/:na]
    at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1848) ~[na:na]
    ... 3 common frames omitted
Caused by: java.nio.file.NoSuchFileException: C:\Users\Knd-a-aaosetskiy\AppData\Local\Temp\tomcat.8080.4482325861160796176\work\Tomcat\localhost\ROOT\upload_8740c826_186b_49e1_b8e7_a358cde48630_00000001.tmp
    at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:85) ~[na:na]
    at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103) ~[na:na]
    at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108) ~[na:na]
    at java.base/sun.nio.fs.WindowsFileSystemProvider.newByteChannel(WindowsFileSystemProvider.java:234) ~[na:na]
    at java.base/java.nio.file.Files.newByteChannel(Files.java:380) ~[na:na]
    at java.base/java.nio.file.Files.newByteChannel(Files.java:432) ~[na:na]
    at java.base/java.nio.file.spi.FileSystemProvider.newInputStream(FileSystemProvider.java:420) ~[na:na]
    at java.base/java.nio.file.Files.newInputStream(Files.java:160) ~[na:na]
    at org.apache.tomcat.util.http.fileupload.disk.DiskFileItem.getInputStream(DiskFileItem.java:196) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.ApplicationPart.getInputStream(ApplicationPart.java:97) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile.getBytes(StandardMultipartHttpServletRequest.java:259) ~[spring-web-6.2.2.jar:6.2.2]
    at com.example.file_multithreading_problem.services.FileService.lambda$parseValuesFromFileToDTO$0(FileService.java:38) ~[classes/:na]
    ... 4 common frames omitted

2025-04-04T20:43:33.533+03:00  INFO 9020 --- [File Multithreading Problem] [pool-2-thread-1] c.e.f.services.FileService               : Чтение данных из файла(ов) завершено

Почему выпадает ошибка java.util.concurrent.CompletionException?

Полный код с запросами и прикрепленными файлами


Ответы (2 шт):

Автор решения: Alexander Pavlov

Если ты делаешь асинхронную обработку файла, то файл надо вычитать полностью до возврата из ResponseEntity<Void> saveFromFiles, потому что после возврата Томкат думает, что ты всё сделал, и удаляет временный файл.

Альтернативно, можно возвращать твой CompletableFuture Спрингу, и он тогда сам будет следить, когда асинхронная обработка закончилась.

Я сам Спринг уже плохо помню, что-то типа

 public CompletableFuture<Void> parseValuesFromFileToDTO(List<MultipartFile> files) {
   CompletableFuture<Void> filesDtoFromFile = CompletableFuture.runAsync(() ->
   ....
   return filesDtoFromFile.thenRun(v -> log.info("Чтение данных из файла(ов) завершено"));
}

и

@PostMapping(value = "/upload-from-files", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public CompletableFuture<ResponseEntity<Void>> saveFromFiles(@RequestPart(value = "files", required = false) List<MultipartFile> files) {
   return fileService.parseValuesFromFileToDTO(files)
      .thenApply(()->new ResponseEntity<>(HttpStatus.OK));
}
→ Ссылка
Автор решения: Алексей Осецкий

Когда запрос с файлами поступает в контроллер, Spring создает для них временные ресурсы. Проблема возникает, если обработка этих файлов запускается в отдельном потоке (например, через CompletableFuture). Spring, не зная о фоновой задаче, считает обработку запроса завершенной и удаляет временные файлы. Это приводит к ошибке, когда фоновый поток пытается обратиться к уже удаленным данным.

Предложенные решения из моей статьи:

  1. Объявить запрос асинхронным.
  • Этот подход сообщает Spring, что обработка запроса еще продолжается в другом потоке, что предотвращает преждевременную очистку ресурсов.

  • Риск: При большом количестве одновременных запросов это может привести к повышенному потреблению памяти, так как связанные с ними ресурсы будут удерживаться дольше.

  1. Считать файлы в память до запуска фоновой задачи.
  • Данные файлов предварительно полностью загружаются в оперативную память (в виде массива байтов), и фоновый поток работает уже с этой копией, а не с временными файлами.

  • Риск: Если обрабатываются крупные файлы, этот метод может создать значительную нагрузку на оперативную память.

Вывод: Оба подхода решают исходную проблему, но требуют учета потенциальных рисков, связанных с использованием памяти.

→ Ссылка